diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index ad738744d..e6f952c3e 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -201,147 +201,11 @@ jobs: Premium/Android/app/build/reports/tests/ Premium/Android/app/build/reports/lint-results.html - # Golang Desktop Bridge Build - desktop-bridge: - name: Desktop Bridge Build - runs-on: ${{ matrix.os }} - needs: security - strategy: - matrix: - os: [ubuntu-latest, windows-latest, macos-latest] - go-version: ['1.21'] - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up Go - uses: actions/setup-go@v4 - with: - go-version: ${{ matrix.go-version }} - - - name: Cache Go modules - uses: actions/cache@v3 - with: - path: | - ~/.cache/go-build - ~/go/pkg/mod - key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} - restore-keys: | - ${{ runner.os }}-go- - - - name: Download dependencies - run: go mod download - working-directory: Premium/Desktop - - - name: Run Go vet - run: go vet ./... - working-directory: Premium/Desktop - - - name: Run Go fmt check - run: | - if [ "$(gofmt -s -l . | wc -l)" -gt 0 ]; then - echo "Code is not formatted properly" - gofmt -s -l . - exit 1 - fi - shell: bash - working-directory: Premium/Desktop - - - name: Install test dependencies - run: | - go install github.com/jstemmer/go-junit-report/v2@latest - go install github.com/axw/gocov/gocov@latest - go install github.com/AlekSi/gocov-xml@latest - shell: bash - - - name: Run unit tests with coverage - run: | - go test -v -race -coverprofile=coverage.out -covermode=atomic ./... 2>&1 | tee test-output.txt - go-junit-report -in test-output.txt -out test-results.xml - gocov convert coverage.out | gocov-xml > coverage.xml - working-directory: Premium/Desktop - shell: bash - - - name: Run integration tests - run: go test -v -race -tags=integration ./... 2>&1 | tee integration-test-output.txt - working-directory: Premium/Desktop - shell: bash - - - name: Upload test results - uses: actions/upload-artifact@v3 - if: always() - with: - name: test-results-${{ matrix.os }} - path: | - Premium/Desktop/test-results.xml - Premium/Desktop/coverage.xml - Premium/Desktop/coverage.out - Premium/Desktop/integration-test-output.txt - - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v3 - with: - file: Premium/Desktop/coverage.out - flags: ${{ matrix.os }} - name: codecov-${{ matrix.os }} - - - name: Build for current platform - run: | - mkdir -p build - go build -ldflags="-s -w -X main.version=${{ github.sha }}" -o build/waddlebot-bridge-${{ runner.os }}-${{ runner.arch }} ./cmd/ - working-directory: Premium/Desktop - shell: bash - - - name: Build cross-platform binaries (Linux only) - if: matrix.os == 'ubuntu-latest' - run: | - # Windows - GOOS=windows GOARCH=amd64 go build -ldflags="-s -w -X main.version=${{ github.sha }}" -o build/waddlebot-bridge-windows-amd64.exe ./cmd/ - # macOS - GOOS=darwin GOARCH=amd64 go build -ldflags="-s -w -X main.version=${{ github.sha }}" -o build/waddlebot-bridge-darwin-amd64 ./cmd/ - GOOS=darwin GOARCH=arm64 go build -ldflags="-s -w -X main.version=${{ github.sha }}" -o build/waddlebot-bridge-darwin-arm64 ./cmd/ - # Linux - GOOS=linux GOARCH=amd64 go build -ldflags="-s -w -X main.version=${{ github.sha }}" -o build/waddlebot-bridge-linux-amd64 ./cmd/ - GOOS=linux GOARCH=arm64 go build -ldflags="-s -w -X main.version=${{ github.sha }}" -o build/waddlebot-bridge-linux-arm64 ./cmd/ - working-directory: Premium/Desktop - shell: bash - - - name: Upload build artifacts - uses: actions/upload-artifact@v3 - with: - name: desktop-bridge-${{ matrix.os }} - path: Premium/Desktop/build/ - - # Performance Tests - performance: - name: Performance Tests - runs-on: ubuntu-latest - needs: [containers, desktop-bridge] - if: github.event_name == 'push' && github.ref == 'refs/heads/main' - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up Go - uses: actions/setup-go@v4 - with: - go-version: '1.21' - - - name: Run benchmarks - run: go test -bench=. -benchmem ./... | tee benchmark-results.txt - working-directory: Premium/Desktop - - - name: Upload benchmark results - uses: actions/upload-artifact@v3 - with: - name: benchmark-results - path: Premium/Desktop/benchmark-results.txt - # Deployment (only on main branch) deploy: name: Deploy runs-on: ubuntu-latest - needs: [containers, android, desktop-bridge] + needs: [containers, android] if: github.event_name == 'push' && github.ref == 'refs/heads/main' environment: production steps: @@ -474,17 +338,17 @@ jobs: notify: name: Notify runs-on: ubuntu-latest - needs: [containers, android, desktop-bridge, deploy, deploy-k8s] + needs: [containers, android, deploy, deploy-k8s] if: always() steps: - name: Notify on success - if: needs.containers.result == 'success' && needs.android.result == 'success' && needs.desktop-bridge.result == 'success' + if: needs.containers.result == 'success' && needs.android.result == 'success' run: | echo "✅ All builds completed successfully!" # Add notification logic here (Slack, Discord, etc.) - name: Notify on failure - if: needs.containers.result == 'failure' || needs.android.result == 'failure' || needs.desktop-bridge.result == 'failure' + if: needs.containers.result == 'failure' || needs.android.result == 'failure' run: | echo "❌ Some builds failed!" # Add notification logic here (Slack, Discord, etc.) \ No newline at end of file diff --git a/.github/workflows/desktop-bridge.yml b/.github/workflows/desktop-bridge.yml deleted file mode 100644 index ab731eeae..000000000 --- a/.github/workflows/desktop-bridge.yml +++ /dev/null @@ -1,566 +0,0 @@ -name: Desktop Bridge Build - -on: - push: - branches: [ main, develop ] - paths: - - 'Premium/Desktop/**' - - '.github/workflows/desktop-bridge.yml' - pull_request: - branches: [ main, develop ] - paths: - - 'Premium/Desktop/**' - - '.github/workflows/desktop-bridge.yml' - workflow_dispatch: - -env: - GO_VERSION: '1.21' - CGO_ENABLED: 0 - -jobs: - # Code Quality and Security - code-quality: - name: Code Quality & Security - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up Go - uses: actions/setup-go@v4 - with: - go-version: ${{ env.GO_VERSION }} - - - name: Cache Go modules - uses: actions/cache@v3 - with: - path: | - ~/.cache/go-build - ~/go/pkg/mod - key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} - restore-keys: | - ${{ runner.os }}-go- - - - name: Download dependencies - run: go mod download - working-directory: Premium/Desktop - - - name: Verify dependencies - run: go mod verify - working-directory: Premium/Desktop - - - name: Run go vet - run: go vet ./... - working-directory: Premium/Desktop - - - name: Run go fmt check - run: | - if [ "$(gofmt -s -l . | wc -l)" -gt 0 ]; then - echo "Code is not formatted properly:" - gofmt -s -l . - exit 1 - fi - working-directory: Premium/Desktop - - - name: Install security tools - run: | - go install github.com/securecodewarrior/gosec/v2/cmd/gosec@latest - go install honnef.co/go/tools/cmd/staticcheck@latest - - - name: Run gosec security scanner - run: gosec -fmt sarif -out gosec-report.sarif ./... - working-directory: Premium/Desktop - continue-on-error: true - - - name: Upload gosec results - uses: github/codeql-action/upload-sarif@v3 - if: always() - with: - sarif_file: Premium/Desktop/gosec-report.sarif - - - name: Run staticcheck - run: staticcheck ./... - working-directory: Premium/Desktop - - # Unit Tests - unit-tests: - name: Unit Tests - runs-on: ${{ matrix.os }} - needs: code-quality - strategy: - matrix: - os: [ubuntu-latest, windows-latest, macos-latest] - go-version: ['1.21', '1.22'] - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up Go - uses: actions/setup-go@v4 - with: - go-version: ${{ matrix.go-version }} - - - name: Cache Go modules - uses: actions/cache@v3 - with: - path: | - ~/.cache/go-build - ~/go/pkg/mod - key: ${{ runner.os }}-go-${{ matrix.go-version }}-${{ hashFiles('**/go.sum') }} - restore-keys: | - ${{ runner.os }}-go-${{ matrix.go-version }}- - - - name: Download dependencies - run: go mod download - working-directory: Premium/Desktop - - - name: Install test dependencies - run: | - go install github.com/jstemmer/go-junit-report/v2@latest - go install github.com/axw/gocov/gocov@latest - go install github.com/AlekSi/gocov-xml@latest - shell: bash - - - name: Run unit tests - run: | - go test -v -race -coverprofile=coverage.out -covermode=atomic ./internal/... 2>&1 | tee test-output.txt - go-junit-report -in test-output.txt -out test-results.xml - working-directory: Premium/Desktop - shell: bash - - - name: Generate coverage report - run: | - gocov convert coverage.out | gocov-xml > coverage.xml - go tool cover -html=coverage.out -o coverage.html - working-directory: Premium/Desktop - shell: bash - - - name: Upload test results - uses: actions/upload-artifact@v3 - if: always() - with: - name: test-results-${{ matrix.os }}-go${{ matrix.go-version }} - path: | - Premium/Desktop/test-results.xml - Premium/Desktop/coverage.xml - Premium/Desktop/coverage.html - Premium/Desktop/coverage.out - - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v3 - with: - file: Premium/Desktop/coverage.out - flags: ${{ matrix.os }}-go${{ matrix.go-version }} - name: codecov-${{ matrix.os }}-go${{ matrix.go-version }} - - # Integration Tests - integration-tests: - name: Integration Tests - runs-on: ${{ matrix.os }} - needs: unit-tests - strategy: - matrix: - os: [ubuntu-latest, windows-latest, macos-latest] - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up Go - uses: actions/setup-go@v4 - with: - go-version: ${{ env.GO_VERSION }} - - - name: Cache Go modules - uses: actions/cache@v3 - with: - path: | - ~/.cache/go-build - ~/go/pkg/mod - key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} - restore-keys: | - ${{ runner.os }}-go- - - - name: Download dependencies - run: go mod download - working-directory: Premium/Desktop - - - name: Run integration tests - run: go test -v -race -tags=integration ./... 2>&1 | tee integration-test-output.txt - working-directory: Premium/Desktop - shell: bash - - - name: Upload integration test results - uses: actions/upload-artifact@v3 - if: always() - with: - name: integration-test-results-${{ matrix.os }} - path: Premium/Desktop/integration-test-output.txt - - # Build Binaries - build: - name: Build Binaries - runs-on: ${{ matrix.os }} - needs: unit-tests - strategy: - matrix: - include: - - os: ubuntu-latest - goos: linux - goarch: amd64 - binary_name: waddlebot-bridge-linux-amd64 - - os: ubuntu-latest - goos: linux - goarch: arm64 - binary_name: waddlebot-bridge-linux-arm64 - - os: windows-latest - goos: windows - goarch: amd64 - binary_name: waddlebot-bridge-windows-amd64.exe - - os: macos-latest - goos: darwin - goarch: amd64 - binary_name: waddlebot-bridge-darwin-amd64 - - os: macos-latest - goos: darwin - goarch: arm64 - binary_name: waddlebot-bridge-darwin-arm64 - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up Go - uses: actions/setup-go@v4 - with: - go-version: ${{ env.GO_VERSION }} - - - name: Cache Go modules - uses: actions/cache@v3 - with: - path: | - ~/.cache/go-build - ~/go/pkg/mod - key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} - restore-keys: | - ${{ runner.os }}-go- - - - name: Download dependencies - run: go mod download - working-directory: Premium/Desktop - - - name: Build binary - run: | - mkdir -p build - go build -ldflags="-s -w -X main.version=${{ github.sha }} -X main.buildTime=$(date -u +%Y-%m-%dT%H:%M:%S)Z" -o build/${{ matrix.binary_name }} ./cmd/ - working-directory: Premium/Desktop - env: - CGO_ENABLED: ${{ env.CGO_ENABLED }} - GOOS: ${{ matrix.goos }} - GOARCH: ${{ matrix.goarch }} - shell: bash - - - name: Test binary - if: matrix.goos == runner.os || (matrix.goos == 'linux' && runner.os == 'Linux') || (matrix.goos == 'windows' && runner.os == 'Windows') || (matrix.goos == 'darwin' && runner.os == 'macOS') - run: | - if [ "${{ matrix.goos }}" = "windows" ]; then - ./build/${{ matrix.binary_name }} --version - else - ./build/${{ matrix.binary_name }} --version - fi - working-directory: Premium/Desktop - shell: bash - - - name: Upload binary - uses: actions/upload-artifact@v3 - with: - name: binary-${{ matrix.goos }}-${{ matrix.goarch }} - path: Premium/Desktop/build/${{ matrix.binary_name }} - - # Build Docker Image - build-docker: - name: Build Docker Image - runs-on: ubuntu-latest - needs: unit-tests - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Log in to Container Registry - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract metadata - id: meta - uses: docker/metadata-action@v5 - with: - images: ghcr.io/${{ github.repository }}/desktop-bridge - tags: | - type=ref,event=branch - type=ref,event=pr - type=sha,prefix={{branch}}- - type=raw,value=latest,enable={{is_default_branch}} - - - name: Build and push Docker image - uses: docker/build-push-action@v5 - with: - context: Premium/Desktop - file: Premium/Desktop/Dockerfile - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - platforms: linux/amd64,linux/arm64 - cache-from: type=gha,scope=desktop-bridge - cache-to: type=gha,mode=max,scope=desktop-bridge - build-args: | - VERSION=${{ github.sha }} - BUILD_TIME=$(date -u +%Y-%m-%dT%H:%M:%S)Z - - # Benchmarks - benchmarks: - name: Benchmarks - runs-on: ubuntu-latest - needs: unit-tests - if: github.event_name == 'push' && github.ref == 'refs/heads/main' - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up Go - uses: actions/setup-go@v4 - with: - go-version: ${{ env.GO_VERSION }} - - - name: Cache Go modules - uses: actions/cache@v3 - with: - path: | - ~/.cache/go-build - ~/go/pkg/mod - key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} - restore-keys: | - ${{ runner.os }}-go- - - - name: Download dependencies - run: go mod download - working-directory: Premium/Desktop - - - name: Run benchmarks - run: | - go test -bench=. -benchmem -run=^$ ./... | tee benchmark-results.txt - go test -bench=. -benchmem -run=^$ -cpuprofile=cpu.prof -memprofile=mem.prof ./... - working-directory: Premium/Desktop - - - name: Upload benchmark results - uses: actions/upload-artifact@v3 - with: - name: benchmark-results - path: | - Premium/Desktop/benchmark-results.txt - Premium/Desktop/cpu.prof - Premium/Desktop/mem.prof - - # Generate Documentation - documentation: - name: Generate Documentation - runs-on: ubuntu-latest - needs: unit-tests - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up Go - uses: actions/setup-go@v4 - with: - go-version: ${{ env.GO_VERSION }} - - - name: Cache Go modules - uses: actions/cache@v3 - with: - path: | - ~/.cache/go-build - ~/go/pkg/mod - key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} - restore-keys: | - ${{ runner.os }}-go- - - - name: Download dependencies - run: go mod download - working-directory: Premium/Desktop - - - name: Install documentation tools - run: go install golang.org/x/tools/cmd/godoc@latest - - - name: Generate documentation - run: | - godoc -http=:6060 & - sleep 5 - curl -s http://localhost:6060/pkg/waddlebot-bridge/ > documentation.html - pkill godoc - working-directory: Premium/Desktop - - - name: Upload documentation - uses: actions/upload-artifact@v3 - with: - name: documentation - path: Premium/Desktop/documentation.html - - # Create Release - release: - name: Create Release - runs-on: ubuntu-latest - needs: [build, integration-tests, benchmarks, build-docker] - if: github.event_name == 'push' && github.ref == 'refs/heads/main' - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Download all artifacts - uses: actions/download-artifact@v3 - - - name: Create release directory - run: mkdir -p release - - - name: Prepare release assets - run: | - # Copy binaries - cp binary-*/waddlebot-bridge-* release/ - - # Create checksums - cd release - sha256sum waddlebot-bridge-* > checksums.txt - - # Create release notes - echo "## WaddleBot Premium Desktop Bridge" > release-notes.md - echo "### Build Information" >> release-notes.md - echo "- Commit: ${{ github.sha }}" >> release-notes.md - echo "- Date: $(date -u +%Y-%m-%dT%H:%M:%S)Z" >> release-notes.md - echo "- Go Version: ${{ env.GO_VERSION }}" >> release-notes.md - echo "" >> release-notes.md - echo "### Supported Platforms" >> release-notes.md - echo "- Linux (amd64, arm64)" >> release-notes.md - echo "- Windows (amd64)" >> release-notes.md - echo "- macOS (amd64, arm64)" >> release-notes.md - echo "" >> release-notes.md - echo "### Installation" >> release-notes.md - echo "1. Download the appropriate binary for your platform" >> release-notes.md - echo "2. Make it executable (Linux/macOS): \`chmod +x waddlebot-bridge-*\`" >> release-notes.md - echo "3. Run: \`./waddlebot-bridge-* --help\`" >> release-notes.md - - - name: Create pre-release - if: contains(github.ref, 'develop') - uses: softprops/action-gh-release@v1 - with: - tag_name: v${{ github.run_number }}-pre - name: Pre-release v${{ github.run_number }} - body_path: release/release-notes.md - files: | - release/waddlebot-bridge-* - release/checksums.txt - prerelease: true - - - name: Create release - if: github.ref == 'refs/heads/main' - uses: softprops/action-gh-release@v1 - with: - tag_name: v${{ github.run_number }} - name: Release v${{ github.run_number }} - body_path: release/release-notes.md - files: | - release/waddlebot-bridge-* - release/checksums.txt - - # Performance Monitoring - performance-monitoring: - name: Performance Monitoring - runs-on: ubuntu-latest - needs: benchmarks - if: github.event_name == 'push' && github.ref == 'refs/heads/main' - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Download benchmark results - uses: actions/download-artifact@v3 - with: - name: benchmark-results - - - name: Store benchmark results - uses: benchmark-action/github-action-benchmark@v1 - with: - name: Go Benchmarks - tool: 'go' - output-file-path: benchmark-results.txt - github-token: ${{ secrets.GITHUB_TOKEN }} - auto-push: true - - # Build Summary - build-summary: - name: Build Summary - runs-on: ubuntu-latest - needs: [code-quality, unit-tests, integration-tests, build, build-docker, documentation] - if: always() - steps: - - name: Generate build summary - run: | - echo "## Desktop Bridge Build Summary" >> $GITHUB_STEP_SUMMARY - echo "| Component | Status |" >> $GITHUB_STEP_SUMMARY - echo "|-----------|--------|" >> $GITHUB_STEP_SUMMARY - echo "| Code Quality | ${{ needs.code-quality.result }} |" >> $GITHUB_STEP_SUMMARY - echo "| Unit Tests | ${{ needs.unit-tests.result }} |" >> $GITHUB_STEP_SUMMARY - echo "| Integration Tests | ${{ needs.integration-tests.result }} |" >> $GITHUB_STEP_SUMMARY - echo "| Binary Build | ${{ needs.build.result }} |" >> $GITHUB_STEP_SUMMARY - echo "| Docker Build | ${{ needs.build-docker.result }} |" >> $GITHUB_STEP_SUMMARY - echo "| Documentation | ${{ needs.documentation.result }} |" >> $GITHUB_STEP_SUMMARY - - - name: Comment PR - if: github.event_name == 'pull_request' - uses: actions/github-script@v7 - with: - script: | - const summary = `## 🖥️ Desktop Bridge Build Results - - | Component | Status | - |-----------|--------| - | Code Quality | ${{ needs.code-quality.result }} | - | Unit Tests | ${{ needs.unit-tests.result }} | - | Integration Tests | ${{ needs.integration-tests.result }} | - | Binary Build | ${{ needs.build.result }} | - | Docker Build | ${{ needs.build-docker.result }} | - | Documentation | ${{ needs.documentation.result }} | - - Build artifacts are available in the Actions tab.`; - - github.rest.issues.createComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: summary - }); - - # Deployment - deploy: - name: Deploy - runs-on: ubuntu-latest - needs: [build, build-docker] - if: github.event_name == 'push' && github.ref == 'refs/heads/main' - environment: production - steps: - - name: Deploy to CDN - run: | - echo "Deploying binaries to CDN..." - # Add your CDN deployment commands here - - - name: Update download links - run: | - echo "Updating download links..." - # Add commands to update download links on website - - - name: Notify deployment - run: | - echo "Deployment completed successfully!" - # Add notification commands here \ No newline at end of file diff --git a/.github/workflows/desktop-linux.yml b/.github/workflows/desktop-linux.yml deleted file mode 100644 index 1d247d683..000000000 --- a/.github/workflows/desktop-linux.yml +++ /dev/null @@ -1,185 +0,0 @@ -name: Desktop Bridge - Linux - -on: - push: - branches: [ main, develop ] - paths: - - 'Premium/Desktop/**' - - '.github/workflows/desktop-linux.yml' - pull_request: - branches: [ main, develop ] - paths: - - 'Premium/Desktop/**' - workflow_dispatch: - -env: - GO_VERSION: '1.24' - CGO_ENABLED: 0 - -jobs: - lint-security: - name: Lint & Security (Linux) - runs-on: ubuntu-latest - defaults: - run: - working-directory: Premium/Desktop - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Go - uses: actions/setup-go@v5 - with: - go-version: ${{ env.GO_VERSION }} - cache: true - cache-dependency-path: Premium/Desktop/go.sum - - - name: Run golangci-lint - uses: golangci/golangci-lint-action@v4 - with: - version: latest - working-directory: Premium/Desktop - args: --timeout=5m - - - name: Run gosec - uses: securego/gosec@master - with: - args: '-no-fail -fmt sarif -out gosec.sarif ./...' - - test: - name: Test (Linux) - runs-on: ubuntu-latest - needs: lint-security - defaults: - run: - working-directory: Premium/Desktop - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Go - uses: actions/setup-go@v5 - with: - go-version: ${{ env.GO_VERSION }} - cache: true - cache-dependency-path: Premium/Desktop/go.sum - - - name: Run tests - run: go test -v -race -coverprofile=coverage.out ./... - - - name: Upload coverage - uses: codecov/codecov-action@v4 - with: - files: Premium/Desktop/coverage.out - flags: desktop-linux - - build: - name: Build (Linux ${{ matrix.arch }}) - runs-on: ubuntu-latest - needs: test - strategy: - matrix: - arch: [amd64, arm64] - defaults: - run: - working-directory: Premium/Desktop - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Go - uses: actions/setup-go@v5 - with: - go-version: ${{ env.GO_VERSION }} - cache: true - cache-dependency-path: Premium/Desktop/go.sum - - - name: Build binary - env: - GOOS: linux - GOARCH: ${{ matrix.arch }} - run: | - go build -v -trimpath -ldflags="-s -w" -o waddlebot-bridge-linux-${{ matrix.arch }} ./cmd/main.go - - - name: Verify binary - run: file waddlebot-bridge-linux-${{ matrix.arch }} - - - name: Upload artifact - uses: actions/upload-artifact@v4 - with: - name: waddlebot-bridge-linux-${{ matrix.arch }} - path: Premium/Desktop/waddlebot-bridge-linux-${{ matrix.arch }} - retention-days: 30 - - package-deb: - name: Package Debian (.deb) - runs-on: ubuntu-latest - needs: build - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Download AMD64 binary - uses: actions/download-artifact@v4 - with: - name: waddlebot-bridge-linux-amd64 - path: Premium/Desktop/dist - - - name: Download ARM64 binary - uses: actions/download-artifact@v4 - with: - name: waddlebot-bridge-linux-arm64 - path: Premium/Desktop/dist - - - name: Install packaging tools - run: sudo apt-get update && sudo apt-get install -y dpkg-dev - - - name: Create .deb packages - working-directory: Premium/Desktop - run: | - # Create AMD64 package - mkdir -p debian-amd64/usr/local/bin - mkdir -p debian-amd64/DEBIAN - cp dist/waddlebot-bridge-linux-amd64 debian-amd64/usr/local/bin/waddlebot-bridge - chmod +x debian-amd64/usr/local/bin/waddlebot-bridge - - cat > debian-amd64/DEBIAN/control << EOF - Package: waddlebot-bridge - Version: 1.0.0 - Section: utils - Priority: optional - Architecture: amd64 - Maintainer: WaddleBot Team - Description: WaddleBot Premium Desktop Bridge - Local system integration platform for WaddleBot communities - EOF - - dpkg-deb --build debian-amd64 waddlebot-bridge_1.0.0_amd64.deb - - # Create ARM64 package - mkdir -p debian-arm64/usr/local/bin - mkdir -p debian-arm64/DEBIAN - cp dist/waddlebot-bridge-linux-arm64 debian-arm64/usr/local/bin/waddlebot-bridge - chmod +x debian-arm64/usr/local/bin/waddlebot-bridge - - cat > debian-arm64/DEBIAN/control << EOF - Package: waddlebot-bridge - Version: 1.0.0 - Section: utils - Priority: optional - Architecture: arm64 - Maintainer: WaddleBot Team - Description: WaddleBot Premium Desktop Bridge - Local system integration platform for WaddleBot communities - EOF - - dpkg-deb --build debian-arm64 waddlebot-bridge_1.0.0_arm64.deb - - - name: Upload .deb packages - uses: actions/upload-artifact@v4 - with: - name: waddlebot-bridge-deb - path: | - Premium/Desktop/waddlebot-bridge_1.0.0_amd64.deb - Premium/Desktop/waddlebot-bridge_1.0.0_arm64.deb - retention-days: 30 diff --git a/.github/workflows/desktop-macos.yml b/.github/workflows/desktop-macos.yml deleted file mode 100644 index 4b9114222..000000000 --- a/.github/workflows/desktop-macos.yml +++ /dev/null @@ -1,106 +0,0 @@ -name: Desktop Bridge - macOS - -on: - push: - branches: [ main, develop ] - paths: - - 'Premium/Desktop/**' - - '.github/workflows/desktop-macos.yml' - pull_request: - branches: [ main, develop ] - paths: - - 'Premium/Desktop/**' - workflow_dispatch: - -env: - GO_VERSION: '1.24' - CGO_ENABLED: 0 - -jobs: - lint: - name: Lint (macOS) - runs-on: macos-latest - defaults: - run: - working-directory: Premium/Desktop - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Go - uses: actions/setup-go@v5 - with: - go-version: ${{ env.GO_VERSION }} - cache: true - cache-dependency-path: Premium/Desktop/go.sum - - - name: Install golangci-lint - run: brew install golangci-lint - - - name: Run golangci-lint - run: golangci-lint run --timeout=5m - - test: - name: Test (macOS) - runs-on: macos-latest - needs: lint - defaults: - run: - working-directory: Premium/Desktop - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Go - uses: actions/setup-go@v5 - with: - go-version: ${{ env.GO_VERSION }} - cache: true - cache-dependency-path: Premium/Desktop/go.sum - - - name: Run tests - run: go test -v -race -coverprofile=coverage.out ./... - - - name: Upload coverage - uses: codecov/codecov-action@v4 - with: - files: Premium/Desktop/coverage.out - flags: desktop-macos - - build: - name: Build (macOS ${{ matrix.arch }}) - runs-on: macos-latest - needs: test - strategy: - matrix: - arch: [amd64, arm64] - defaults: - run: - working-directory: Premium/Desktop - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Go - uses: actions/setup-go@v5 - with: - go-version: ${{ env.GO_VERSION }} - cache: true - cache-dependency-path: Premium/Desktop/go.sum - - - name: Build binary - env: - GOOS: darwin - GOARCH: ${{ matrix.arch }} - run: | - go build -v -trimpath -ldflags="-s -w" -o waddlebot-bridge-darwin-${{ matrix.arch }} ./cmd/main.go - - - name: Verify binary - run: file waddlebot-bridge-darwin-${{ matrix.arch }} - - - name: Upload artifact - uses: actions/upload-artifact@v4 - with: - name: waddlebot-bridge-darwin-${{ matrix.arch }} - path: Premium/Desktop/waddlebot-bridge-darwin-${{ matrix.arch }} - retention-days: 30 diff --git a/.github/workflows/desktop-release.yml b/.github/workflows/desktop-release.yml deleted file mode 100644 index 093cdc155..000000000 --- a/.github/workflows/desktop-release.yml +++ /dev/null @@ -1,147 +0,0 @@ -name: Desktop Bridge - Release - -on: - workflow_run: - workflows: - - "Desktop Bridge - macOS" - - "Desktop Bridge - Windows" - - "Desktop Bridge - Linux" - branches: [main] - types: [completed] - workflow_dispatch: - inputs: - version: - description: 'Release version (e.g., v1.0.0)' - required: true - type: string - -jobs: - release: - name: Create Release - runs-on: ubuntu-latest - if: ${{ github.event.workflow_run.conclusion == 'success' || github.event_name == 'workflow_dispatch' }} - permissions: - contents: write - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Determine version - id: version - run: | - if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then - echo "version=${{ inputs.version }}" >> $GITHUB_OUTPUT - else - echo "version=v$(date +%Y.%m.%d)-${GITHUB_SHA::7}" >> $GITHUB_OUTPUT - fi - - - name: Download all artifacts - uses: actions/download-artifact@v4 - with: - path: artifacts - - - name: List artifacts - run: | - echo "Downloaded artifacts:" - ls -R artifacts/ - - - name: Create checksums - run: | - cd artifacts - for artifact in */waddlebot-bridge-*; do - if [ -f "$artifact" ]; then - sha256sum "$artifact" >> checksums.txt - fi - done - cat checksums.txt - - - name: Create release notes - run: | - cat > release-notes.md << 'EOF' - # WaddleBot Premium Desktop Bridge Release - - ## Features - - **OBS Integration**: Full OBS Studio control via WebSocket - - **Local API Gateway**: REST + WebSocket endpoints for local apps - - **Scripting Engine**: Lua, Python3, PowerShell, and Bash support - - **Cross-Platform**: macOS (AMD64 + ARM64), Windows (AMD64), Linux (AMD64 + ARM64) - - ## Installation - - ### macOS - ```bash - # AMD64 (Intel) - chmod +x waddlebot-bridge-darwin-amd64 - ./waddlebot-bridge-darwin-amd64 - - # ARM64 (Apple Silicon) - chmod +x waddlebot-bridge-darwin-arm64 - ./waddlebot-bridge-darwin-arm64 - ``` - - ### Windows - ```powershell - # Double-click or run in PowerShell - .\waddlebot-bridge-windows-amd64.exe - ``` - - ### Linux (Debian/Ubuntu) - ```bash - # Install .deb package (AMD64) - sudo dpkg -i waddlebot-bridge_1.0.0_amd64.deb - - # Install .deb package (ARM64) - sudo dpkg -i waddlebot-bridge_1.0.0_arm64.deb - - # Or run binary directly - chmod +x waddlebot-bridge-linux-amd64 - ./waddlebot-bridge-linux-amd64 - ``` - - ## Configuration - Create `~/.waddlebot-bridge.yaml`: - ```yaml - community-id: "your-community-id" - user-id: "your-user-id" - api-url: "https://api.waddlebot.io" - - obs: - enabled: true - host: localhost - port: 4455 - password: "your-obs-password" - - gateway: - enabled: true - host: 127.0.0.1 - port: 8090 - - scripting: - enabled: true - enable-lua: true - enable-python: true - ``` - - ## Checksums - See `checksums.txt` for SHA256 verification. - EOF - - - name: Create GitHub Release - uses: softprops/action-gh-release@v1 - with: - tag_name: ${{ steps.version.outputs.version }} - name: Desktop Bridge ${{ steps.version.outputs.version }} - body_path: release-notes.md - draft: false - prerelease: false - files: | - artifacts/waddlebot-bridge-darwin-amd64/waddlebot-bridge-darwin-amd64 - artifacts/waddlebot-bridge-darwin-arm64/waddlebot-bridge-darwin-arm64 - artifacts/waddlebot-bridge-windows-amd64/waddlebot-bridge-windows-amd64.exe - artifacts/waddlebot-bridge-linux-amd64/waddlebot-bridge-linux-amd64 - artifacts/waddlebot-bridge-linux-arm64/waddlebot-bridge-linux-arm64 - artifacts/waddlebot-bridge-deb/waddlebot-bridge_1.0.0_amd64.deb - artifacts/waddlebot-bridge-deb/waddlebot-bridge_1.0.0_arm64.deb - artifacts/checksums.txt - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/desktop-windows.yml b/.github/workflows/desktop-windows.yml deleted file mode 100644 index deecd90fe..000000000 --- a/.github/workflows/desktop-windows.yml +++ /dev/null @@ -1,100 +0,0 @@ -name: Desktop Bridge - Windows - -on: - push: - branches: [ main, develop ] - paths: - - 'Premium/Desktop/**' - - '.github/workflows/desktop-windows.yml' - pull_request: - branches: [ main, develop ] - paths: - - 'Premium/Desktop/**' - workflow_dispatch: - -env: - GO_VERSION: '1.24' - CGO_ENABLED: 0 - -jobs: - lint: - name: Lint (Windows) - runs-on: windows-latest - defaults: - run: - working-directory: Premium/Desktop - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Go - uses: actions/setup-go@v5 - with: - go-version: ${{ env.GO_VERSION }} - cache: true - cache-dependency-path: Premium/Desktop/go.sum - - - name: Install golangci-lint - run: go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest - - - name: Run golangci-lint - run: golangci-lint run --timeout=5m - - test: - name: Test (Windows) - runs-on: windows-latest - needs: lint - defaults: - run: - working-directory: Premium/Desktop - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Go - uses: actions/setup-go@v5 - with: - go-version: ${{ env.GO_VERSION }} - cache: true - cache-dependency-path: Premium/Desktop/go.sum - - - name: Run tests - run: go test -v -race -coverprofile=coverage.out ./... - - - name: Upload coverage - uses: codecov/codecov-action@v4 - with: - files: Premium/Desktop/coverage.out - flags: desktop-windows - - build: - name: Build (Windows AMD64) - runs-on: windows-latest - needs: test - defaults: - run: - working-directory: Premium/Desktop - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Go - uses: actions/setup-go@v5 - with: - go-version: ${{ env.GO_VERSION }} - cache: true - cache-dependency-path: Premium/Desktop/go.sum - - - name: Build binary - env: - GOOS: windows - GOARCH: amd64 - run: | - go build -v -trimpath -ldflags="-s -w" -o waddlebot-bridge-windows-amd64.exe ./cmd/main.go - - - name: Upload artifact - uses: actions/upload-artifact@v4 - with: - name: waddlebot-bridge-windows-amd64 - path: Premium/Desktop/waddlebot-bridge-windows-amd64.exe - retention-days: 30 diff --git a/Premium/Desktop/Makefile b/Premium/Desktop/Makefile deleted file mode 100644 index 6898b21d5..000000000 --- a/Premium/Desktop/Makefile +++ /dev/null @@ -1,167 +0,0 @@ -# WaddleBot Premium Desktop Bridge Makefile - -.PHONY: all build clean deps test lint install uninstall dev help - -# Configuration -APP_NAME = waddlebot-bridge -VERSION = 1.0.0 -BUILD_DIR = build -DIST_DIR = dist -GO_VERSION = 1.21 - -# Colors for output -GREEN = \033[0;32m -YELLOW = \033[1;33m -RED = \033[0;31m -NC = \033[0m # No Color - -# Default target -all: clean deps build - -# Build all targets -build: - @echo "$(GREEN)[INFO]$(NC) Building WaddleBot Premium Desktop Bridge..." - @chmod +x scripts/build.sh - @./scripts/build.sh - -# Build for specific platform -build-windows: - @echo "$(GREEN)[INFO]$(NC) Building for Windows..." - @mkdir -p $(BUILD_DIR) - @CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags="-s -w -X main.version=$(VERSION)" -o $(BUILD_DIR)/$(APP_NAME)-windows-amd64.exe cmd/main.go - -build-macos: - @echo "$(GREEN)[INFO]$(NC) Building for macOS..." - @mkdir -p $(BUILD_DIR) - @CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags="-s -w -X main.version=$(VERSION)" -o $(BUILD_DIR)/$(APP_NAME)-darwin-amd64 cmd/main.go - @CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -ldflags="-s -w -X main.version=$(VERSION)" -o $(BUILD_DIR)/$(APP_NAME)-darwin-arm64 cmd/main.go - -build-linux: - @echo "$(GREEN)[INFO]$(NC) Building for Linux..." - @mkdir -p $(BUILD_DIR) - @CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w -X main.version=$(VERSION)" -o $(BUILD_DIR)/$(APP_NAME)-linux-amd64 cmd/main.go - -# Build modules -build-modules: - @echo "$(GREEN)[INFO]$(NC) Building modules..." - @mkdir -p $(BUILD_DIR) - @cd internal/modules/examples/system && go build -buildmode=plugin -o ../../../../$(BUILD_DIR)/system.so system.go - -# Clean build artifacts -clean: - @echo "$(GREEN)[INFO]$(NC) Cleaning build artifacts..." - @rm -rf $(BUILD_DIR) $(DIST_DIR) - -# Install dependencies -deps: - @echo "$(GREEN)[INFO]$(NC) Installing dependencies..." - @go mod download - @go mod tidy - -# Run tests -test: - @echo "$(GREEN)[INFO]$(NC) Running tests..." - @go test -v ./... - -# Run linter -lint: - @echo "$(GREEN)[INFO]$(NC) Running linter..." - @if command -v golangci-lint >/dev/null 2>&1; then \ - golangci-lint run; \ - else \ - echo "$(YELLOW)[WARNING]$(NC) golangci-lint not found, skipping lint"; \ - fi - -# Format code -fmt: - @echo "$(GREEN)[INFO]$(NC) Formatting code..." - @go fmt ./... - -# Run in development mode -dev: - @echo "$(GREEN)[INFO]$(NC) Running in development mode..." - @go run cmd/main.go --log-level debug - -# Install the application -install: build - @echo "$(GREEN)[INFO]$(NC) Installing $(APP_NAME)..." - @sudo cp $(BUILD_DIR)/$(APP_NAME)-$(shell go env GOOS)-$(shell go env GOARCH) /usr/local/bin/$(APP_NAME) - @echo "$(GREEN)[INFO]$(NC) Installation complete. Run with: $(APP_NAME)" - -# Uninstall the application -uninstall: - @echo "$(GREEN)[INFO]$(NC) Uninstalling $(APP_NAME)..." - @sudo rm -f /usr/local/bin/$(APP_NAME) - @echo "$(GREEN)[INFO]$(NC) Uninstallation complete" - -# Generate documentation -docs: - @echo "$(GREEN)[INFO]$(NC) Generating documentation..." - @if command -v godoc >/dev/null 2>&1; then \ - echo "Documentation server starting at http://localhost:6060"; \ - godoc -http=:6060; \ - else \ - echo "$(YELLOW)[WARNING]$(NC) godoc not found, install with: go install golang.org/x/tools/cmd/godoc@latest"; \ - fi - -# Check Go version -check-go: - @echo "$(GREEN)[INFO]$(NC) Checking Go version..." - @go version - @echo "$(GREEN)[INFO]$(NC) Required Go version: $(GO_VERSION)" - -# Security audit -security: - @echo "$(GREEN)[INFO]$(NC) Running security audit..." - @if command -v gosec >/dev/null 2>&1; then \ - gosec ./...; \ - else \ - echo "$(YELLOW)[WARNING]$(NC) gosec not found, install with: go install github.com/securecodewarrior/gosec/v2/cmd/gosec@latest"; \ - fi - -# Show build info -info: - @echo "$(GREEN)[INFO]$(NC) Build Information:" - @echo "App Name: $(APP_NAME)" - @echo "Version: $(VERSION)" - @echo "Build Directory: $(BUILD_DIR)" - @echo "Distribution Directory: $(DIST_DIR)" - @echo "Go Version: $(shell go version)" - @echo "Platform: $(shell go env GOOS)/$(shell go env GOARCH)" - -# Release build -release: clean deps test lint build - @echo "$(GREEN)[INFO]$(NC) Release build completed!" - -# Help -help: - @echo "WaddleBot Premium Desktop Bridge Makefile" - @echo "==========================================" - @echo "" - @echo "Available targets:" - @echo " all - Clean, install deps, and build (default)" - @echo " build - Build all platforms" - @echo " build-windows - Build for Windows" - @echo " build-macos - Build for macOS" - @echo " build-linux - Build for Linux" - @echo " build-modules - Build plugin modules" - @echo " clean - Clean build artifacts" - @echo " deps - Install dependencies" - @echo " test - Run tests" - @echo " lint - Run linter" - @echo " fmt - Format code" - @echo " dev - Run in development mode" - @echo " install - Install the application" - @echo " uninstall - Uninstall the application" - @echo " docs - Generate documentation" - @echo " check-go - Check Go version" - @echo " security - Run security audit" - @echo " info - Show build information" - @echo " release - Full release build" - @echo " help - Show this help message" - @echo "" - @echo "Examples:" - @echo " make build # Build all platforms" - @echo " make build-windows # Build for Windows only" - @echo " make dev # Run in development mode" - @echo " make release # Full release build" \ No newline at end of file diff --git a/Premium/Desktop/README.md b/Premium/Desktop/README.md deleted file mode 100644 index 9b21389c7..000000000 --- a/Premium/Desktop/README.md +++ /dev/null @@ -1,244 +0,0 @@ -# WaddleBot Premium Desktop Bridge - -A powerful desktop bridge client that connects your local system to WaddleBot communities, enabling chat commands to trigger local actions and system integrations. - -## Features - -- **Premium Application**: Requires active WaddleBot Premium subscription -- **WebAuthn Authentication**: Secure authentication using WebAuthn for device registration -- **Community Restricted**: Each bridge instance is restricted to a single community and user -- **Configurable Polling**: Polls server for actions every 30 seconds (configurable, minimum 5 seconds) -- **Module System**: Extensible plugin architecture for local system interactions -- **Multi-Platform**: Native support for macOS Universal and Windows 11 -- **Local System Integration**: Execute commands, monitor system resources, and more -- **Real-time Web Interface**: Browser-based configuration and monitoring - -## Requirements - -- **Operating System**: macOS 10.15+ or Windows 11 -- **Premium Subscription**: Active WaddleBot Premium subscription required -- **Go 1.21+**: For building from source -- **WebAuthn Compatible Browser**: Chrome, Firefox, Safari, or Edge - -## Installation - -### macOS - -1. Download the macOS package from releases -2. Extract the archive: `tar -xzf WaddleBot-Bridge-macOS-1.0.0.tar.gz` -3. Navigate to the extracted directory -4. Configure your settings in `config.yaml` -5. Run the bridge: `./start.sh` - -### Windows 11 - -1. Download the Windows package from releases -2. Extract the ZIP file -3. Navigate to the extracted directory -4. Configure your settings in `config.yaml` -5. Run the bridge: `start.bat` - -## Configuration - -Edit the `config.yaml` file to configure your bridge: - -```yaml -# WaddleBot Bridge Configuration -api-url: "https://api.waddlebot.io" -community-id: "your-community-id" -user-id: "your-user-id" -poll-interval: 30 -web-port: 8080 -web-host: "127.0.0.1" -log-level: "info" -``` - -### Configuration Options - -- `api-url`: WaddleBot API endpoint -- `community-id`: Your community identifier -- `user-id`: Your user identifier -- `poll-interval`: Polling interval in seconds (minimum 5) -- `web-port`: Web interface port -- `web-host`: Web interface host -- `log-level`: Logging level (debug, info, warn, error) - -## Web Interface - -Access the web interface at `http://localhost:8080` to: - -- Authenticate using WebAuthn -- View bridge status -- Monitor system information -- Configure settings - -## Module System - -The bridge supports a plugin-based module system for extending functionality: - -### Built-in Modules - -- **System Module**: System information, process management, and command execution - - `get_info`: Get system information - - `get_processes`: List running processes - - `get_memory_info`: Memory usage statistics - - `get_cpu_info`: CPU usage and information - - `get_disk_usage`: Disk usage statistics - - `execute_command`: Execute allowed system commands - -### Creating Custom Modules - -1. Implement the `ModuleInterface` in Go -2. Build as a plugin: `go build -buildmode=plugin -o module.so module.go` -3. Place the `.so` file in the modules directory -4. Restart the bridge to load the module - -Example module structure: - -```go -package main - -import ( - "context" - "waddlebot-bridge/internal/modules" -) - -type MyModule struct { - config map[string]string -} - -func NewModule() modules.ModuleInterface { - return &MyModule{} -} - -func (m *MyModule) Initialize(config map[string]string) error { - m.config = config - return nil -} - -func (m *MyModule) ExecuteAction(ctx context.Context, action string, parameters map[string]string) (map[string]interface{}, error) { - // Implement your action logic here - return map[string]interface{}{"result": "success"}, nil -} - -// ... implement other required methods -``` - -## Security - -- **WebAuthn Authentication**: Uses WebAuthn for secure device registration -- **Community Isolation**: Each bridge is restricted to a single community -- **Command Restrictions**: Only allowed system commands can be executed -- **Encrypted Communication**: All API communication uses HTTPS -- **Session Management**: Secure session handling with automatic expiration - -## Building from Source - -### Prerequisites - -- Go 1.21 or later -- Git - -### Build Instructions - -1. Clone the repository: - ```bash - git clone https://github.com/your-org/waddlebot-bridge.git - cd waddlebot-bridge - ``` - -2. Build using the provided script: - ```bash - chmod +x scripts/build.sh - ./scripts/build.sh - ``` - -3. Or use the Makefile: - ```bash - make build - ``` - -### Platform-Specific Builds - -- **macOS**: `make build-macos` -- **Windows**: `make build-windows` -- **Linux**: `make build-linux` - -## Development - -### Running in Development Mode - -```bash -make dev -``` - -### Building Modules - -```bash -make build-modules -``` - -### Running Tests - -```bash -make test -``` - -### Code Formatting - -```bash -make fmt -``` - -## API Integration - -The bridge communicates with WaddleBot through the following endpoints: - -- `GET /api/bridge/poll` - Poll for actions to execute -- `POST /api/bridge/response` - Send action results -- `POST /api/bridge/register` - Register bridge with server -- `POST /api/bridge/heartbeat` - Send heartbeat - -## Troubleshooting - -### Common Issues - -1. **License Error**: Ensure you have an active WaddleBot Premium subscription -2. **Authentication Failed**: Check your community-id and user-id in config.yaml -3. **Module Loading Error**: Verify module files are in the correct directory -4. **Network Issues**: Check firewall settings and API connectivity - -### Debug Mode - -Run with debug logging: - -```bash -./waddlebot-bridge --log-level debug -``` - -### Log Files - -Logs are written to: -- macOS: `~/Library/Logs/WaddleBot/bridge.log` -- Windows: `%APPDATA%/WaddleBot/bridge.log` - -## Support - -For support and questions: - -- Visit: https://waddlebot.io/support -- Email: support@waddlebot.io -- Discord: https://discord.gg/waddlebot - -## License - -This software is licensed exclusively to users with active WaddleBot Premium subscriptions. See LICENSE file for details. - -## Contributing - -This is proprietary software. Contributions are not accepted from external parties. - ---- - -**WaddleBot Premium Desktop Bridge v1.0.0** -© 2024 WaddleBot. All rights reserved. \ No newline at end of file diff --git a/Premium/Desktop/cmd/main.go b/Premium/Desktop/cmd/main.go deleted file mode 100644 index 2566929b4..000000000 --- a/Premium/Desktop/cmd/main.go +++ /dev/null @@ -1,299 +0,0 @@ -package main - -import ( - "context" - "fmt" - "log" - "os" - "os/signal" - "syscall" - "time" - - "github.com/spf13/cobra" - "github.com/spf13/viper" - "waddlebot-bridge/internal/auth" - "waddlebot-bridge/internal/bridge" - "waddlebot-bridge/internal/config" - "waddlebot-bridge/internal/gateway" - "waddlebot-bridge/internal/license" - "waddlebot-bridge/internal/logger" - "waddlebot-bridge/internal/modules" - "waddlebot-bridge/internal/obs" - "waddlebot-bridge/internal/poller" - "waddlebot-bridge/internal/scripting" - "waddlebot-bridge/internal/server" - "waddlebot-bridge/internal/storage" -) - -var ( - version = "1.0.0" - cfgFile string -) - -var rootCmd = &cobra.Command{ - Use: "waddlebot-bridge", - Short: "WaddleBot Premium Desktop Bridge", - Long: `WaddleBot Premium Desktop Bridge - Connect your local system to WaddleBot communities`, - Version: version, - Run: runBridge, -} - -func init() { - cobra.OnInitialize(initConfig) - rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.waddlebot-bridge.yaml)") - rootCmd.PersistentFlags().String("api-url", "https://api.waddlebot.io", "WaddleBot API URL") - rootCmd.PersistentFlags().String("community-id", "", "Community ID to connect to") - rootCmd.PersistentFlags().String("user-id", "", "User ID for authentication") - rootCmd.PersistentFlags().Int("poll-interval", 30, "Polling interval in seconds (minimum 5)") - rootCmd.PersistentFlags().String("log-level", "info", "Log level (debug, info, warn, error)") - rootCmd.PersistentFlags().String("data-dir", "", "Data directory for storage (default: $HOME/.waddlebot-bridge)") - - viper.BindPFlag("api-url", rootCmd.PersistentFlags().Lookup("api-url")) - viper.BindPFlag("community-id", rootCmd.PersistentFlags().Lookup("community-id")) - viper.BindPFlag("user-id", rootCmd.PersistentFlags().Lookup("user-id")) - viper.BindPFlag("poll-interval", rootCmd.PersistentFlags().Lookup("poll-interval")) - viper.BindPFlag("log-level", rootCmd.PersistentFlags().Lookup("log-level")) - viper.BindPFlag("data-dir", rootCmd.PersistentFlags().Lookup("data-dir")) -} - -func initConfig() { - if cfgFile != "" { - viper.SetConfigFile(cfgFile) - } else { - home, err := os.UserHomeDir() - if err != nil { - log.Fatal(err) - } - viper.AddConfigPath(home) - viper.SetConfigType("yaml") - viper.SetConfigName(".waddlebot-bridge") - } - - viper.AutomaticEnv() - viper.ReadInConfig() -} - -func runBridge(cmd *cobra.Command, args []string) { - // Initialize logger - logger.Init(viper.GetString("log-level")) - log := logger.GetLogger() - - // Display banner - displayBanner() - - // Check premium license - if !license.ValidateLicense() { - log.Fatal("Invalid or missing premium license. Please ensure you have a valid WaddleBot Premium subscription.") - } - - // Load configuration - cfg, err := config.Load() - if err != nil { - log.WithError(err).Fatal("Failed to load configuration") - } - - // Validate required configuration - if cfg.CommunityID == "" { - log.Fatal("Community ID is required. Use --community-id flag or set in config file.") - } - if cfg.UserID == "" { - log.Fatal("User ID is required. Use --user-id flag or set in config file.") - } - - // Validate poll interval - if cfg.PollInterval < 5 { - log.Warn("Poll interval cannot be less than 5 seconds. Setting to 5 seconds.") - cfg.PollInterval = 5 - } - - // Initialize storage - store, err := storage.NewBoltStorage(cfg.DataDir) - if err != nil { - log.WithError(err).Fatal("Failed to initialize storage") - } - defer store.Close() - - // Initialize WebAuthn authenticator - authenticator, err := auth.NewWebAuthnManager(cfg, store) - if err != nil { - log.WithError(err).Fatal("Failed to initialize WebAuthn") - } - - // Initialize module manager - moduleManager := modules.NewManager(cfg, store) - - // Initialize OBS client if enabled - var obsClient *obs.Client - if cfg.OBS.Enabled { - obsConfig := obs.Config{ - Host: cfg.OBS.Host, - Port: cfg.OBS.Port, - Password: cfg.OBS.Password, - AutoReconnect: cfg.OBS.AutoReconnect, - ReconnectInterval: cfg.OBS.ReconnectInterval, - MaxReconnectInterval: cfg.OBS.MaxReconnectInterval, - Timeout: cfg.OBS.Timeout, - Enabled: cfg.OBS.Enabled, - } - obsClient = obs.NewClient(obsConfig, log) - log.Info("OBS integration enabled") - } - - // Initialize scripting manager if enabled - var scriptManager *scripting.Manager - if cfg.Scripting.Enabled { - scriptManager, err = scripting.NewManager(cfg.Scripting, log) - if err != nil { - log.WithError(err).Warn("Failed to initialize scripting manager") - } else { - log.WithField("engines", scriptManager.GetEnabledTypes()).Info("Scripting engine initialized") - } - } - - // Initialize bridge client - bridgeClient, err := bridge.NewClient(cfg, authenticator, moduleManager) - if err != nil { - log.WithError(err).Fatal("Failed to initialize bridge client") - } - - // Initialize poller - pollerInstance := poller.NewPoller(cfg, bridgeClient, moduleManager) - - // Initialize web server for WebAuthn - webServer := server.NewWebServer(cfg, authenticator, bridgeClient) - - // Initialize local API gateway if enabled - var gatewayServer *gateway.Gateway - if cfg.Gateway.Enabled { - gatewayServer = gateway.New(cfg.Gateway, obsClient, log) - log.WithFields(map[string]interface{}{ - "host": cfg.Gateway.Host, - "port": cfg.Gateway.Port, - }).Info("Local API gateway enabled") - } - - // Create context for graceful shutdown - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - // Handle signals for graceful shutdown - sigChan := make(chan os.Signal, 1) - signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) - - // Start components - log.Info("Starting WaddleBot Premium Desktop Bridge...") - - // Start OBS client if enabled - if obsClient != nil { - go func() { - if err := obsClient.Connect(ctx); err != nil { - log.WithError(err).Warn("OBS connection failed - will retry automatically") - } else { - log.Info("Connected to OBS Studio") - // Start event listener - if err := obsClient.StartEventListener(); err != nil { - log.WithError(err).Error("Failed to start OBS event listener") - } - } - }() - } - - // Start local API gateway if enabled - if gatewayServer != nil { - go func() { - if err := gatewayServer.Start(ctx); err != nil { - log.WithError(err).Error("Gateway server error") - } - }() - } - - // Start web server - go func() { - if err := webServer.Start(ctx); err != nil { - log.WithError(err).Error("Web server error") - } - }() - - // Start poller - go func() { - if err := pollerInstance.Start(ctx); err != nil { - log.WithError(err).Error("Poller error") - } - }() - - // Display connection info - connectionInfo := map[string]interface{}{ - "community_id": cfg.CommunityID, - "user_id": cfg.UserID, - "poll_interval": cfg.PollInterval, - "api_url": cfg.APIURL, - "web_port": cfg.WebPort, - } - - if cfg.OBS.Enabled { - connectionInfo["obs_enabled"] = true - connectionInfo["obs_host"] = cfg.OBS.Host - connectionInfo["obs_port"] = cfg.OBS.Port - } - - if cfg.Gateway.Enabled { - connectionInfo["gateway_enabled"] = true - connectionInfo["gateway_port"] = cfg.Gateway.Port - } - - if cfg.Scripting.Enabled && scriptManager != nil { - connectionInfo["scripting_enabled"] = true - connectionInfo["script_engines"] = scriptManager.GetEnabledTypes() - } - - log.WithFields(connectionInfo).Info("Bridge initialized successfully") - - // Wait for shutdown signal - <-sigChan - log.Info("Shutting down WaddleBot Bridge...") - - // Cancel context to stop all components - cancel() - - // Shutdown OBS client - if obsClient != nil { - if err := obsClient.Disconnect(); err != nil { - log.WithError(err).Warn("Error disconnecting from OBS") - } else { - log.Info("Disconnected from OBS Studio") - } - } - - // Shutdown gateway server - if gatewayServer != nil { - if err := gatewayServer.Stop(); err != nil { - log.WithError(err).Warn("Error stopping gateway server") - } else { - log.Info("Gateway server stopped") - } - } - - // Give components time to shutdown gracefully - time.Sleep(2 * time.Second) - log.Info("WaddleBot Bridge stopped") -} - -func displayBanner() { - fmt.Println(` -██╗ ██╗ █████╗ ██████╗ ██████╗ ██╗ ███████╗██████╗ ██████╗ ████████╗ -██║ ██║██╔══██╗██╔══██╗██╔══██╗██║ ██╔════╝██╔══██╗██╔═══██╗╚══██╔══╝ -██║ █╗ ██║███████║██║ ██║██║ ██║██║ █████╗ ██████╔╝██║ ██║ ██║ -██║███╗██║██╔══██║██║ ██║██║ ██║██║ ██╔══╝ ██╔══██╗██║ ██║ ██║ -╚███╔███╔╝██║ ██║██████╔╝██████╔╝███████╗███████╗██████╔╝╚██████╔╝ ██║ - ╚══╝╚══╝ ╚═╝ ╚═╝╚═════╝ ╚═════╝ ╚══════╝╚══════╝╚═════╝ ╚═════╝ ╚═╝ - - Premium Desktop Bridge v` + version + ` - Local System Integration Platform -`) -} - -func main() { - if err := rootCmd.Execute(); err != nil { - log.Fatal(err) - } -} \ No newline at end of file diff --git a/Premium/Desktop/go.mod b/Premium/Desktop/go.mod deleted file mode 100644 index 42504e62d..000000000 --- a/Premium/Desktop/go.mod +++ /dev/null @@ -1,57 +0,0 @@ -module waddlebot-bridge - -go 1.24.0 - -toolchain go1.24.10 - -require ( - github.com/andreykaipov/goobs v1.3.0 - github.com/go-webauthn/webauthn v0.15.0 - github.com/gorilla/mux v1.8.0 - github.com/gorilla/websocket v1.5.3 - github.com/shirou/gopsutil v3.21.11+incompatible - github.com/sirupsen/logrus v1.9.3 - github.com/spf13/cobra v1.7.0 - github.com/spf13/viper v1.16.0 - go.etcd.io/bbolt v1.3.7 - golang.org/x/crypto v0.47.0 // indirect - golang.org/x/sys v0.40.0 // indirect -) - -require ( - github.com/golang-jwt/jwt/v5 v5.3.0 - github.com/google/uuid v1.6.0 - github.com/yuin/gopher-lua v1.1.1 - golang.org/x/time v0.1.0 -) - -require ( - github.com/buger/jsonparser v1.1.1 // indirect - github.com/fsnotify/fsnotify v1.6.0 // indirect - github.com/fxamacker/cbor/v2 v2.9.0 // indirect - github.com/go-ole/go-ole v1.2.6 // indirect - github.com/go-viper/mapstructure/v2 v2.4.0 // indirect - github.com/go-webauthn/x v0.1.26 // indirect - github.com/google/go-tpm v0.9.6 // indirect - github.com/hashicorp/hcl v1.0.0 // indirect - github.com/hashicorp/logutils v1.0.0 // indirect - github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/magiconair/properties v1.8.7 // indirect - github.com/mitchellh/mapstructure v1.5.0 // indirect - github.com/mmcloughlin/profile v0.1.1 // indirect - github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect - github.com/pelletier/go-toml/v2 v2.0.8 // indirect - github.com/spf13/afero v1.9.5 // indirect - github.com/spf13/cast v1.5.1 // indirect - github.com/spf13/jwalterweatherman v1.1.0 // indirect - github.com/spf13/pflag v1.0.5 // indirect - github.com/subosito/gotenv v1.4.2 // indirect - github.com/tklauser/go-sysconf v0.3.12 // indirect - github.com/tklauser/numcpus v0.6.1 // indirect - github.com/x448/float16 v0.8.4 // indirect - github.com/yusufpapurcu/wmi v1.2.4 // indirect - golang.org/x/text v0.33.0 // indirect - google.golang.org/protobuf v1.33.0 // indirect - gopkg.in/ini.v1 v1.67.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect -) diff --git a/Premium/Desktop/go.sum b/Premium/Desktop/go.sum deleted file mode 100644 index 95ba1ce93..000000000 --- a/Premium/Desktop/go.sum +++ /dev/null @@ -1,551 +0,0 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= -cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= -cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= -cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= -cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= -cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= -cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= -cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= -cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= -cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= -cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= -cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= -cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= -cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= -cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= -cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= -cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= -cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= -cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= -cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= -cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= -cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= -cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= -cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= -cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= -cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= -cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= -cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= -cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= -cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= -cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/andreykaipov/goobs v1.3.0 h1:iciwZziY8aC286PejMmJXPZCMn5HED/o2mZFQTAS8rU= -github.com/andreykaipov/goobs v1.3.0/go.mod h1:WnS56smX4QZok4VPldy0jXO3v+HrLXp2ymaOZsh1r3k= -github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= -github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= -github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= -github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= -github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= -github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= -github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= -github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= -github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= -github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= -github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= -github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= -github.com/go-webauthn/webauthn v0.15.0 h1:LR1vPv62E0/6+sTenX35QrCmpMCzLeVAcnXeH4MrbJY= -github.com/go-webauthn/webauthn v0.15.0/go.mod h1:hcAOhVChPRG7oqG7Xj6XKN1mb+8eXTGP/B7zBLzkX5A= -github.com/go-webauthn/x v0.1.26 h1:eNzreFKnwNLDFoywGh9FA8YOMebBWTUNlNSdolQRebs= -github.com/go-webauthn/x v0.1.26/go.mod h1:jmf/phPV6oIsF6hmdVre+ovHkxjDOmNH0t6fekWUxvg= -github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= -github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= -github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-tpm v0.9.6 h1:Ku42PT4LmjDu1H5C5ISWLlpI1mj+Zq7sPGKoRw2XROA= -github.com/google/go-tpm v0.9.6/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= -github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= -github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= -github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= -github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= -github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= -github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= -github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y= -github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= -github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= -github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= -github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= -github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= -github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/mmcloughlin/profile v0.1.1 h1:jhDmAqPyebOsVDOCICJoINoLb/AnLBaUw58nFzxWS2w= -github.com/mmcloughlin/profile v0.1.1/go.mod h1:IhHD7q1ooxgwTgjxQYkACGA77oFTDdFVejUS1/tS/qU= -github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d h1:VhgPp6v9qf9Agr/56bj7Y/xa04UccTW04VP0Qed4vnQ= -github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U= -github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= -github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= -github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= -github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI= -github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= -github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= -github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM= -github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= -github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA= -github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48= -github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= -github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= -github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= -github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.16.0 h1:rGGH0XDZhdUOryiDWjmIvUSWpbNqisK8Wk0Vyefw8hc= -github.com/spf13/viper v1.16.0/go.mod h1:yg78JgCJcbrQOvV9YLXgkLaZqUidkY9K+Dd1FofRzQg= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8= -github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= -github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= -github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= -github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= -github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= -github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= -github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= -github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= -github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= -github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= -github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= -go.etcd.io/bbolt v1.3.7 h1:j+zJOnnEjF/kyHlDDgGnVL/AIqIJPq8UoB2GSNfkUfQ= -go.etcd.io/bbolt v1.3.7/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw= -go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= -go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= -go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= -go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= -go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= -golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= -golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= -golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= -golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= -golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= -golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= -golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= -golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= -golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= -golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= -golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.1.0 h1:xYY+Bajn2a7VBmTM5GikTmnK8ZuX8YgnQCqZpbBNtmA= -golang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= -golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= -golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= -google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= -google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= -google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= -google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= -google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= -google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= -google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= -google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= -google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= -google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= -google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= -google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= -gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= -rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= -rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/Premium/Desktop/integration_test.go b/Premium/Desktop/integration_test.go deleted file mode 100644 index 4617db015..000000000 --- a/Premium/Desktop/integration_test.go +++ /dev/null @@ -1,515 +0,0 @@ -package main - -import ( - "context" - "encoding/json" - "fmt" - "net/http" - "net/http/httptest" - "os" - "path/filepath" - "testing" - "time" - - "waddlebot-bridge/internal/auth" - "waddlebot-bridge/internal/bridge" - "waddlebot-bridge/internal/config" - "waddlebot-bridge/internal/license" - "waddlebot-bridge/internal/modules" - "waddlebot-bridge/internal/poller" - "waddlebot-bridge/internal/server" - "waddlebot-bridge/internal/storage" - "waddlebot-bridge/internal/testutils" -) - -func TestIntegration_FullSystem(t *testing.T) { - // Skip if running in CI without proper setup - if os.Getenv("CI") != "" { - t.Skip("Skipping integration tests in CI") - } - - // Create temporary directory for test - tmpDir := t.TempDir() - - // Create test config - cfg := &config.Config{ - APIURL: "https://api.waddlebot.io", - CommunityID: "test-community", - UserID: "test-user", - PollInterval: 30, - WebPort: 8080, - WebHost: "127.0.0.1", - LogLevel: "info", - DataDir: tmpDir, - ModulesDir: filepath.Join(tmpDir, "modules"), - WebAuthnDisplayName: "WaddleBot Bridge for Test", - WebAuthnOrigin: "http://127.0.0.1:8080", - WebAuthnTimeout: 60, - ModuleTimeout: 30, - MaxConcurrentTasks: 10, - } - - // Create storage - storage, err := storage.NewBoltStorage(tmpDir) - if err != nil { - t.Fatalf("Failed to create storage: %v", err) - } - defer storage.Close() - - // Create authenticator - authenticator, err := auth.NewWebAuthnManager(cfg, storage) - if err != nil { - t.Fatalf("Failed to create authenticator: %v", err) - } - - // Create module manager - moduleManager := modules.NewManager(cfg, storage) - - // Create bridge client - bridgeClient, err := bridge.NewClient(cfg, authenticator, moduleManager) - if err != nil { - t.Fatalf("Failed to create bridge client: %v", err) - } - - // Create poller - poller := poller.NewPoller(cfg, bridgeClient, moduleManager) - - // Create web server - webServer := server.NewWebServer(cfg, authenticator, bridgeClient) - - // Test that all components are properly initialized - if storage == nil { - t.Error("Storage should be initialized") - } - - if authenticator == nil { - t.Error("Authenticator should be initialized") - } - - if moduleManager == nil { - t.Error("Module manager should be initialized") - } - - if bridgeClient == nil { - t.Error("Bridge client should be initialized") - } - - if poller == nil { - t.Error("Poller should be initialized") - } - - if webServer == nil { - t.Error("Web server should be initialized") - } - - // Test storage operations - testKey := "test-key" - testValue := []byte("test-value") - - err = storage.Set(testKey, testValue) - if err != nil { - t.Fatalf("Failed to set storage value: %v", err) - } - - retrievedValue, err := storage.Get(testKey) - if err != nil { - t.Fatalf("Failed to get storage value: %v", err) - } - - if string(retrievedValue) != string(testValue) { - t.Errorf("Expected value %s, got %s", string(testValue), string(retrievedValue)) - } - - // Test module manager operations - stats := moduleManager.GetStats() - if stats == nil { - t.Error("Module manager stats should not be nil") - } - - // Test bridge client operations - if bridgeClient.IsAuthenticated() { - t.Error("Bridge client should not be authenticated initially") - } - - // Test poller operations - pollerStats := poller.GetStats() - if pollerStats == nil { - t.Error("Poller stats should not be nil") - } - - // Test cleanup - err = moduleManager.Cleanup() - if err != nil { - t.Errorf("Module manager cleanup failed: %v", err) - } -} - -func TestIntegration_LicenseValidation(t *testing.T) { - // Test license validation - result := license.ValidateLicense() - if result { - t.Error("License should not be valid initially") - } - - // Test license info - info := license.GetLicenseInfo() - if info == nil { - t.Error("License info should not be nil") - } - - if info["version"] != "1.0.0" { - t.Errorf("Expected version '1.0.0', got %v", info["version"]) - } - - if info["type"] != "Premium" { - t.Errorf("Expected type 'Premium', got %v", info["type"]) - } - - // Test license display (should not panic) - license.DisplayLicenseInfo() -} - -func TestIntegration_WebServerEndpoints(t *testing.T) { - // Create test components - cfg := testutils.TestConfig() - authenticator := testutils.NewMockWebAuthnManager() - bridgeClient := testutils.NewMockBridgeClient(cfg) - - webServer := server.NewWebServer(cfg, authenticator, bridgeClient) - - // Test endpoints - endpoints := []struct { - path string - method string - status int - }{ - {"/", "GET", http.StatusOK}, - {"/health", "GET", http.StatusOK}, - {"/status", "GET", http.StatusOK}, - } - - for _, endpoint := range endpoints { - t.Run(fmt.Sprintf("%s %s", endpoint.method, endpoint.path), func(t *testing.T) { - req := httptest.NewRequest(endpoint.method, endpoint.path, nil) - w := httptest.NewRecorder() - - switch endpoint.path { - case "/": - webServer.handleIndex(w, req) - case "/health": - webServer.handleHealth(w, req) - case "/status": - webServer.handleStatus(w, req) - } - - if w.Code != endpoint.status { - t.Errorf("Expected status %d, got %d", endpoint.status, w.Code) - } - }) - } -} - -func TestIntegration_PollerWithMockServer(t *testing.T) { - // Create mock server - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path == "/api/bridge/poll" { - response := map[string]interface{}{ - "actions": []interface{}{}, - "next_poll": time.Now().Add(30 * time.Second), - "server_time": time.Now(), - "has_more": false, - "poll_count": 1, - "client_info": map[string]interface{}{ - "last_seen": time.Now(), - "actions_total": 0, - "actions_success": 0, - "actions_failed": 0, - "uptime": 3600, - }, - } - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(response) - } - })) - defer server.Close() - - // Create test components - cfg := testutils.TestConfig() - cfg.APIURL = server.URL - cfg.PollInterval = 1 // Short interval for testing - - authenticator := testutils.NewMockWebAuthnManager() - authenticator.AddSession("test-session", "test-user", "test-community") - - moduleManager := testutils.NewMockModuleManager() - bridgeClient := testutils.NewMockBridgeClient(cfg) - - poller := poller.NewPoller(cfg, bridgeClient, moduleManager) - - // Test polling - ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) - defer cancel() - - // Start poller (should not error) - err := poller.Start(ctx) - if err != nil { - t.Fatalf("Poller start failed: %v", err) - } -} - -func TestIntegration_ModuleExecution(t *testing.T) { - // Create test components - cfg := testutils.TestConfig() - storage := testutils.NewMockStorage() - moduleManager := modules.NewManager(cfg, storage) - - // Add test module - testModule := testutils.TestModule("test-module") - moduleManager.AddModule("test-module", testModule) - - // Test module execution - ctx, cancel := testutils.TestContext() - defer cancel() - - result, err := moduleManager.ExecuteAction(ctx, "test-module", "ping", map[string]string{}) - if err != nil { - t.Fatalf("Module execution failed: %v", err) - } - - if result["message"] != "pong" { - t.Errorf("Expected message 'pong', got %v", result["message"]) - } - - // Test module info - infos := moduleManager.GetModuleInfos() - if len(infos) != 1 { - t.Errorf("Expected 1 module info, got %d", len(infos)) - } - - if infos[0].Name != "test-module" { - t.Errorf("Expected module name 'test-module', got %s", infos[0].Name) - } - - // Test module management - err = moduleManager.DisableModule("test-module") - if err != nil { - t.Fatalf("Failed to disable module: %v", err) - } - - err = moduleManager.EnableModule("test-module") - if err != nil { - t.Fatalf("Failed to enable module: %v", err) - } - - // Test cleanup - err = moduleManager.Cleanup() - if err != nil { - t.Fatalf("Module cleanup failed: %v", err) - } -} - -func TestIntegration_AuthenticationFlow(t *testing.T) { - // Create test components - cfg := testutils.TestConfig() - storage := testutils.NewMockStorage() - - authenticator, err := auth.NewWebAuthnManager(cfg, storage) - if err != nil { - t.Fatalf("Failed to create authenticator: %v", err) - } - - // Test that initially not authenticated - session := authenticator.GetCurrentSession() - if session != nil { - t.Error("Should not have session initially") - } - - // Test authentication stats - stats := authenticator.GetStats() - if stats == nil { - t.Error("Authenticator stats should not be nil") - } - - // Test session management - sessions := authenticator.GetActiveSessions() - if len(sessions) != 0 { - t.Errorf("Expected 0 active sessions, got %d", len(sessions)) - } - - // Test cleanup - err = authenticator.Cleanup() - if err != nil { - t.Errorf("Authenticator cleanup failed: %v", err) - } -} - -func TestIntegration_StorageOperations(t *testing.T) { - // Create temporary directory - tmpDir := t.TempDir() - - // Create storage - storage, err := storage.NewBoltStorage(tmpDir) - if err != nil { - t.Fatalf("Failed to create storage: %v", err) - } - defer storage.Close() - - // Test basic operations - testData := map[string][]byte{ - "key1": []byte("value1"), - "key2": []byte("value2"), - "key3": []byte("value3"), - } - - // Set values - for key, value := range testData { - err = storage.Set(key, value) - if err != nil { - t.Fatalf("Failed to set key %s: %v", key, err) - } - } - - // Get values - for key, expectedValue := range testData { - actualValue, err := storage.Get(key) - if err != nil { - t.Fatalf("Failed to get key %s: %v", key, err) - } - - if string(actualValue) != string(expectedValue) { - t.Errorf("Expected value %s for key %s, got %s", string(expectedValue), key, string(actualValue)) - } - } - - // Test bucket operations - bucketName := "test-bucket" - err = storage.SetWithBucket(bucketName, "bucket-key", []byte("bucket-value")) - if err != nil { - t.Fatalf("Failed to set bucket value: %v", err) - } - - bucketValue, err := storage.GetWithBucket(bucketName, "bucket-key") - if err != nil { - t.Fatalf("Failed to get bucket value: %v", err) - } - - if string(bucketValue) != "bucket-value" { - t.Errorf("Expected bucket value 'bucket-value', got %s", string(bucketValue)) - } - - // Test list operations - keys, err := storage.List("key") - if err != nil { - t.Fatalf("Failed to list keys: %v", err) - } - - if len(keys) != 3 { - t.Errorf("Expected 3 keys, got %d", len(keys)) - } - - // Test stats - stats := storage.Stats() - if stats == nil { - t.Error("Storage stats should not be nil") - } - - // Test backup - backupPath := filepath.Join(tmpDir, "backup.db") - err = storage.Backup(backupPath) - if err != nil { - t.Fatalf("Failed to backup storage: %v", err) - } - - // Verify backup file exists - if _, err := os.Stat(backupPath); os.IsNotExist(err) { - t.Error("Backup file was not created") - } -} - -func TestIntegration_ConfigurationLoading(t *testing.T) { - // Test configuration loading with temporary directory - tmpDir := t.TempDir() - - // Set environment variables for testing - os.Setenv("WADDLEBOT_DATA_DIR", tmpDir) - os.Setenv("WADDLEBOT_API_URL", "https://test.api.com") - os.Setenv("WADDLEBOT_POLL_INTERVAL", "60") - defer func() { - os.Unsetenv("WADDLEBOT_DATA_DIR") - os.Unsetenv("WADDLEBOT_API_URL") - os.Unsetenv("WADDLEBOT_POLL_INTERVAL") - }() - - // Test that configuration directories are created - expectedModulesDir := filepath.Join(tmpDir, "modules") - - // Create directories - err := os.MkdirAll(tmpDir, 0755) - if err != nil { - t.Fatalf("Failed to create data directory: %v", err) - } - - err = os.MkdirAll(expectedModulesDir, 0755) - if err != nil { - t.Fatalf("Failed to create modules directory: %v", err) - } - - // Verify directories exist - if _, err := os.Stat(tmpDir); os.IsNotExist(err) { - t.Error("Data directory was not created") - } - - if _, err := os.Stat(expectedModulesDir); os.IsNotExist(err) { - t.Error("Modules directory was not created") - } -} - -func TestIntegration_ErrorHandling(t *testing.T) { - // Test error handling throughout the system - cfg := testutils.TestConfig() - cfg.APIURL = "http://nonexistent.domain.com" - - // Create components - storage := testutils.NewMockStorage() - authenticator := testutils.NewMockWebAuthnManager() - moduleManager := modules.NewManager(cfg, storage) - bridgeClient := testutils.NewMockBridgeClient(cfg) - - // Test bridge client with invalid URL - bridgeClient.SetAuthError(true) - _, err := bridgeClient.GetAuthToken() - if err == nil { - t.Error("Expected error for invalid auth") - } - - // Test module manager with nonexistent module - ctx, cancel := testutils.TestContext() - defer cancel() - - _, err = moduleManager.ExecuteAction(ctx, "nonexistent-module", "ping", map[string]string{}) - if err == nil { - t.Error("Expected error for nonexistent module") - } - - // Test storage with mock error - mockStorage := testutils.NewMockStorage() - mockStorage.SetError(true) - - _, err = mockStorage.Get("test-key") - if err == nil { - t.Error("Expected error from mock storage") - } - - // Test authenticator error conditions - authenticator.SetRegistrationError(true) - _, err = authenticator.StartRegistration("test-user", "test-community") - if err == nil { - t.Error("Expected registration error") - } - - authenticator.SetAuthenticationError(true) - _, err = authenticator.StartAuthentication("test-user") - if err == nil { - t.Error("Expected authentication error") - } -} \ No newline at end of file diff --git a/Premium/Desktop/internal/auth/errors.go b/Premium/Desktop/internal/auth/errors.go deleted file mode 100644 index 67545f6ae..000000000 --- a/Premium/Desktop/internal/auth/errors.go +++ /dev/null @@ -1,16 +0,0 @@ -package auth - -import "fmt" - -// Common auth errors -var ( - ErrSessionNotFound = fmt.Errorf("session not found") - ErrSessionExpired = fmt.Errorf("session expired") - ErrInvalidCredentials = fmt.Errorf("invalid credentials") - ErrUserNotFound = fmt.Errorf("user not found") - ErrRegistrationFailed = fmt.Errorf("registration failed") - ErrAuthenticationFailed = fmt.Errorf("authentication failed") - ErrInvalidToken = fmt.Errorf("invalid token") - ErrTokenExpired = fmt.Errorf("token expired") - ErrPermissionDenied = fmt.Errorf("permission denied") -) \ No newline at end of file diff --git a/Premium/Desktop/internal/auth/webauthn.go b/Premium/Desktop/internal/auth/webauthn.go deleted file mode 100644 index 3d56801b7..000000000 --- a/Premium/Desktop/internal/auth/webauthn.go +++ /dev/null @@ -1,465 +0,0 @@ -package auth - -import ( - "encoding/json" - "fmt" - "time" - - "github.com/go-webauthn/webauthn/protocol" - "github.com/go-webauthn/webauthn/webauthn" - "github.com/golang-jwt/jwt/v5" - "github.com/google/uuid" - "github.com/sirupsen/logrus" - "waddlebot-bridge/internal/config" - "waddlebot-bridge/internal/logger" - "waddlebot-bridge/internal/models" - "waddlebot-bridge/internal/storage" -) - -// WebAuthnManager handles WebAuthn authentication -type WebAuthnManager struct { - config *config.Config - storage storage.Storage - webauthn *webauthn.WebAuthn - logger *logrus.Logger - sessions map[string]*models.AuthSession - jwtSecret []byte -} - -// Session is an alias for models.AuthSession to avoid package name stuttering -type Session = models.AuthSession - -// User represents a WebAuthn user -type User struct { - ID []byte `json:"id"` - Name string `json:"name"` - DisplayName string `json:"display_name"` - CommunityID string `json:"community_id"` - Credentials []webauthn.Credential `json:"credentials"` -} - -// WebAuthnID returns the user's WebAuthn ID -func (u *User) WebAuthnID() []byte { - return u.ID -} - -// WebAuthnName returns the user's WebAuthn name -func (u *User) WebAuthnName() string { - return u.Name -} - -// WebAuthnDisplayName returns the user's WebAuthn display name -func (u *User) WebAuthnDisplayName() string { - return u.DisplayName -} - -// WebAuthnCredentials returns the user's WebAuthn credentials -func (u *User) WebAuthnCredentials() []webauthn.Credential { - return u.Credentials -} - -// WebAuthnIcon returns the user's WebAuthn icon (optional) -func (u *User) WebAuthnIcon() string { - return "" -} - -// NewWebAuthnManager creates a new WebAuthn manager -func NewWebAuthnManager(cfg *config.Config, store storage.Storage) (*WebAuthnManager, error) { - // Configure WebAuthn - timeoutDuration := time.Duration(cfg.WebAuthnTimeout) * time.Second - wconfig := &webauthn.Config{ - RPDisplayName: cfg.WebAuthnDisplayName, - RPID: "localhost", - RPOrigins: []string{cfg.GetWebAuthnURL()}, - AuthenticatorSelection: protocol.AuthenticatorSelection{ - ResidentKey: protocol.ResidentKeyRequirementDiscouraged, - UserVerification: protocol.VerificationRequired, - }, - Timeouts: webauthn.TimeoutsConfig{ - Registration: webauthn.TimeoutConfig{ - Timeout: timeoutDuration, - }, - Login: webauthn.TimeoutConfig{ - Timeout: timeoutDuration, - }, - }, - } - - // Create WebAuthn instance - webAuthn, err := webauthn.New(wconfig) - if err != nil { - return nil, fmt.Errorf("failed to create WebAuthn instance: %w", err) - } - - // Generate JWT secret if not provided - jwtSecret := []byte(cfg.JWTSecret) - if len(jwtSecret) == 0 { - jwtSecret = []byte(uuid.New().String()) - } - - manager := &WebAuthnManager{ - config: cfg, - storage: store, - webauthn: webAuthn, - logger: logger.GetLogger(), - sessions: make(map[string]*Session), - jwtSecret: jwtSecret, - } - - // Load existing sessions from storage - manager.loadSessions() - - return manager, nil -} - -// StartRegistration starts the WebAuthn registration process -func (m *WebAuthnManager) StartRegistration(userID, communityID string) (*protocol.CredentialCreation, error) { - // Check if user is already registered - if _, exists := m.getUserByID(userID); exists { - return nil, fmt.Errorf("user %s is already registered", userID) - } - - // Create new user - user := &User{ - ID: []byte(userID), - Name: userID, - DisplayName: fmt.Sprintf("User %s", userID), - CommunityID: communityID, - Credentials: []webauthn.Credential{}, - } - - // Begin registration - creation, session, err := m.webauthn.BeginRegistration(user) - if err != nil { - return nil, fmt.Errorf("failed to begin registration: %w", err) - } - - // Store session for completion - sessionData, err := json.Marshal(session) - if err != nil { - return nil, fmt.Errorf("failed to marshal session: %w", err) - } - - key := fmt.Sprintf("registration_session_%s", userID) - if err := m.storage.Set(key, sessionData); err != nil { - return nil, fmt.Errorf("failed to store session: %w", err) - } - - // Store user temporarily - userData, err := json.Marshal(user) - if err != nil { - return nil, fmt.Errorf("failed to marshal user: %w", err) - } - - userKey := fmt.Sprintf("temp_user_%s", userID) - if err := m.storage.Set(userKey, userData); err != nil { - return nil, fmt.Errorf("failed to store user: %w", err) - } - - m.logger.WithFields(logrus.Fields{ - "user_id": userID, - "community_id": communityID, - }).Info("Started WebAuthn registration") - - return creation, nil -} - -// CompleteRegistration completes the WebAuthn registration process -func (m *WebAuthnManager) CompleteRegistration(userID string, response []byte) (*models.AuthSession, error) { - // Get stored session - sessionKey := fmt.Sprintf("registration_session_%s", userID) - sessionData, err := m.storage.Get(sessionKey) - if err != nil { - return nil, fmt.Errorf("failed to get session: %w", err) - } - - var session webauthn.SessionData - if err := json.Unmarshal(sessionData, &session); err != nil { - return nil, fmt.Errorf("failed to unmarshal session: %w", err) - } - - // Get temporary user - userKey := fmt.Sprintf("temp_user_%s", userID) - userData, err := m.storage.Get(userKey) - if err != nil { - return nil, fmt.Errorf("failed to get user: %w", err) - } - - var user User - if err := json.Unmarshal(userData, &user); err != nil { - return nil, fmt.Errorf("failed to unmarshal user: %w", err) - } - - // Parse the credential creation response - parsedResponse, err := protocol.ParseCredentialCreationResponseBytes(response) - if err != nil { - return nil, fmt.Errorf("failed to parse credential response: %w", err) - } - - // Complete registration - credential, err := m.webauthn.CreateCredential(&user, session, parsedResponse) - if err != nil { - return nil, fmt.Errorf("failed to create credential: %w", err) - } - - // Add credential to user - user.Credentials = append(user.Credentials, *credential) - - // Store user permanently - finalUserData, err := json.Marshal(user) - if err != nil { - return nil, fmt.Errorf("failed to marshal final user: %w", err) - } - - permanentUserKey := fmt.Sprintf("user_%s", userID) - if err := m.storage.Set(permanentUserKey, finalUserData); err != nil { - return nil, fmt.Errorf("failed to store final user: %w", err) - } - - // Clean up temporary data - m.storage.Delete(sessionKey) - m.storage.Delete(userKey) - - // Create auth session - authSession := &models.AuthSession{ - ID: uuid.New().String(), - UserID: userID, - CommunityID: user.CommunityID, - IssuedAt: time.Now(), - ExpiresAt: time.Now().Add(24 * time.Hour), - Credential: credential.ID, - } - - // Store auth session - m.sessions[authSession.ID] = authSession - m.saveSessions() - - m.logger.WithFields(logrus.Fields{ - "user_id": userID, - "community_id": user.CommunityID, - "session_id": authSession.ID, - }).Info("Completed WebAuthn registration") - - return authSession, nil -} - -// StartAuthentication starts the WebAuthn authentication process -func (m *WebAuthnManager) StartAuthentication(userID string) (*protocol.CredentialAssertion, error) { - // Get user - user, exists := m.getUserByID(userID) - if !exists { - return nil, fmt.Errorf("user %s not found", userID) - } - - // Begin authentication - assertion, session, err := m.webauthn.BeginLogin(user) - if err != nil { - return nil, fmt.Errorf("failed to begin authentication: %w", err) - } - - // Store session for completion - sessionData, err := json.Marshal(session) - if err != nil { - return nil, fmt.Errorf("failed to marshal session: %w", err) - } - - key := fmt.Sprintf("auth_session_%s", userID) - if err := m.storage.Set(key, sessionData); err != nil { - return nil, fmt.Errorf("failed to store session: %w", err) - } - - m.logger.WithFields(logrus.Fields{ - "user_id": userID, - "community_id": user.CommunityID, - }).Info("Started WebAuthn authentication") - - return assertion, nil -} - -// CompleteAuthentication completes the WebAuthn authentication process -func (m *WebAuthnManager) CompleteAuthentication(userID string, response []byte) (*models.AuthSession, error) { - // Get stored session - sessionKey := fmt.Sprintf("auth_session_%s", userID) - sessionData, err := m.storage.Get(sessionKey) - if err != nil { - return nil, fmt.Errorf("failed to get session: %w", err) - } - - var session webauthn.SessionData - if err := json.Unmarshal(sessionData, &session); err != nil { - return nil, fmt.Errorf("failed to unmarshal session: %w", err) - } - - // Get user - user, exists := m.getUserByID(userID) - if !exists { - return nil, fmt.Errorf("user %s not found", userID) - } - - // Parse the credential assertion response - parsedResponse, err := protocol.ParseCredentialRequestResponseBytes(response) - if err != nil { - return nil, fmt.Errorf("failed to parse credential response: %w", err) - } - - // Complete authentication - credential, err := m.webauthn.ValidateLogin(user, session, parsedResponse) - if err != nil { - return nil, fmt.Errorf("failed to validate login: %w", err) - } - - // Clean up session - m.storage.Delete(sessionKey) - - // Create auth session - authSession := &models.AuthSession{ - ID: uuid.New().String(), - UserID: userID, - CommunityID: user.CommunityID, - IssuedAt: time.Now(), - ExpiresAt: time.Now().Add(24 * time.Hour), - Credential: credential.ID, - } - - // Store auth session - m.sessions[authSession.ID] = authSession - m.saveSessions() - - m.logger.WithFields(logrus.Fields{ - "user_id": userID, - "community_id": user.CommunityID, - "session_id": authSession.ID, - }).Info("Completed WebAuthn authentication") - - return authSession, nil -} - -// ValidateSession validates an authentication session -func (m *WebAuthnManager) ValidateSession(sessionID string) (*models.AuthSession, error) { - session, exists := m.sessions[sessionID] - if !exists { - return nil, fmt.Errorf("session not found") - } - - if time.Now().After(session.ExpiresAt) { - delete(m.sessions, sessionID) - m.saveSessions() - return nil, fmt.Errorf("session expired") - } - - return session, nil -} - -// GenerateJWT generates a JWT token for the session -func (m *WebAuthnManager) GenerateJWT(session *models.AuthSession) (string, error) { - claims := jwt.MapClaims{ - "sub": session.UserID, - "community_id": session.CommunityID, - "session_id": session.ID, - "iat": session.IssuedAt.Unix(), - "exp": session.ExpiresAt.Unix(), - } - - token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - return token.SignedString(m.jwtSecret) -} - -// ValidateJWT validates a JWT token -func (m *WebAuthnManager) ValidateJWT(tokenString string) (*models.AuthSession, error) { - token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { - if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { - return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) - } - return m.jwtSecret, nil - }) - - if err != nil { - return nil, fmt.Errorf("failed to parse token: %w", err) - } - - if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid { - sessionID, ok := claims["session_id"].(string) - if !ok { - return nil, fmt.Errorf("invalid session_id in token") - } - - return m.ValidateSession(sessionID) - } - - return nil, fmt.Errorf("invalid token") -} - -// RevokeSession revokes an authentication session -func (m *WebAuthnManager) RevokeSession(sessionID string) error { - delete(m.sessions, sessionID) - m.saveSessions() - - m.logger.WithField("session_id", sessionID).Info("Revoked authentication session") - return nil -} - -// getUserByID retrieves a user by ID -func (m *WebAuthnManager) getUserByID(userID string) (*User, bool) { - key := fmt.Sprintf("user_%s", userID) - userData, err := m.storage.Get(key) - if err != nil { - return nil, false - } - - var user User - if err := json.Unmarshal(userData, &user); err != nil { - return nil, false - } - - return &user, true -} - -// loadSessions loads existing sessions from storage -func (m *WebAuthnManager) loadSessions() { - data, err := m.storage.Get("auth_sessions") - if err != nil { - return // No existing sessions - } - - var sessions map[string]*models.AuthSession - if err := json.Unmarshal(data, &sessions); err != nil { - m.logger.WithError(err).Error("Failed to unmarshal sessions") - return - } - - // Filter out expired sessions - now := time.Now() - for id, session := range sessions { - if now.Before(session.ExpiresAt) { - m.sessions[id] = session - } - } -} - -// saveSessions saves current sessions to storage -func (m *WebAuthnManager) saveSessions() { - data, err := json.Marshal(m.sessions) - if err != nil { - m.logger.WithError(err).Error("Failed to marshal sessions") - return - } - - if err := m.storage.Set("auth_sessions", data); err != nil { - m.logger.WithError(err).Error("Failed to save sessions") - } -} - -// IsAuthenticated checks if the current session is authenticated -func (m *WebAuthnManager) IsAuthenticated() bool { - return len(m.sessions) > 0 -} - -// GetCurrentSession returns the current active session (if any) -func (m *WebAuthnManager) GetCurrentSession() *models.AuthSession { - for _, session := range m.sessions { - if time.Now().Before(session.ExpiresAt) { - return session - } - } - return nil -} \ No newline at end of file diff --git a/Premium/Desktop/internal/auth/webauthn_test.go b/Premium/Desktop/internal/auth/webauthn_test.go deleted file mode 100644 index b6f93575e..000000000 --- a/Premium/Desktop/internal/auth/webauthn_test.go +++ /dev/null @@ -1,449 +0,0 @@ -package auth - -import ( - "testing" - "time" - - "github.com/go-webauthn/webauthn/webauthn" - "waddlebot-bridge/internal/testutils" -) - -func TestNewWebAuthnManager(t *testing.T) { - cfg := testutils.TestConfig() - storage := testutils.NewMockStorage() - - manager, err := NewWebAuthnManager(cfg, storage) - if err != nil { - t.Fatalf("NewWebAuthnManager failed: %v", err) - } - - if manager == nil { - t.Fatal("Expected non-nil manager") - } - - if manager.config != cfg { - t.Error("Expected config to be set") - } - - if manager.storage != storage { - t.Error("Expected storage to be set") - } - - if manager.sessions == nil { - t.Error("Expected sessions map to be initialized") - } -} - -func TestWebAuthnManager_StartRegistration(t *testing.T) { - cfg := testutils.TestConfig() - storage := testutils.NewMockStorage() - manager, err := NewWebAuthnManager(cfg, storage) - if err != nil { - t.Fatalf("NewWebAuthnManager failed: %v", err) - } - - userID := "test-user" - communityID := "test-community" - - // Test successful registration start - creation, err := manager.StartRegistration(userID, communityID) - if err != nil { - t.Fatalf("StartRegistration failed: %v", err) - } - - if creation == nil { - t.Fatal("Expected non-nil creation") - } - - // Test duplicate registration - _, err = manager.StartRegistration(userID, communityID) - if err == nil { - t.Error("Expected error for duplicate registration") - } -} - -func TestWebAuthnManager_StartAuthentication(t *testing.T) { - cfg := testutils.TestConfig() - storage := testutils.NewMockStorage() - manager, err := NewWebAuthnManager(cfg, storage) - if err != nil { - t.Fatalf("NewWebAuthnManager failed: %v", err) - } - - userID := "test-user" - - // Test authentication for non-existent user - _, err = manager.StartAuthentication(userID) - if err == nil { - t.Error("Expected error for non-existent user") - } - - // TODO: Add test for successful authentication start - // This requires a more complex setup with actual WebAuthn credentials -} - -func TestWebAuthnManager_ValidateSession(t *testing.T) { - cfg := testutils.TestConfig() - storage := testutils.NewMockStorage() - manager, err := NewWebAuthnManager(cfg, storage) - if err != nil { - t.Fatalf("NewWebAuthnManager failed: %v", err) - } - - sessionID := "test-session" - userID := "test-user" - communityID := "test-community" - - // Test non-existent session - _, err = manager.ValidateSession(sessionID) - if err == nil { - t.Error("Expected error for non-existent session") - } - - // Create a test session - session := &Session{ - ID: sessionID, - UserID: userID, - CommunityID: communityID, - IssuedAt: time.Now(), - ExpiresAt: time.Now().Add(time.Hour), - } - manager.sessions[sessionID] = session - - // Test valid session - validatedSession, err := manager.ValidateSession(sessionID) - if err != nil { - t.Fatalf("ValidateSession failed: %v", err) - } - - if validatedSession.ID != sessionID { - t.Errorf("Expected session ID %s, got %s", sessionID, validatedSession.ID) - } - - if validatedSession.UserID != userID { - t.Errorf("Expected user ID %s, got %s", userID, validatedSession.UserID) - } - - // Test expired session - session.ExpiresAt = time.Now().Add(-time.Hour) - _, err = manager.ValidateSession(sessionID) - if err == nil { - t.Error("Expected error for expired session") - } -} - -func TestWebAuthnManager_GenerateJWT(t *testing.T) { - cfg := testutils.TestConfig() - storage := testutils.NewMockStorage() - manager, err := NewWebAuthnManager(cfg, storage) - if err != nil { - t.Fatalf("NewWebAuthnManager failed: %v", err) - } - - session := &Session{ - ID: "test-session", - UserID: "test-user", - CommunityID: "test-community", - IssuedAt: time.Now(), - ExpiresAt: time.Now().Add(time.Hour), - } - - token, err := manager.GenerateJWT(session) - if err != nil { - t.Fatalf("GenerateJWT failed: %v", err) - } - - if token == "" { - t.Error("Expected non-empty token") - } - - // Test token validation - validatedSession, err := manager.ValidateJWT(token) - if err != nil { - t.Fatalf("ValidateJWT failed: %v", err) - } - - if validatedSession == nil { - t.Fatal("Expected non-nil session") - } - - if validatedSession.ID != session.ID { - t.Errorf("Expected session ID %s, got %s", session.ID, validatedSession.ID) - } -} - -func TestWebAuthnManager_ValidateJWT(t *testing.T) { - cfg := testutils.TestConfig() - storage := testutils.NewMockStorage() - manager, err := NewWebAuthnManager(cfg, storage) - if err != nil { - t.Fatalf("NewWebAuthnManager failed: %v", err) - } - - // Test invalid token - _, err = manager.ValidateJWT("invalid-token") - if err == nil { - t.Error("Expected error for invalid token") - } - - // Test empty token - _, err = manager.ValidateJWT("") - if err == nil { - t.Error("Expected error for empty token") - } -} - -func TestWebAuthnManager_RevokeSession(t *testing.T) { - cfg := testutils.TestConfig() - storage := testutils.NewMockStorage() - manager, err := NewWebAuthnManager(cfg, storage) - if err != nil { - t.Fatalf("NewWebAuthnManager failed: %v", err) - } - - sessionID := "test-session" - session := &Session{ - ID: sessionID, - UserID: "test-user", - CommunityID: "test-community", - IssuedAt: time.Now(), - ExpiresAt: time.Now().Add(time.Hour), - } - - manager.sessions[sessionID] = session - - // Test session exists - if _, exists := manager.sessions[sessionID]; !exists { - t.Error("Expected session to exist") - } - - // Revoke session - err = manager.RevokeSession(sessionID) - if err != nil { - t.Fatalf("RevokeSession failed: %v", err) - } - - // Test session no longer exists - if _, exists := manager.sessions[sessionID]; exists { - t.Error("Expected session to be removed") - } -} - -func TestWebAuthnManager_IsAuthenticated(t *testing.T) { - cfg := testutils.TestConfig() - storage := testutils.NewMockStorage() - manager, err := NewWebAuthnManager(cfg, storage) - if err != nil { - t.Fatalf("NewWebAuthnManager failed: %v", err) - } - - // Test no sessions - if manager.IsAuthenticated() { - t.Error("Expected false for no sessions") - } - - // Add a session - session := &Session{ - ID: "test-session", - UserID: "test-user", - CommunityID: "test-community", - IssuedAt: time.Now(), - ExpiresAt: time.Now().Add(time.Hour), - } - manager.sessions[session.ID] = session - - // Test with sessions - if !manager.IsAuthenticated() { - t.Error("Expected true for existing sessions") - } -} - -func TestWebAuthnManager_GetCurrentSession(t *testing.T) { - cfg := testutils.TestConfig() - storage := testutils.NewMockStorage() - manager, err := NewWebAuthnManager(cfg, storage) - if err != nil { - t.Fatalf("NewWebAuthnManager failed: %v", err) - } - - // Test no sessions - session := manager.GetCurrentSession() - if session != nil { - t.Error("Expected nil for no sessions") - } - - // Add a valid session - validSession := &Session{ - ID: "test-session", - UserID: "test-user", - CommunityID: "test-community", - IssuedAt: time.Now(), - ExpiresAt: time.Now().Add(time.Hour), - } - manager.sessions[validSession.ID] = validSession - - // Test with valid session - currentSession := manager.GetCurrentSession() - if currentSession == nil { - t.Error("Expected non-nil session") - } - - if currentSession.ID != validSession.ID { - t.Errorf("Expected session ID %s, got %s", validSession.ID, currentSession.ID) - } - - // Add an expired session - expiredSession := &Session{ - ID: "expired-session", - UserID: "test-user", - CommunityID: "test-community", - IssuedAt: time.Now().Add(-2 * time.Hour), - ExpiresAt: time.Now().Add(-time.Hour), - } - manager.sessions[expiredSession.ID] = expiredSession - - // Should still return the valid session - currentSession = manager.GetCurrentSession() - if currentSession == nil { - t.Error("Expected non-nil session") - } - - if currentSession.ID != validSession.ID { - t.Errorf("Expected session ID %s, got %s", validSession.ID, currentSession.ID) - } -} - -func TestUser_WebAuthnMethods(t *testing.T) { - user := &User{ - ID: []byte("test-user"), - Name: "test-user", - DisplayName: "Test User", - CommunityID: "test-community", - Credentials: []webauthn.Credential{}, - } - - // Test WebAuthnID - id := user.WebAuthnID() - if string(id) != "test-user" { - t.Errorf("Expected WebAuthnID 'test-user', got %s", string(id)) - } - - // Test WebAuthnName - name := user.WebAuthnName() - if name != "test-user" { - t.Errorf("Expected WebAuthnName 'test-user', got %s", name) - } - - // Test WebAuthnDisplayName - displayName := user.WebAuthnDisplayName() - if displayName != "Test User" { - t.Errorf("Expected WebAuthnDisplayName 'Test User', got %s", displayName) - } - - // Test WebAuthnCredentials - credentials := user.WebAuthnCredentials() - if len(credentials) != 0 { - t.Errorf("Expected 0 credentials, got %d", len(credentials)) - } - - // Test WebAuthnIcon - icon := user.WebAuthnIcon() - if icon != "" { - t.Errorf("Expected empty icon, got %s", icon) - } -} - -func TestAuthSession_Validation(t *testing.T) { - now := time.Now() - - tests := []struct { - name string - session *Session - expectErr bool - }{ - { - name: "valid session", - session: &Session{ - ID: "test-session", - UserID: "test-user", - CommunityID: "test-community", - IssuedAt: now, - ExpiresAt: now.Add(time.Hour), - }, - expectErr: false, - }, - { - name: "expired session", - session: &Session{ - ID: "test-session", - UserID: "test-user", - CommunityID: "test-community", - IssuedAt: now.Add(-2 * time.Hour), - ExpiresAt: now.Add(-time.Hour), - }, - expectErr: true, - }, - { - name: "empty session ID", - session: &Session{ - ID: "", - UserID: "test-user", - CommunityID: "test-community", - IssuedAt: now, - ExpiresAt: now.Add(time.Hour), - }, - expectErr: true, - }, - { - name: "empty user ID", - session: &Session{ - ID: "test-session", - UserID: "", - CommunityID: "test-community", - IssuedAt: now, - ExpiresAt: now.Add(time.Hour), - }, - expectErr: true, - }, - { - name: "empty community ID", - session: &Session{ - ID: "test-session", - UserID: "test-user", - CommunityID: "", - IssuedAt: now, - ExpiresAt: now.Add(time.Hour), - }, - expectErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - err := validateAuthSession(tt.session) - if tt.expectErr && err == nil { - t.Error("Expected error but got none") - } - if !tt.expectErr && err != nil { - t.Errorf("Expected no error but got: %v", err) - } - }) - } -} - -// Helper function to validate auth session -func validateAuthSession(session *Session) error { - if session.ID == "" { - return ErrInvalidCredentials - } - if session.UserID == "" { - return ErrInvalidCredentials - } - if session.CommunityID == "" { - return ErrInvalidCredentials - } - if time.Now().After(session.ExpiresAt) { - return ErrSessionExpired - } - return nil -} \ No newline at end of file diff --git a/Premium/Desktop/internal/bridge/client.go b/Premium/Desktop/internal/bridge/client.go deleted file mode 100644 index 950d8ef9b..000000000 --- a/Premium/Desktop/internal/bridge/client.go +++ /dev/null @@ -1,344 +0,0 @@ -package bridge - -import ( - "context" - "encoding/json" - "fmt" - "io" - "net/http" - "strings" - "time" - - "github.com/sirupsen/logrus" - "waddlebot-bridge/internal/auth" - "waddlebot-bridge/internal/config" - "waddlebot-bridge/internal/logger" - "waddlebot-bridge/internal/modules" -) - -// Client handles communication with the WaddleBot API -type Client struct { - config *config.Config - authenticator *auth.WebAuthnManager - moduleManager *modules.Manager - logger *logrus.Logger - httpClient *http.Client -} - -// Info represents bridge information -type Info struct { - BridgeID string `json:"bridge_id"` - UserID string `json:"user_id"` - CommunityID string `json:"community_id"` - Status string `json:"status"` - Version string `json:"version"` - Platform string `json:"platform"` - LastSeen time.Time `json:"last_seen"` - Capabilities []string `json:"capabilities"` -} - -// RegistrationRequest represents a bridge registration request -type RegistrationRequest struct { - UserID string `json:"user_id"` - CommunityID string `json:"community_id"` - BridgeInfo Info `json:"bridge_info"` - Modules []modules.ModuleInfo `json:"modules"` -} - -// RegistrationResponse represents the response from bridge registration -type RegistrationResponse struct { - Success bool `json:"success"` - BridgeID string `json:"bridge_id"` - Message string `json:"message"` - PollInterval int `json:"poll_interval"` -} - -// NewClient creates a new bridge client -func NewClient(cfg *config.Config, authenticator *auth.WebAuthnManager, moduleManager *modules.Manager) (*Client, error) { - return &Client{ - config: cfg, - authenticator: authenticator, - moduleManager: moduleManager, - logger: logger.GetLogger(), - httpClient: &http.Client{ - Timeout: 30 * time.Second, - }, - }, nil -} - -// GetAuthToken gets the current authentication token -func (c *Client) GetAuthToken() (string, error) { - session := c.authenticator.GetCurrentSession() - if session == nil { - return "", fmt.Errorf("no authenticated session found") - } - - return c.authenticator.GenerateJWT(session) -} - -// RegisterBridge registers the bridge with the WaddleBot API -func (c *Client) RegisterBridge(ctx context.Context) error { - c.logger.Info("Registering bridge with WaddleBot API") - - // Get authentication token - token, err := c.GetAuthToken() - if err != nil { - return fmt.Errorf("failed to get auth token: %w", err) - } - - // Get module information - moduleInfos := c.moduleManager.GetModuleInfos() - - // Create registration request - bridgeInfo := Info{ - UserID: c.config.UserID, - CommunityID: c.config.CommunityID, - Status: "active", - Version: "1.0.0", - Platform: fmt.Sprintf("%s/%s", c.config.GetUserAgent(), "desktop"), - LastSeen: time.Now(), - Capabilities: []string{ - "local_execution", - "file_operations", - "system_info", - "process_management", - "network_operations", - }, - } - - request := RegistrationRequest{ - UserID: c.config.UserID, - CommunityID: c.config.CommunityID, - BridgeInfo: bridgeInfo, - Modules: moduleInfos, - } - - // Marshal request - requestData, err := json.Marshal(request) - if err != nil { - return fmt.Errorf("failed to marshal registration request: %w", err) - } - - // Build registration URL - registrationURL := c.config.GetAPIEndpoint("/api/bridge/register") - - // Create request - req, err := http.NewRequestWithContext(ctx, "POST", registrationURL, - strings.NewReader(string(requestData))) - if err != nil { - return fmt.Errorf("failed to create request: %w", err) - } - - // Add headers - req.Header.Set("Authorization", "Bearer "+token) - req.Header.Set("User-Agent", c.config.GetUserAgent()) - req.Header.Set("Content-Type", "application/json") - req.Header.Set("X-Community-ID", c.config.CommunityID) - req.Header.Set("X-User-ID", c.config.UserID) - - // Make request - resp, err := c.httpClient.Do(req) - if err != nil { - return fmt.Errorf("failed to make request: %w", err) - } - defer resp.Body.Close() - - // Read response - body, err := io.ReadAll(resp.Body) - if err != nil { - return fmt.Errorf("failed to read response: %w", err) - } - - // Check status code - if resp.StatusCode != http.StatusOK { - return fmt.Errorf("server returned status %d: %s", resp.StatusCode, string(body)) - } - - // Parse response - var registrationResponse RegistrationResponse - if err := json.Unmarshal(body, ®istrationResponse); err != nil { - return fmt.Errorf("failed to parse response: %w", err) - } - - if !registrationResponse.Success { - return fmt.Errorf("registration failed: %s", registrationResponse.Message) - } - - c.logger.WithFields(logrus.Fields{ - "bridge_id": registrationResponse.BridgeID, - "poll_interval": registrationResponse.PollInterval, - }).Info("Bridge registered successfully") - - return nil -} - -// SendHeartbeat sends a heartbeat to the server -func (c *Client) SendHeartbeat(ctx context.Context) error { - // Get authentication token - token, err := c.GetAuthToken() - if err != nil { - return fmt.Errorf("failed to get auth token: %w", err) - } - - // Create heartbeat data - heartbeat := map[string]interface{}{ - "timestamp": time.Now(), - "status": "active", - "module_count": len(c.moduleManager.GetModuleInfos()), - "capabilities": []string{ - "local_execution", - "file_operations", - "system_info", - "process_management", - "network_operations", - }, - } - - // Marshal heartbeat - heartbeatData, err := json.Marshal(heartbeat) - if err != nil { - return fmt.Errorf("failed to marshal heartbeat: %w", err) - } - - // Build heartbeat URL - heartbeatURL := c.config.GetAPIEndpoint("/api/bridge/heartbeat") - - // Create request - req, err := http.NewRequestWithContext(ctx, "POST", heartbeatURL, - strings.NewReader(string(heartbeatData))) - if err != nil { - return fmt.Errorf("failed to create request: %w", err) - } - - // Add headers - req.Header.Set("Authorization", "Bearer "+token) - req.Header.Set("User-Agent", c.config.GetUserAgent()) - req.Header.Set("Content-Type", "application/json") - req.Header.Set("X-Community-ID", c.config.CommunityID) - req.Header.Set("X-User-ID", c.config.UserID) - - // Make request - resp, err := c.httpClient.Do(req) - if err != nil { - return fmt.Errorf("failed to make request: %w", err) - } - defer resp.Body.Close() - - // Check status code - if resp.StatusCode != http.StatusOK { - body, _ := io.ReadAll(resp.Body) - return fmt.Errorf("server returned status %d: %s", resp.StatusCode, string(body)) - } - - c.logger.Debug("Heartbeat sent successfully") - return nil -} - -// GetBridgeInfo retrieves bridge information from the server -func (c *Client) GetBridgeInfo(ctx context.Context) (*Info, error) { - // Get authentication token - token, err := c.GetAuthToken() - if err != nil { - return nil, fmt.Errorf("failed to get auth token: %w", err) - } - - // Build info URL - infoURL := c.config.GetAPIEndpoint("/api/bridge/info") - - // Create request - req, err := http.NewRequestWithContext(ctx, "GET", infoURL, nil) - if err != nil { - return nil, fmt.Errorf("failed to create request: %w", err) - } - - // Add headers - req.Header.Set("Authorization", "Bearer "+token) - req.Header.Set("User-Agent", c.config.GetUserAgent()) - req.Header.Set("X-Community-ID", c.config.CommunityID) - req.Header.Set("X-User-ID", c.config.UserID) - - // Make request - resp, err := c.httpClient.Do(req) - if err != nil { - return nil, fmt.Errorf("failed to make request: %w", err) - } - defer resp.Body.Close() - - // Read response - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("failed to read response: %w", err) - } - - // Check status code - if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("server returned status %d: %s", resp.StatusCode, string(body)) - } - - // Parse response - var bridgeInfo Info - if err := json.Unmarshal(body, &bridgeInfo); err != nil { - return nil, fmt.Errorf("failed to parse response: %w", err) - } - - return &bridgeInfo, nil -} - -// UnregisterBridge unregisters the bridge from the WaddleBot API -func (c *Client) UnregisterBridge(ctx context.Context) error { - c.logger.Info("Unregistering bridge from WaddleBot API") - - // Get authentication token - token, err := c.GetAuthToken() - if err != nil { - return fmt.Errorf("failed to get auth token: %w", err) - } - - // Build unregister URL - unregisterURL := c.config.GetAPIEndpoint("/api/bridge/unregister") - - // Create request - req, err := http.NewRequestWithContext(ctx, "POST", unregisterURL, nil) - if err != nil { - return fmt.Errorf("failed to create request: %w", err) - } - - // Add headers - req.Header.Set("Authorization", "Bearer "+token) - req.Header.Set("User-Agent", c.config.GetUserAgent()) - req.Header.Set("X-Community-ID", c.config.CommunityID) - req.Header.Set("X-User-ID", c.config.UserID) - - // Make request - resp, err := c.httpClient.Do(req) - if err != nil { - return fmt.Errorf("failed to make request: %w", err) - } - defer resp.Body.Close() - - // Check status code - if resp.StatusCode != http.StatusOK { - body, _ := io.ReadAll(resp.Body) - return fmt.Errorf("server returned status %d: %s", resp.StatusCode, string(body)) - } - - c.logger.Info("Bridge unregistered successfully") - return nil -} - -// IsAuthenticated checks if the client is authenticated -func (c *Client) IsAuthenticated() bool { - return c.authenticator.IsAuthenticated() -} - -// GetStats returns client statistics -func (c *Client) GetStats() map[string]interface{} { - return map[string]interface{}{ - "authenticated": c.IsAuthenticated(), - "user_id": c.config.UserID, - "community_id": c.config.CommunityID, - "api_url": c.config.APIURL, - "user_agent": c.config.GetUserAgent(), - "modules": len(c.moduleManager.GetModuleInfos()), - } -} diff --git a/Premium/Desktop/internal/bridge/client_test.go b/Premium/Desktop/internal/bridge/client_test.go deleted file mode 100644 index 68229f617..000000000 --- a/Premium/Desktop/internal/bridge/client_test.go +++ /dev/null @@ -1,461 +0,0 @@ -package bridge - -import ( - "context" - "net/http" - "net/http/httptest" - "testing" - "time" - - "waddlebot-bridge/internal/testutils" -) - -func TestNewClient(t *testing.T) { - cfg := testutils.TestConfig() - auth := testutils.NewMockWebAuthnManager() - moduleManager := testutils.NewMockModuleManager() - - client, err := NewClient(cfg, auth, moduleManager) - if err != nil { - t.Fatalf("NewClient failed: %v", err) - } - - if client == nil { - t.Fatal("Expected non-nil client") - } - - if client.config != cfg { - t.Error("Expected config to be set") - } - - if client.authenticator != auth { - t.Error("Expected authenticator to be set") - } - - if client.moduleManager != moduleManager { - t.Error("Expected moduleManager to be set") - } -} - -func TestClient_GetAuthToken(t *testing.T) { - cfg := testutils.TestConfig() - auth := testutils.NewMockWebAuthnManager() - moduleManager := testutils.NewMockModuleManager() - - client, err := NewClient(cfg, auth, moduleManager) - if err != nil { - t.Fatalf("NewClient failed: %v", err) - } - - // Test without session - _, err = client.GetAuthToken() - if err == nil { - t.Error("Expected error for no session") - } - - // Add a mock session - auth.AddSession("test-session", "test-user", "test-community") - - // Test with session - token, err := client.GetAuthToken() - if err != nil { - t.Fatalf("GetAuthToken failed: %v", err) - } - - if token == "" { - t.Error("Expected non-empty token") - } -} - -func TestClient_RegisterBridge(t *testing.T) { - // Create test server - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path != "/api/bridge/register" { - t.Errorf("Expected path '/api/bridge/register', got %s", r.URL.Path) - } - - if r.Method != "POST" { - t.Errorf("Expected method POST, got %s", r.Method) - } - - // Check headers - if r.Header.Get("Authorization") == "" { - t.Error("Expected Authorization header") - } - - if r.Header.Get("Content-Type") != "application/json" { - t.Error("Expected Content-Type application/json") - } - - // Return success response - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - w.Write([]byte(`{ - "success": true, - "bridge_id": "test-bridge-id", - "message": "Registration successful", - "poll_interval": 30 - }`)) - })) - defer server.Close() - - cfg := testutils.TestConfig() - cfg.APIURL = server.URL - auth := testutils.NewMockWebAuthnManager() - moduleManager := testutils.NewMockModuleManager() - - // Add a mock session - auth.AddSession("test-session", "test-user", "test-community") - - client, err := NewClient(cfg, auth, moduleManager) - if err != nil { - t.Fatalf("NewClient failed: %v", err) - } - - // Test successful registration - ctx, cancel := testutils.TestContext() - defer cancel() - - err = client.RegisterBridge(ctx) - if err != nil { - t.Fatalf("RegisterBridge failed: %v", err) - } -} - -func TestClient_RegisterBridge_Failure(t *testing.T) { - // Create test server that returns error - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte("Internal server error")) - })) - defer server.Close() - - cfg := testutils.TestConfig() - cfg.APIURL = server.URL - auth := testutils.NewMockWebAuthnManager() - moduleManager := testutils.NewMockModuleManager() - - // Add a mock session - auth.AddSession("test-session", "test-user", "test-community") - - client, err := NewClient(cfg, auth, moduleManager) - if err != nil { - t.Fatalf("NewClient failed: %v", err) - } - - // Test registration failure - ctx, cancel := testutils.TestContext() - defer cancel() - - err = client.RegisterBridge(ctx) - if err == nil { - t.Error("Expected error for registration failure") - } -} - -func TestClient_SendHeartbeat(t *testing.T) { - // Create test server - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path != "/api/bridge/heartbeat" { - t.Errorf("Expected path '/api/bridge/heartbeat', got %s", r.URL.Path) - } - - if r.Method != "POST" { - t.Errorf("Expected method POST, got %s", r.Method) - } - - // Check headers - if r.Header.Get("Authorization") == "" { - t.Error("Expected Authorization header") - } - - w.WriteHeader(http.StatusOK) - })) - defer server.Close() - - cfg := testutils.TestConfig() - cfg.APIURL = server.URL - auth := testutils.NewMockWebAuthnManager() - moduleManager := testutils.NewMockModuleManager() - - // Add a mock session - auth.AddSession("test-session", "test-user", "test-community") - - client, err := NewClient(cfg, auth, moduleManager) - if err != nil { - t.Fatalf("NewClient failed: %v", err) - } - - // Test successful heartbeat - ctx, cancel := testutils.TestContext() - defer cancel() - - err = client.SendHeartbeat(ctx) - if err != nil { - t.Fatalf("SendHeartbeat failed: %v", err) - } -} - -func TestClient_GetBridgeInfo(t *testing.T) { - // Create test server - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path != "/api/bridge/info" { - t.Errorf("Expected path '/api/bridge/info', got %s", r.URL.Path) - } - - if r.Method != "GET" { - t.Errorf("Expected method GET, got %s", r.Method) - } - - // Return bridge info - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - w.Write([]byte(`{ - "bridge_id": "test-bridge-id", - "user_id": "test-user", - "community_id": "test-community", - "status": "active", - "version": "1.0.0", - "platform": "test-platform", - "last_seen": "2024-01-01T00:00:00Z", - "capabilities": ["local_execution", "file_operations"] - }`)) - })) - defer server.Close() - - cfg := testutils.TestConfig() - cfg.APIURL = server.URL - auth := testutils.NewMockWebAuthnManager() - moduleManager := testutils.NewMockModuleManager() - - // Add a mock session - auth.AddSession("test-session", "test-user", "test-community") - - client, err := NewClient(cfg, auth, moduleManager) - if err != nil { - t.Fatalf("NewClient failed: %v", err) - } - - // Test getting bridge info - ctx, cancel := testutils.TestContext() - defer cancel() - - info, err := client.GetBridgeInfo(ctx) - if err != nil { - t.Fatalf("GetBridgeInfo failed: %v", err) - } - - if info.BridgeID != "test-bridge-id" { - t.Errorf("Expected bridge ID 'test-bridge-id', got %s", info.BridgeID) - } - - if info.UserID != "test-user" { - t.Errorf("Expected user ID 'test-user', got %s", info.UserID) - } - - if info.CommunityID != "test-community" { - t.Errorf("Expected community ID 'test-community', got %s", info.CommunityID) - } - - if info.Status != "active" { - t.Errorf("Expected status 'active', got %s", info.Status) - } -} - -func TestClient_UnregisterBridge(t *testing.T) { - // Create test server - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path != "/api/bridge/unregister" { - t.Errorf("Expected path '/api/bridge/unregister', got %s", r.URL.Path) - } - - if r.Method != "POST" { - t.Errorf("Expected method POST, got %s", r.Method) - } - - w.WriteHeader(http.StatusOK) - })) - defer server.Close() - - cfg := testutils.TestConfig() - cfg.APIURL = server.URL - auth := testutils.NewMockWebAuthnManager() - moduleManager := testutils.NewMockModuleManager() - - // Add a mock session - auth.AddSession("test-session", "test-user", "test-community") - - client, err := NewClient(cfg, auth, moduleManager) - if err != nil { - t.Fatalf("NewClient failed: %v", err) - } - - // Test successful unregistration - ctx, cancel := testutils.TestContext() - defer cancel() - - err = client.UnregisterBridge(ctx) - if err != nil { - t.Fatalf("UnregisterBridge failed: %v", err) - } -} - -func TestClient_IsAuthenticated(t *testing.T) { - cfg := testutils.TestConfig() - auth := testutils.NewMockWebAuthnManager() - moduleManager := testutils.NewMockModuleManager() - - client, err := NewClient(cfg, auth, moduleManager) - if err != nil { - t.Fatalf("NewClient failed: %v", err) - } - - // Test without session - if client.IsAuthenticated() { - t.Error("Expected false for no session") - } - - // Add a mock session - auth.AddSession("test-session", "test-user", "test-community") - - // Test with session - if !client.IsAuthenticated() { - t.Error("Expected true for existing session") - } -} - -func TestClient_GetStats(t *testing.T) { - cfg := testutils.TestConfig() - auth := testutils.NewMockWebAuthnManager() - moduleManager := testutils.NewMockModuleManager() - - client, err := NewClient(cfg, auth, moduleManager) - if err != nil { - t.Fatalf("NewClient failed: %v", err) - } - - stats := client.GetStats() - - if stats == nil { - t.Fatal("Expected non-nil stats") - } - - // Check required fields - if stats["user_id"] != cfg.UserID { - t.Errorf("Expected user_id %s, got %v", cfg.UserID, stats["user_id"]) - } - - if stats["community_id"] != cfg.CommunityID { - t.Errorf("Expected community_id %s, got %v", cfg.CommunityID, stats["community_id"]) - } - - if stats["api_url"] != cfg.APIURL { - t.Errorf("Expected api_url %s, got %v", cfg.APIURL, stats["api_url"]) - } - - if stats["authenticated"] != false { - t.Errorf("Expected authenticated false, got %v", stats["authenticated"]) - } -} - -func TestClient_RequestTimeout(t *testing.T) { - // Create test server that delays response - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - time.Sleep(2 * time.Second) - w.WriteHeader(http.StatusOK) - })) - defer server.Close() - - cfg := testutils.TestConfig() - cfg.APIURL = server.URL - auth := testutils.NewMockWebAuthnManager() - moduleManager := testutils.NewMockModuleManager() - - // Add a mock session - auth.AddSession("test-session", "test-user", "test-community") - - client, err := NewClient(cfg, auth, moduleManager) - if err != nil { - t.Fatalf("NewClient failed: %v", err) - } - - // Test request timeout - ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) - defer cancel() - - err = client.SendHeartbeat(ctx) - if err == nil { - t.Error("Expected timeout error") - } -} - -func TestClient_InvalidJSON(t *testing.T) { - // Create test server that returns invalid JSON - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - w.Write([]byte("invalid json")) - })) - defer server.Close() - - cfg := testutils.TestConfig() - cfg.APIURL = server.URL - auth := testutils.NewMockWebAuthnManager() - moduleManager := testutils.NewMockModuleManager() - - // Add a mock session - auth.AddSession("test-session", "test-user", "test-community") - - client, err := NewClient(cfg, auth, moduleManager) - if err != nil { - t.Fatalf("NewClient failed: %v", err) - } - - // Test invalid JSON response - ctx, cancel := testutils.TestContext() - defer cancel() - - _, err = client.GetBridgeInfo(ctx) - if err == nil { - t.Error("Expected error for invalid JSON") - } -} - -func TestClient_AuthorizationHeader(t *testing.T) { - // Create test server to check authorization header - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - authHeader := r.Header.Get("Authorization") - if authHeader == "" { - t.Error("Expected Authorization header") - } - - if !strings.HasPrefix(authHeader, "Bearer ") { - t.Errorf("Expected Bearer token, got %s", authHeader) - } - - w.WriteHeader(http.StatusOK) - })) - defer server.Close() - - cfg := testutils.TestConfig() - cfg.APIURL = server.URL - auth := testutils.NewMockWebAuthnManager() - moduleManager := testutils.NewMockModuleManager() - - // Add a mock session - auth.AddSession("test-session", "test-user", "test-community") - - client, err := NewClient(cfg, auth, moduleManager) - if err != nil { - t.Fatalf("NewClient failed: %v", err) - } - - // Test authorization header - ctx, cancel := testutils.TestContext() - defer cancel() - - err = client.SendHeartbeat(ctx) - if err != nil { - t.Fatalf("SendHeartbeat failed: %v", err) - } -} \ No newline at end of file diff --git a/Premium/Desktop/internal/config/config.go b/Premium/Desktop/internal/config/config.go deleted file mode 100644 index 6dd28e0b3..000000000 --- a/Premium/Desktop/internal/config/config.go +++ /dev/null @@ -1,237 +0,0 @@ -package config - -import ( - "fmt" - "os" - "path/filepath" - "runtime" - "time" - - "github.com/spf13/viper" -) - -// Config holds the application configuration -type Config struct { - // API Configuration - APIURL string `mapstructure:"api-url"` - CommunityID string `mapstructure:"community-id"` - UserID string `mapstructure:"user-id"` - - // Polling Configuration - PollInterval int `mapstructure:"poll-interval"` // in seconds - - // Web Server Configuration - WebPort int `mapstructure:"web-port"` - WebHost string `mapstructure:"web-host"` - - // Storage Configuration - DataDir string `mapstructure:"data-dir"` - - // Logging Configuration - LogLevel string `mapstructure:"log-level"` - - // WebAuthn Configuration - WebAuthnDisplayName string `mapstructure:"webauthn-display-name"` - WebAuthnOrigin string `mapstructure:"webauthn-origin"` - WebAuthnTimeout int `mapstructure:"webauthn-timeout"` - - // Security Configuration - JWTSecret string `mapstructure:"jwt-secret"` - - // Module Configuration - ModulesDir string `mapstructure:"modules-dir"` - ModuleTimeout int `mapstructure:"module-timeout"` - MaxConcurrentTasks int `mapstructure:"max-concurrent-tasks"` - - // OBS Configuration - OBS OBSConfig `mapstructure:"obs"` - - // Gateway Configuration - Gateway GatewayConfig `mapstructure:"gateway"` - - // Scripting Configuration - Scripting ScriptingConfig `mapstructure:"scripting"` -} - -// OBSConfig holds OBS WebSocket connection configuration -type OBSConfig struct { - Enabled bool `mapstructure:"enabled"` - Host string `mapstructure:"host"` - Port int `mapstructure:"port"` - Password string `mapstructure:"password"` - AutoReconnect bool `mapstructure:"auto-reconnect"` - ReconnectInterval time.Duration `mapstructure:"reconnect-interval"` - MaxReconnectInterval time.Duration `mapstructure:"max-reconnect-interval"` - Timeout time.Duration `mapstructure:"timeout"` -} - -// GatewayConfig holds local API gateway configuration -type GatewayConfig struct { - Enabled bool `mapstructure:"enabled"` - Host string `mapstructure:"host"` - Port int `mapstructure:"port"` - EnableAuth bool `mapstructure:"enable-auth"` - APIKey string `mapstructure:"api-key"` - RateLimitRPS int `mapstructure:"rate-limit-rps"` - EnableCORS bool `mapstructure:"enable-cors"` - AllowedOrigins []string `mapstructure:"allowed-origins"` - WSPingInterval int `mapstructure:"ws-ping-interval"` -} - -// ScriptingConfig holds scripting engine configuration -type ScriptingConfig struct { - Enabled bool `mapstructure:"enabled"` - EnableLua bool `mapstructure:"enable-lua"` - EnablePython bool `mapstructure:"enable-python"` - EnablePowerShell bool `mapstructure:"enable-powershell"` - EnableBash bool `mapstructure:"enable-bash"` - ScriptsDir string `mapstructure:"scripts-dir"` - DefaultTimeout int `mapstructure:"default-timeout"` - MaxMemoryMB int `mapstructure:"max-memory-mb"` - AllowNetwork bool `mapstructure:"allow-network"` - AllowFileSystem bool `mapstructure:"allow-filesystem"` - PythonPath string `mapstructure:"python-path"` - PowerShellPath string `mapstructure:"powershell-path"` - BashPath string `mapstructure:"bash-path"` -} - -// Load loads the configuration from various sources -func Load() (*Config, error) { - // Set defaults - setDefaults() - - // Create config instance - cfg := &Config{} - - // Unmarshal configuration - if err := viper.Unmarshal(cfg); err != nil { - return nil, fmt.Errorf("failed to unmarshal config: %w", err) - } - - // Set default data directory if not specified - if cfg.DataDir == "" { - homeDir, err := os.UserHomeDir() - if err != nil { - return nil, fmt.Errorf("failed to get user home directory: %w", err) - } - cfg.DataDir = filepath.Join(homeDir, ".waddlebot-bridge") - } - - // Set default modules directory - if cfg.ModulesDir == "" { - cfg.ModulesDir = filepath.Join(cfg.DataDir, "modules") - } - - // Set default scripts directory - if cfg.Scripting.ScriptsDir == "" { - cfg.Scripting.ScriptsDir = filepath.Join(cfg.DataDir, "scripts") - } - - // Ensure data directory exists - if err := os.MkdirAll(cfg.DataDir, 0755); err != nil { - return nil, fmt.Errorf("failed to create data directory: %w", err) - } - - // Ensure modules directory exists - if err := os.MkdirAll(cfg.ModulesDir, 0755); err != nil { - return nil, fmt.Errorf("failed to create modules directory: %w", err) - } - - // Ensure scripts directory exists if scripting is enabled - if cfg.Scripting.Enabled { - if err := os.MkdirAll(cfg.Scripting.ScriptsDir, 0755); err != nil { - return nil, fmt.Errorf("failed to create scripts directory: %w", err) - } - } - - // Set platform-specific defaults - setPlatformDefaults(cfg) - - return cfg, nil -} - -// setDefaults sets default configuration values -func setDefaults() { - viper.SetDefault("api-url", "https://api.waddlebot.io") - viper.SetDefault("poll-interval", 30) - viper.SetDefault("web-port", 8080) - viper.SetDefault("web-host", "127.0.0.1") - viper.SetDefault("log-level", "info") - viper.SetDefault("webauthn-display-name", "WaddleBot Bridge") - viper.SetDefault("webauthn-origin", "http://127.0.0.1:8080") - viper.SetDefault("webauthn-timeout", 60) - viper.SetDefault("module-timeout", 30) - viper.SetDefault("max-concurrent-tasks", 10) - - // OBS defaults - viper.SetDefault("obs.enabled", true) - viper.SetDefault("obs.host", "localhost") - viper.SetDefault("obs.port", 4455) - viper.SetDefault("obs.password", "") - viper.SetDefault("obs.auto-reconnect", true) - viper.SetDefault("obs.reconnect-interval", time.Second) - viper.SetDefault("obs.max-reconnect-interval", 30*time.Second) - viper.SetDefault("obs.timeout", 10*time.Second) - - // Gateway defaults - viper.SetDefault("gateway.enabled", true) - viper.SetDefault("gateway.host", "127.0.0.1") - viper.SetDefault("gateway.port", 8090) - viper.SetDefault("gateway.enable-auth", true) - viper.SetDefault("gateway.api-key", "") - viper.SetDefault("gateway.rate-limit-rps", 100) - viper.SetDefault("gateway.enable-cors", false) - viper.SetDefault("gateway.allowed-origins", []string{}) - viper.SetDefault("gateway.ws-ping-interval", 30) - - // Scripting defaults - viper.SetDefault("scripting.enabled", true) - viper.SetDefault("scripting.enable-lua", true) - viper.SetDefault("scripting.enable-python", true) - viper.SetDefault("scripting.enable-powershell", true) - viper.SetDefault("scripting.enable-bash", true) - viper.SetDefault("scripting.scripts-dir", "") - viper.SetDefault("scripting.default-timeout", 30) - viper.SetDefault("scripting.max-memory-mb", 256) - viper.SetDefault("scripting.allow-network", false) - viper.SetDefault("scripting.allow-filesystem", false) - viper.SetDefault("scripting.python-path", "python3") - viper.SetDefault("scripting.powershell-path", "pwsh") - viper.SetDefault("scripting.bash-path", "bash") -} - -// setPlatformDefaults sets platform-specific default values -func setPlatformDefaults(cfg *Config) { - switch runtime.GOOS { - case "darwin": - // macOS specific defaults - if cfg.WebAuthnDisplayName == "" { - cfg.WebAuthnDisplayName = "WaddleBot Bridge for macOS" - } - case "windows": - // Windows specific defaults - if cfg.WebAuthnDisplayName == "" { - cfg.WebAuthnDisplayName = "WaddleBot Bridge for Windows" - } - case "linux": - // Linux specific defaults - if cfg.WebAuthnDisplayName == "" { - cfg.WebAuthnDisplayName = "WaddleBot Bridge for Linux" - } - } -} - -// GetWebAuthnURL returns the WebAuthn origin URL -func (c *Config) GetWebAuthnURL() string { - return fmt.Sprintf("http://%s:%d", c.WebHost, c.WebPort) -} - -// GetAPIEndpoint returns a formatted API endpoint URL -func (c *Config) GetAPIEndpoint(path string) string { - return fmt.Sprintf("%s%s", c.APIURL, path) -} - -// GetUserAgent returns the user agent string for API requests -func (c *Config) GetUserAgent() string { - return fmt.Sprintf("WaddleBot-Bridge/1.0.0 (%s %s)", runtime.GOOS, runtime.GOARCH) -} \ No newline at end of file diff --git a/Premium/Desktop/internal/config/config_test.go b/Premium/Desktop/internal/config/config_test.go deleted file mode 100644 index 64f3c0021..000000000 --- a/Premium/Desktop/internal/config/config_test.go +++ /dev/null @@ -1,414 +0,0 @@ -package config - -import ( - "fmt" - "os" - "path/filepath" - "runtime" - "testing" - - "github.com/spf13/viper" -) - -func TestLoad(t *testing.T) { - // Reset viper for clean test - viper.Reset() - - // Create temporary directory for testing - tmpDir := t.TempDir() - - // Set test data directory - viper.Set("data-dir", tmpDir) - - cfg, err := Load() - if err != nil { - t.Fatalf("Load() failed: %v", err) - } - - if cfg == nil { - t.Fatal("Expected non-nil config") - } - - // Test default values - if cfg.APIURL != "https://api.waddlebot.io" { - t.Errorf("Expected default APIURL 'https://api.waddlebot.io', got %s", cfg.APIURL) - } - - if cfg.PollInterval != 30 { - t.Errorf("Expected default PollInterval 30, got %d", cfg.PollInterval) - } - - if cfg.WebPort != 8080 { - t.Errorf("Expected default WebPort 8080, got %d", cfg.WebPort) - } - - if cfg.WebHost != "127.0.0.1" { - t.Errorf("Expected default WebHost '127.0.0.1', got %s", cfg.WebHost) - } - - if cfg.LogLevel != "info" { - t.Errorf("Expected default LogLevel 'info', got %s", cfg.LogLevel) - } - - // Test data directory was set - if cfg.DataDir != tmpDir { - t.Errorf("Expected DataDir %s, got %s", tmpDir, cfg.DataDir) - } - - // Test modules directory was set - expectedModulesDir := filepath.Join(tmpDir, "modules") - if cfg.ModulesDir != expectedModulesDir { - t.Errorf("Expected ModulesDir %s, got %s", expectedModulesDir, cfg.ModulesDir) - } - - // Test directories were created - if _, err := os.Stat(cfg.DataDir); os.IsNotExist(err) { - t.Error("Data directory was not created") - } - - if _, err := os.Stat(cfg.ModulesDir); os.IsNotExist(err) { - t.Error("Modules directory was not created") - } -} - -func TestLoadWithCustomValues(t *testing.T) { - // Reset viper for clean test - viper.Reset() - - // Set custom values - viper.Set("api-url", "https://custom.api.com") - viper.Set("community-id", "test-community") - viper.Set("user-id", "test-user") - viper.Set("poll-interval", 60) - viper.Set("web-port", 9000) - viper.Set("web-host", "0.0.0.0") - viper.Set("log-level", "debug") - - cfg, err := Load() - if err != nil { - t.Fatalf("Load() failed: %v", err) - } - - // Test custom values - if cfg.APIURL != "https://custom.api.com" { - t.Errorf("Expected APIURL 'https://custom.api.com', got %s", cfg.APIURL) - } - - if cfg.CommunityID != "test-community" { - t.Errorf("Expected CommunityID 'test-community', got %s", cfg.CommunityID) - } - - if cfg.UserID != "test-user" { - t.Errorf("Expected UserID 'test-user', got %s", cfg.UserID) - } - - if cfg.PollInterval != 60 { - t.Errorf("Expected PollInterval 60, got %d", cfg.PollInterval) - } - - if cfg.WebPort != 9000 { - t.Errorf("Expected WebPort 9000, got %d", cfg.WebPort) - } - - if cfg.WebHost != "0.0.0.0" { - t.Errorf("Expected WebHost '0.0.0.0', got %s", cfg.WebHost) - } - - if cfg.LogLevel != "debug" { - t.Errorf("Expected LogLevel 'debug', got %s", cfg.LogLevel) - } -} - -func TestSetDefaults(t *testing.T) { - // Reset viper for clean test - viper.Reset() - - setDefaults() - - // Test all defaults are set - expectedDefaults := map[string]interface{}{ - "api-url": "https://api.waddlebot.io", - "poll-interval": 30, - "web-port": 8080, - "web-host": "127.0.0.1", - "log-level": "info", - "webauthn-display-name": "WaddleBot Bridge", - "webauthn-origin": "http://127.0.0.1:8080", - "webauthn-timeout": 60, - "module-timeout": 30, - "max-concurrent-tasks": 10, - } - - for key, expected := range expectedDefaults { - actual := viper.Get(key) - if actual != expected { - t.Errorf("Expected default %s to be %v, got %v", key, expected, actual) - } - } -} - -func TestSetPlatformDefaults(t *testing.T) { - tests := []struct { - name string - goos string - expectedDisplay string - }{ - { - name: "macOS", - goos: "darwin", - expectedDisplay: "WaddleBot Bridge for macOS", - }, - { - name: "Windows", - goos: "windows", - expectedDisplay: "WaddleBot Bridge for Windows", - }, - { - name: "Linux", - goos: "linux", - expectedDisplay: "WaddleBot Bridge for Linux", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - cfg := &Config{ - WebAuthnDisplayName: "", - } - - // We can't actually change runtime.GOOS, so we'll test the logic manually - switch tt.goos { - case "darwin": - cfg.WebAuthnDisplayName = "WaddleBot Bridge for macOS" - case "windows": - cfg.WebAuthnDisplayName = "WaddleBot Bridge for Windows" - case "linux": - cfg.WebAuthnDisplayName = "WaddleBot Bridge for Linux" - } - - if cfg.WebAuthnDisplayName != tt.expectedDisplay { - t.Errorf("Expected WebAuthnDisplayName %s, got %s", tt.expectedDisplay, cfg.WebAuthnDisplayName) - } - }) - } -} - -func TestConfig_GetWebAuthnURL(t *testing.T) { - cfg := &Config{ - WebHost: "127.0.0.1", - WebPort: 8080, - } - - expected := "http://127.0.0.1:8080" - actual := cfg.GetWebAuthnURL() - - if actual != expected { - t.Errorf("Expected WebAuthnURL %s, got %s", expected, actual) - } -} - -func TestConfig_GetAPIEndpoint(t *testing.T) { - cfg := &Config{ - APIURL: "https://api.waddlebot.io", - } - - tests := []struct { - path string - expected string - }{ - { - path: "/api/bridge/poll", - expected: "https://api.waddlebot.io/api/bridge/poll", - }, - { - path: "/health", - expected: "https://api.waddlebot.io/health", - }, - { - path: "", - expected: "https://api.waddlebot.io", - }, - } - - for _, tt := range tests { - t.Run(tt.path, func(t *testing.T) { - actual := cfg.GetAPIEndpoint(tt.path) - if actual != tt.expected { - t.Errorf("Expected API endpoint %s, got %s", tt.expected, actual) - } - }) - } -} - -func TestConfig_GetUserAgent(t *testing.T) { - cfg := &Config{} - - userAgent := cfg.GetUserAgent() - - expectedPrefix := "WaddleBot-Bridge/1.0.0" - if len(userAgent) < len(expectedPrefix) { - t.Errorf("Expected user agent to start with %s, got %s", expectedPrefix, userAgent) - } - - if userAgent[:len(expectedPrefix)] != expectedPrefix { - t.Errorf("Expected user agent to start with %s, got %s", expectedPrefix, userAgent) - } - - // Should contain OS and architecture - if !contains(userAgent, runtime.GOOS) { - t.Errorf("Expected user agent to contain OS %s, got %s", runtime.GOOS, userAgent) - } - - if !contains(userAgent, runtime.GOARCH) { - t.Errorf("Expected user agent to contain arch %s, got %s", runtime.GOARCH, userAgent) - } -} - -func TestLoadWithInvalidDataDir(t *testing.T) { - // Reset viper for clean test - viper.Reset() - - // Set invalid data directory (file instead of directory) - tmpFile := filepath.Join(t.TempDir(), "invalid-file") - if err := os.WriteFile(tmpFile, []byte("test"), 0644); err != nil { - t.Fatalf("Failed to create test file: %v", err) - } - - viper.Set("data-dir", tmpFile) - - _, err := Load() - if err == nil { - t.Error("Expected error for invalid data directory, got none") - } -} - -func TestLoadWithValidDataDir(t *testing.T) { - // Reset viper for clean test - viper.Reset() - - tmpDir := t.TempDir() - viper.Set("data-dir", tmpDir) - - cfg, err := Load() - if err != nil { - t.Fatalf("Load() failed: %v", err) - } - - if cfg.DataDir != tmpDir { - t.Errorf("Expected DataDir %s, got %s", tmpDir, cfg.DataDir) - } -} - -func TestLoadWithHomeDir(t *testing.T) { - // Reset viper for clean test - viper.Reset() - - // Don't set data-dir, should use home directory - cfg, err := Load() - if err != nil { - t.Fatalf("Load() failed: %v", err) - } - - // Should contain .waddlebot-bridge in the path - if !contains(cfg.DataDir, ".waddlebot-bridge") { - t.Errorf("Expected DataDir to contain '.waddlebot-bridge', got %s", cfg.DataDir) - } -} - -func TestValidateConfig(t *testing.T) { - tests := []struct { - name string - cfg *Config - expectErr bool - }{ - { - name: "valid config", - cfg: &Config{ - APIURL: "https://api.waddlebot.io", - CommunityID: "test-community", - UserID: "test-user", - PollInterval: 30, - WebPort: 8080, - WebHost: "127.0.0.1", - DataDir: "/tmp/test", - ModulesDir: "/tmp/test/modules", - }, - expectErr: false, - }, - { - name: "invalid poll interval", - cfg: &Config{ - APIURL: "https://api.waddlebot.io", - CommunityID: "test-community", - UserID: "test-user", - PollInterval: 3, - WebPort: 8080, - WebHost: "127.0.0.1", - DataDir: "/tmp/test", - ModulesDir: "/tmp/test/modules", - }, - expectErr: true, - }, - { - name: "invalid web port", - cfg: &Config{ - APIURL: "https://api.waddlebot.io", - CommunityID: "test-community", - UserID: "test-user", - PollInterval: 30, - WebPort: 0, - WebHost: "127.0.0.1", - DataDir: "/tmp/test", - ModulesDir: "/tmp/test/modules", - }, - expectErr: true, - }, - { - name: "empty API URL", - cfg: &Config{ - APIURL: "", - CommunityID: "test-community", - UserID: "test-user", - PollInterval: 30, - WebPort: 8080, - WebHost: "127.0.0.1", - DataDir: "/tmp/test", - ModulesDir: "/tmp/test/modules", - }, - expectErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - err := validateConfig(tt.cfg) - if tt.expectErr && err == nil { - t.Error("Expected error but got none") - } - if !tt.expectErr && err != nil { - t.Errorf("Expected no error but got: %v", err) - } - }) - } -} - -// Helper function to check if a string contains a substring -func contains(s, substr string) bool { - return len(s) >= len(substr) && s[len(s)-len(substr):] == substr || - len(s) >= len(substr) && s[:len(substr)] == substr || - len(s) > len(substr) && s[len(s)/2-len(substr)/2:len(s)/2+len(substr)/2] == substr -} - -// Helper function to validate config -func validateConfig(cfg *Config) error { - if cfg.APIURL == "" { - return fmt.Errorf("APIURL cannot be empty") - } - if cfg.PollInterval < 5 { - return fmt.Errorf("PollInterval must be at least 5 seconds") - } - if cfg.WebPort <= 0 || cfg.WebPort > 65535 { - return fmt.Errorf("WebPort must be between 1 and 65535") - } - return nil -} diff --git a/Premium/Desktop/internal/gateway/gateway.go b/Premium/Desktop/internal/gateway/gateway.go deleted file mode 100644 index c2d4f2df2..000000000 --- a/Premium/Desktop/internal/gateway/gateway.go +++ /dev/null @@ -1,175 +0,0 @@ -package gateway - -import ( - "context" - "fmt" - "net/http" - "sync" - "time" - - "github.com/gorilla/mux" - "github.com/sirupsen/logrus" - "golang.org/x/time/rate" - - "waddlebot-bridge/internal/config" - "waddlebot-bridge/internal/obs" -) - -// Gateway represents the local API gateway server -type Gateway struct { - config config.GatewayConfig - server *http.Server - router *mux.Router - obsClient *obs.Client - logger *logrus.Logger - rateLimiters map[string]*rate.Limiter - limiterMux sync.RWMutex - wsHub *WebSocketHub - running bool - runningMux sync.RWMutex -} - -// New creates a new Gateway instance -func New(cfg config.GatewayConfig, obsClient *obs.Client, logger *logrus.Logger) *Gateway { - g := &Gateway{ - config: cfg, - obsClient: obsClient, - logger: logger, - rateLimiters: make(map[string]*rate.Limiter), - wsHub: NewWebSocketHub(logger), - } - - g.setupRouter() - return g -} - -// setupRouter initializes the HTTP router with middleware and routes -func (g *Gateway) setupRouter() { - g.router = mux.NewRouter() - - // Apply global middleware - g.router.Use(g.loggingMiddleware) - if g.config.EnableAuth { - g.router.Use(g.authMiddleware) - } - g.router.Use(g.rateLimitMiddleware) - if g.config.EnableCORS { - g.router.Use(g.corsMiddleware) - } - - // Register all routes - RegisterRoutes(g) -} - -// Start starts the gateway server -func (g *Gateway) Start(ctx context.Context) error { - g.runningMux.Lock() - if g.running { - g.runningMux.Unlock() - return fmt.Errorf("gateway already running") - } - g.running = true - g.runningMux.Unlock() - - // Start WebSocket hub - go g.wsHub.Run() - - // Create HTTP server - addr := fmt.Sprintf("%s:%d", g.config.Host, g.config.Port) - g.server = &http.Server{ - Addr: addr, - Handler: g.router, - ReadTimeout: 15 * time.Second, - WriteTimeout: 15 * time.Second, - IdleTimeout: 60 * time.Second, - } - - g.logger.WithFields(logrus.Fields{ - "host": g.config.Host, - "port": g.config.Port, - "auth": g.config.EnableAuth, - "cors": g.config.EnableCORS, - }).Info("Starting local API gateway") - - // Start server in goroutine - errChan := make(chan error, 1) - go func() { - if err := g.server.ListenAndServe(); err != nil && err != http.ErrServerClosed { - errChan <- err - } - }() - - // Wait for context cancellation or error - select { - case <-ctx.Done(): - return g.Stop() - case err := <-errChan: - g.runningMux.Lock() - g.running = false - g.runningMux.Unlock() - return err - } -} - -// Stop gracefully stops the gateway server -func (g *Gateway) Stop() error { - g.runningMux.Lock() - if !g.running { - g.runningMux.Unlock() - return nil - } - g.running = false - g.runningMux.Unlock() - - g.logger.Info("Stopping local API gateway") - - // Stop WebSocket hub - g.wsHub.Stop() - - // Shutdown HTTP server with timeout - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - - if err := g.server.Shutdown(ctx); err != nil { - g.logger.WithError(err).Error("Error shutting down gateway server") - return err - } - - g.logger.Info("Gateway stopped successfully") - return nil -} - -// IsRunning returns whether the gateway is currently running -func (g *Gateway) IsRunning() bool { - g.runningMux.RLock() - defer g.runningMux.RUnlock() - return g.running -} - -// GetRouter returns the HTTP router -func (g *Gateway) GetRouter() *mux.Router { - return g.router -} - -// GetOBSClient returns the OBS client -func (g *Gateway) GetOBSClient() *obs.Client { - return g.obsClient -} - -// GetLogger returns the logger -func (g *Gateway) GetLogger() *logrus.Logger { - return g.logger -} - -// GetWebSocketHub returns the WebSocket hub -func (g *Gateway) GetWebSocketHub() *WebSocketHub { - return g.wsHub -} - -// BroadcastEvent sends an event to all WebSocket clients -func (g *Gateway) BroadcastEvent(eventType string, data interface{}) { - g.wsHub.Broadcast(WSMessage{ - Type: eventType, - Data: data, - }) -} diff --git a/Premium/Desktop/internal/gateway/handlers/bridge.go b/Premium/Desktop/internal/gateway/handlers/bridge.go deleted file mode 100644 index 7f6fb181d..000000000 --- a/Premium/Desktop/internal/gateway/handlers/bridge.go +++ /dev/null @@ -1,85 +0,0 @@ -package handlers - -import ( - "encoding/json" - "net/http" - "time" - - "github.com/sirupsen/logrus" -) - -// BridgeHandler handles bridge-related endpoints -type BridgeHandler struct { - logger *logrus.Logger -} - -// NewBridgeHandler creates a new bridge handler -func NewBridgeHandler(logger *logrus.Logger) *BridgeHandler { - return &BridgeHandler{ - logger: logger, - } -} - -// BridgeStatus represents bridge status information -type BridgeStatus struct { - Status string `json:"status"` - Version string `json:"version"` - Uptime int64 `json:"uptime"` - Connected bool `json:"connected"` -} - -// GetStatus returns the current bridge status -func (h *BridgeHandler) GetStatus(w http.ResponseWriter, r *http.Request) { - status := BridgeStatus{ - Status: "running", - Version: "1.0.0", // TODO: Get from config - Uptime: int64(time.Since(time.Now()).Seconds()), - Connected: true, // TODO: Get from bridge client - } - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(status) -} - -// HealthResponse represents a health check response -type HealthResponse struct { - Healthy bool `json:"healthy"` - Timestamp int64 `json:"timestamp"` - Services map[string]string `json:"services"` -} - -// GetHealth returns health check information -func (h *BridgeHandler) GetHealth(w http.ResponseWriter, r *http.Request) { - health := HealthResponse{ - Healthy: true, - Timestamp: time.Now().Unix(), - Services: map[string]string{ - "gateway": "ok", - "bridge": "ok", - }, - } - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(health) -} - -// ReconnectResponse represents a reconnection response -type ReconnectResponse struct { - Success bool `json:"success"` - Message string `json:"message"` -} - -// Reconnect forces a bridge reconnection -func (h *BridgeHandler) Reconnect(w http.ResponseWriter, r *http.Request) { - // TODO: Implement bridge reconnection logic - - response := ReconnectResponse{ - Success: true, - Message: "Reconnection initiated", - } - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(response) - - h.logger.Info("Bridge reconnection requested") -} diff --git a/Premium/Desktop/internal/gateway/handlers/obs.go b/Premium/Desktop/internal/gateway/handlers/obs.go deleted file mode 100644 index 24c2e7762..000000000 --- a/Premium/Desktop/internal/gateway/handlers/obs.go +++ /dev/null @@ -1,427 +0,0 @@ -package handlers - -import ( - "context" - "encoding/json" - "net/http" - - "github.com/gorilla/mux" - "github.com/sirupsen/logrus" - - "waddlebot-bridge/internal/obs" -) - -// OBSHandler handles OBS-related endpoints -type OBSHandler struct { - obsClient *obs.Client - logger *logrus.Logger -} - -// NewOBSHandler creates a new OBS handler -func NewOBSHandler(obsClient *obs.Client, logger *logrus.Logger) *OBSHandler { - return &OBSHandler{ - obsClient: obsClient, - logger: logger, - } -} - -// ErrorResponse represents an error response -type ErrorResponse struct { - Error string `json:"error"` -} - -// SuccessResponse represents a success response -type SuccessResponse struct { - Success bool `json:"success"` - Message string `json:"message,omitempty"` -} - -// GetStatus returns OBS connection status -func (h *OBSHandler) GetStatus(w http.ResponseWriter, r *http.Request) { - status := map[string]interface{}{ - "connected": h.obsClient.IsConnected(), - "state": h.obsClient.GetState().String(), - } - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(status) -} - -// Connect connects to OBS -func (h *OBSHandler) Connect(w http.ResponseWriter, r *http.Request) { - if err := h.obsClient.Connect(context.Background()); err != nil { - h.sendError(w, err.Error(), http.StatusInternalServerError) - return - } - - h.sendSuccess(w, "Connected to OBS") -} - -// Disconnect disconnects from OBS -func (h *OBSHandler) Disconnect(w http.ResponseWriter, r *http.Request) { - if err := h.obsClient.Disconnect(); err != nil { - h.sendError(w, err.Error(), http.StatusInternalServerError) - return - } - - h.sendSuccess(w, "Disconnected from OBS") -} - -// GetScenes returns all scenes -func (h *OBSHandler) GetScenes(w http.ResponseWriter, r *http.Request) { - scenes, err := h.obsClient.GetScenes(context.Background()) - if err != nil { - h.sendError(w, err.Error(), http.StatusInternalServerError) - return - } - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]interface{}{ - "scenes": scenes, - }) -} - -// GetCurrentScene returns the current scene -func (h *OBSHandler) GetCurrentScene(w http.ResponseWriter, r *http.Request) { - scene, err := h.obsClient.GetCurrentScene(context.Background()) - if err != nil { - h.sendError(w, err.Error(), http.StatusInternalServerError) - return - } - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(scene) -} - -// SwitchSceneRequest represents a scene switch request -type SwitchSceneRequest struct { - SceneName string `json:"scene_name"` -} - -// SwitchScene switches to a different scene -func (h *OBSHandler) SwitchScene(w http.ResponseWriter, r *http.Request) { - var req SwitchSceneRequest - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - h.sendError(w, "Invalid request body", http.StatusBadRequest) - return - } - - if req.SceneName == "" { - h.sendError(w, "scene_name is required", http.StatusBadRequest) - return - } - - if err := h.obsClient.SetCurrentScene(context.Background(), req.SceneName); err != nil { - h.sendError(w, err.Error(), http.StatusInternalServerError) - return - } - - h.sendSuccess(w, "Scene switched to "+req.SceneName) -} - -// GetSceneSources returns sources in a scene -func (h *OBSHandler) GetSceneSources(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - sceneName := vars["name"] - - sources, err := h.obsClient.GetSceneSources(context.Background(), sceneName) - if err != nil { - h.sendError(w, err.Error(), http.StatusInternalServerError) - return - } - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]interface{}{ - "sources": sources, - }) -} - -// SetSourceVisibilityRequest represents a source visibility request -type SetSourceVisibilityRequest struct { - SceneName string `json:"scene_name"` - Visible bool `json:"visible"` -} - -// SetSourceVisibility sets source visibility -func (h *OBSHandler) SetSourceVisibility(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - sourceName := vars["name"] - - var req SetSourceVisibilityRequest - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - h.sendError(w, "Invalid request body", http.StatusBadRequest) - return - } - - if req.SceneName == "" { - h.sendError(w, "scene_name is required", http.StatusBadRequest) - return - } - - if err := h.obsClient.SetSourceVisibility(context.Background(), req.SceneName, sourceName, req.Visible); err != nil { - h.sendError(w, err.Error(), http.StatusInternalServerError) - return - } - - h.sendSuccess(w, "Source visibility updated") -} - -// SetSourceTransformRequest represents a source transform request -type SetSourceTransformRequest struct { - SceneName string `json:"scene_name"` - X float64 `json:"x,omitempty"` - Y float64 `json:"y,omitempty"` - ScaleX float64 `json:"scale_x,omitempty"` - ScaleY float64 `json:"scale_y,omitempty"` - Rotation float64 `json:"rotation,omitempty"` -} - -// SetSourceTransform sets source transform -func (h *OBSHandler) SetSourceTransform(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - sourceName := vars["name"] - - var req SetSourceTransformRequest - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - h.sendError(w, "Invalid request body", http.StatusBadRequest) - return - } - - if req.SceneName == "" { - h.sendError(w, "scene_name is required", http.StatusBadRequest) - return - } - - // Build transform - transform := obs.SourceTransform{} - if req.X != 0 || req.Y != 0 { - transform.PositionX = &req.X - transform.PositionY = &req.Y - } - if req.ScaleX != 0 { - transform.ScaleX = &req.ScaleX - } - if req.ScaleY != 0 { - transform.ScaleY = &req.ScaleY - } - if req.Rotation != 0 { - transform.Rotation = &req.Rotation - } - - if err := h.obsClient.SetSourceTransform(context.Background(), req.SceneName, sourceName, transform); err != nil { - h.sendError(w, err.Error(), http.StatusInternalServerError) - return - } - - h.sendSuccess(w, "Source transform updated") -} - -// GetSourceFilters returns filters for a source -func (h *OBSHandler) GetSourceFilters(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - sourceName := vars["name"] - - filters, err := h.obsClient.GetSourceFilters(context.Background(), sourceName) - if err != nil { - h.sendError(w, err.Error(), http.StatusInternalServerError) - return - } - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]interface{}{ - "filters": filters, - }) -} - -// UpdateFilterRequest represents a filter update request -type UpdateFilterRequest struct { - Enabled *bool `json:"enabled,omitempty"` - Settings map[string]interface{} `json:"settings,omitempty"` -} - -// UpdateFilter updates a filter -func (h *OBSHandler) UpdateFilter(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - sourceName := vars["source"] - filterName := vars["filter"] - - var req UpdateFilterRequest - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - h.sendError(w, "Invalid request body", http.StatusBadRequest) - return - } - - // Update enabled state if provided - if req.Enabled != nil { - if err := h.obsClient.SetFilterEnabled(context.Background(), sourceName, filterName, *req.Enabled); err != nil { - h.sendError(w, err.Error(), http.StatusInternalServerError) - return - } - } - - // Update settings if provided - if req.Settings != nil { - if err := h.obsClient.SetFilterSettings(context.Background(), sourceName, filterName, req.Settings); err != nil { - h.sendError(w, err.Error(), http.StatusInternalServerError) - return - } - } - - h.sendSuccess(w, "Filter updated") -} - -// GetStreamStatus returns stream status -func (h *OBSHandler) GetStreamStatus(w http.ResponseWriter, r *http.Request) { - status, err := h.obsClient.GetStreamStatus(context.Background()) - if err != nil { - h.sendError(w, err.Error(), http.StatusInternalServerError) - return - } - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(status) -} - -// StartStream starts streaming -func (h *OBSHandler) StartStream(w http.ResponseWriter, r *http.Request) { - if err := h.obsClient.StartStream(context.Background()); err != nil { - h.sendError(w, err.Error(), http.StatusInternalServerError) - return - } - - h.sendSuccess(w, "Stream started") -} - -// StopStream stops streaming -func (h *OBSHandler) StopStream(w http.ResponseWriter, r *http.Request) { - if err := h.obsClient.StopStream(context.Background()); err != nil { - h.sendError(w, err.Error(), http.StatusInternalServerError) - return - } - - h.sendSuccess(w, "Stream stopped") -} - -// ToggleStream toggles streaming -func (h *OBSHandler) ToggleStream(w http.ResponseWriter, r *http.Request) { - active, err := h.obsClient.ToggleStream(context.Background()) - if err != nil { - h.sendError(w, err.Error(), http.StatusInternalServerError) - return - } - - message := "Stream started" - if !active { - message = "Stream stopped" - } - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]interface{}{ - "success": true, - "active": active, - "message": message, - }) -} - -// GetRecordingStatus returns recording status -func (h *OBSHandler) GetRecordingStatus(w http.ResponseWriter, r *http.Request) { - status, err := h.obsClient.GetRecordingStatus(context.Background()) - if err != nil { - h.sendError(w, err.Error(), http.StatusInternalServerError) - return - } - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(status) -} - -// StartRecording starts recording -func (h *OBSHandler) StartRecording(w http.ResponseWriter, r *http.Request) { - if err := h.obsClient.StartRecording(context.Background()); err != nil { - h.sendError(w, err.Error(), http.StatusInternalServerError) - return - } - - h.sendSuccess(w, "Recording started") -} - -// StopRecording stops recording -func (h *OBSHandler) StopRecording(w http.ResponseWriter, r *http.Request) { - outputPath, err := h.obsClient.StopRecording(context.Background()) - if err != nil { - h.sendError(w, err.Error(), http.StatusInternalServerError) - return - } - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]interface{}{ - "success": true, - "message": "Recording stopped", - "output_path": outputPath, - }) -} - -// PauseRecording pauses recording -func (h *OBSHandler) PauseRecording(w http.ResponseWriter, r *http.Request) { - if err := h.obsClient.PauseRecording(context.Background()); err != nil { - h.sendError(w, err.Error(), http.StatusInternalServerError) - return - } - - h.sendSuccess(w, "Recording paused") -} - -// ResumeRecording resumes recording -func (h *OBSHandler) ResumeRecording(w http.ResponseWriter, r *http.Request) { - if err := h.obsClient.ResumeRecording(context.Background()); err != nil { - h.sendError(w, err.Error(), http.StatusInternalServerError) - return - } - - h.sendSuccess(w, "Recording resumed") -} - -// ToggleRecording toggles recording -func (h *OBSHandler) ToggleRecording(w http.ResponseWriter, r *http.Request) { - // Get current status first - status, err := h.obsClient.GetRecordingStatus(context.Background()) - if err != nil { - h.sendError(w, err.Error(), http.StatusInternalServerError) - return - } - - wasActive := status.Active - - // Toggle recording - if err := h.obsClient.ToggleRecording(context.Background()); err != nil { - h.sendError(w, err.Error(), http.StatusInternalServerError) - return - } - - message := "Recording started" - nowActive := !wasActive - if !nowActive { - message = "Recording stopped" - } - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]interface{}{ - "success": true, - "active": nowActive, - "message": message, - }) -} - -// Helper methods - -func (h *OBSHandler) sendError(w http.ResponseWriter, message string, statusCode int) { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(statusCode) - json.NewEncoder(w).Encode(ErrorResponse{Error: message}) - h.logger.WithField("error", message).Warn("OBS API error") -} - -func (h *OBSHandler) sendSuccess(w http.ResponseWriter, message string) { - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(SuccessResponse{Success: true, Message: message}) -} diff --git a/Premium/Desktop/internal/gateway/handlers/webhooks.go b/Premium/Desktop/internal/gateway/handlers/webhooks.go deleted file mode 100644 index bab21e7c4..000000000 --- a/Premium/Desktop/internal/gateway/handlers/webhooks.go +++ /dev/null @@ -1,165 +0,0 @@ -package handlers - -import ( - "encoding/json" - "net/http" - "sync" - - "github.com/gorilla/mux" - "github.com/sirupsen/logrus" -) - -// WebhookHandler handles webhook-related endpoints -type WebhookHandler struct { - logger *logrus.Logger - webhooks map[string]*Webhook - mu sync.RWMutex -} - -// Webhook represents a registered webhook -type Webhook struct { - ID string `json:"id"` - URL string `json:"url"` - Events []string `json:"events"` - Secret string `json:"secret,omitempty"` -} - -// NewWebhookHandler creates a new webhook handler -func NewWebhookHandler(logger *logrus.Logger) *WebhookHandler { - return &WebhookHandler{ - logger: logger, - webhooks: make(map[string]*Webhook), - } -} - -// ListWebhooks returns all registered webhooks -func (h *WebhookHandler) ListWebhooks(w http.ResponseWriter, r *http.Request) { - h.mu.RLock() - defer h.mu.RUnlock() - - webhooks := make([]*Webhook, 0, len(h.webhooks)) - for _, wh := range h.webhooks { - // Don't expose secrets - wh.Secret = "" - webhooks = append(webhooks, wh) - } - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]interface{}{ - "webhooks": webhooks, - }) -} - -// RegisterWebhookRequest represents a webhook registration request -type RegisterWebhookRequest struct { - URL string `json:"url"` - Events []string `json:"events"` - Secret string `json:"secret,omitempty"` -} - -// RegisterWebhook registers a new webhook -func (h *WebhookHandler) RegisterWebhook(w http.ResponseWriter, r *http.Request) { - var req RegisterWebhookRequest - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - h.sendError(w, "Invalid request body", http.StatusBadRequest) - return - } - - if req.URL == "" { - h.sendError(w, "url is required", http.StatusBadRequest) - return - } - - if len(req.Events) == 0 { - h.sendError(w, "at least one event is required", http.StatusBadRequest) - return - } - - // Generate ID (simple implementation) - h.mu.Lock() - id := generateID() - webhook := &Webhook{ - ID: id, - URL: req.URL, - Events: req.Events, - Secret: req.Secret, - } - h.webhooks[id] = webhook - h.mu.Unlock() - - h.logger.WithFields(logrus.Fields{ - "id": id, - "url": req.URL, - "events": req.Events, - }).Info("Webhook registered") - - // Don't return secret in response - webhook.Secret = "" - - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusCreated) - json.NewEncoder(w).Encode(webhook) -} - -// RemoveWebhook removes a registered webhook -func (h *WebhookHandler) RemoveWebhook(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - id := vars["id"] - - h.mu.Lock() - _, exists := h.webhooks[id] - if !exists { - h.mu.Unlock() - h.sendError(w, "webhook not found", http.StatusNotFound) - return - } - - delete(h.webhooks, id) - h.mu.Unlock() - - h.logger.WithField("id", id).Info("Webhook removed") - - h.sendSuccess(w, "Webhook removed") -} - -// TestWebhook tests a webhook delivery -func (h *WebhookHandler) TestWebhook(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - id := vars["id"] - - h.mu.RLock() - webhook, exists := h.webhooks[id] - h.mu.RUnlock() - - if !exists { - h.sendError(w, "webhook not found", http.StatusNotFound) - return - } - - // TODO: Implement actual webhook delivery test - h.logger.WithFields(logrus.Fields{ - "id": id, - "url": webhook.URL, - }).Info("Testing webhook delivery") - - h.sendSuccess(w, "Test webhook sent to "+webhook.URL) -} - -// Helper methods - -func (h *WebhookHandler) sendError(w http.ResponseWriter, message string, statusCode int) { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(statusCode) - json.NewEncoder(w).Encode(ErrorResponse{Error: message}) -} - -func (h *WebhookHandler) sendSuccess(w http.ResponseWriter, message string) { - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(SuccessResponse{Success: true, Message: message}) -} - -// generateID generates a simple webhook ID -func generateID() string { - // TODO: Use proper UUID generation - return "webhook_" + string(rune(len("temp"))) -} diff --git a/Premium/Desktop/internal/gateway/middleware.go b/Premium/Desktop/internal/gateway/middleware.go deleted file mode 100644 index 3989b6162..000000000 --- a/Premium/Desktop/internal/gateway/middleware.go +++ /dev/null @@ -1,194 +0,0 @@ -package gateway - -import ( - "net/http" - "strings" - "time" - - "github.com/sirupsen/logrus" - "golang.org/x/time/rate" -) - -// loggingMiddleware logs all HTTP requests -func (g *Gateway) loggingMiddleware(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - start := time.Now() - - // Create response writer wrapper to capture status code - rw := &responseWriter{ResponseWriter: w, statusCode: http.StatusOK} - - next.ServeHTTP(rw, r) - - duration := time.Since(start) - g.logger.WithFields(logrus.Fields{ - "method": r.Method, - "path": r.URL.Path, - "remote_addr": r.RemoteAddr, - "status": rw.statusCode, - "duration_ms": duration.Milliseconds(), - }).Info("HTTP request") - }) -} - -// responseWriter wraps http.ResponseWriter to capture status code -type responseWriter struct { - http.ResponseWriter - statusCode int -} - -func (rw *responseWriter) WriteHeader(code int) { - rw.statusCode = code - rw.ResponseWriter.WriteHeader(code) -} - -// authMiddleware validates API key authentication -func (g *Gateway) authMiddleware(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // Skip auth for health check - if r.URL.Path == "/health" { - next.ServeHTTP(w, r) - return - } - - // Get API key from header - apiKey := r.Header.Get("X-API-Key") - if apiKey == "" { - // Try query parameter as fallback - apiKey = r.URL.Query().Get("api_key") - } - - // Validate API key - if apiKey != g.config.APIKey { - g.logger.WithFields(logrus.Fields{ - "path": r.URL.Path, - "remote_addr": r.RemoteAddr, - }).Warn("Unauthorized access attempt") - - http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized) - return - } - - next.ServeHTTP(w, r) - }) -} - -// rateLimitMiddleware implements per-IP rate limiting -func (g *Gateway) rateLimitMiddleware(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // Skip rate limiting for health check - if r.URL.Path == "/health" { - next.ServeHTTP(w, r) - return - } - - // Get client IP - ip := getClientIP(r) - - // Get or create rate limiter for this IP - limiter := g.getRateLimiter(ip) - - // Check if request is allowed - if !limiter.Allow() { - g.logger.WithFields(logrus.Fields{ - "ip": ip, - "path": r.URL.Path, - }).Warn("Rate limit exceeded") - - w.Header().Set("Retry-After", "1") - http.Error(w, `{"error":"rate limit exceeded"}`, http.StatusTooManyRequests) - return - } - - next.ServeHTTP(w, r) - }) -} - -// getRateLimiter gets or creates a rate limiter for an IP -func (g *Gateway) getRateLimiter(ip string) *rate.Limiter { - g.limiterMux.RLock() - limiter, exists := g.rateLimiters[ip] - g.limiterMux.RUnlock() - - if exists { - return limiter - } - - // Create new limiter (requests per second, burst) - g.limiterMux.Lock() - defer g.limiterMux.Unlock() - - // Double-check after acquiring write lock - if limiter, exists := g.rateLimiters[ip]; exists { - return limiter - } - - // Create limiter with configured RPS and burst of 2x - limiter = rate.NewLimiter(rate.Limit(g.config.RateLimitRPS), g.config.RateLimitRPS*2) - g.rateLimiters[ip] = limiter - - return limiter -} - -// corsMiddleware adds CORS headers -func (g *Gateway) corsMiddleware(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - origin := r.Header.Get("Origin") - - // Check if origin is allowed - if origin != "" && g.isOriginAllowed(origin) { - w.Header().Set("Access-Control-Allow-Origin", origin) - w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") - w.Header().Set("Access-Control-Allow-Headers", "Content-Type, X-API-Key") - w.Header().Set("Access-Control-Max-Age", "86400") - } - - // Handle preflight requests - if r.Method == "OPTIONS" { - w.WriteHeader(http.StatusNoContent) - return - } - - next.ServeHTTP(w, r) - }) -} - -// isOriginAllowed checks if an origin is in the allowed list -func (g *Gateway) isOriginAllowed(origin string) bool { - // If no origins configured, allow all - if len(g.config.AllowedOrigins) == 0 { - return true - } - - // Check if origin is in allowed list - for _, allowed := range g.config.AllowedOrigins { - if allowed == "*" || allowed == origin { - return true - } - } - - return false -} - -// getClientIP extracts the client IP from the request -func getClientIP(r *http.Request) string { - // Try X-Forwarded-For header - xff := r.Header.Get("X-Forwarded-For") - if xff != "" { - ips := strings.Split(xff, ",") - return strings.TrimSpace(ips[0]) - } - - // Try X-Real-IP header - xri := r.Header.Get("X-Real-IP") - if xri != "" { - return xri - } - - // Fall back to RemoteAddr - ip := r.RemoteAddr - if colon := strings.LastIndex(ip, ":"); colon != -1 { - ip = ip[:colon] - } - - return ip -} diff --git a/Premium/Desktop/internal/gateway/router.go b/Premium/Desktop/internal/gateway/router.go deleted file mode 100644 index 5153c1923..000000000 --- a/Premium/Desktop/internal/gateway/router.go +++ /dev/null @@ -1,78 +0,0 @@ -package gateway - -import ( - "net/http" - - "waddlebot-bridge/internal/gateway/handlers" -) - -// RegisterRoutes registers all API routes with the gateway -func RegisterRoutes(g *Gateway) { - // Create handler instances - bridgeHandler := handlers.NewBridgeHandler(g.logger) - obsHandler := handlers.NewOBSHandler(g.obsClient, g.logger) - webhookHandler := handlers.NewWebhookHandler(g.logger) - - // Health check (no auth required) - g.router.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - w.Write([]byte(`{"status":"ok"}`)) - }).Methods("GET") - - // API v1 routes - api := g.router.PathPrefix("/api/v1").Subrouter() - - // Bridge endpoints - bridge := api.PathPrefix("/bridge").Subrouter() - bridge.HandleFunc("/status", bridgeHandler.GetStatus).Methods("GET") - bridge.HandleFunc("/health", bridgeHandler.GetHealth).Methods("GET") - bridge.HandleFunc("/reconnect", bridgeHandler.Reconnect).Methods("POST") - - // OBS Control endpoints - obs := api.PathPrefix("/obs").Subrouter() - - // OBS Connection - obs.HandleFunc("/status", obsHandler.GetStatus).Methods("GET") - obs.HandleFunc("/connect", obsHandler.Connect).Methods("POST") - obs.HandleFunc("/disconnect", obsHandler.Disconnect).Methods("POST") - - // OBS Scenes - obs.HandleFunc("/scenes", obsHandler.GetScenes).Methods("GET") - obs.HandleFunc("/scenes/current", obsHandler.GetCurrentScene).Methods("GET") - obs.HandleFunc("/scenes/switch", obsHandler.SwitchScene).Methods("POST") - obs.HandleFunc("/scenes/{name}/sources", obsHandler.GetSceneSources).Methods("GET") - - // OBS Sources - obs.HandleFunc("/sources/{name}/visibility", obsHandler.SetSourceVisibility).Methods("PUT") - obs.HandleFunc("/sources/{name}/transform", obsHandler.SetSourceTransform).Methods("PUT") - obs.HandleFunc("/sources/{name}/filters", obsHandler.GetSourceFilters).Methods("GET") - - // OBS Filters - obs.HandleFunc("/filters/{source}/{filter}", obsHandler.UpdateFilter).Methods("PUT") - - // OBS Streaming - obs.HandleFunc("/stream/status", obsHandler.GetStreamStatus).Methods("GET") - obs.HandleFunc("/stream/start", obsHandler.StartStream).Methods("POST") - obs.HandleFunc("/stream/stop", obsHandler.StopStream).Methods("POST") - obs.HandleFunc("/stream/toggle", obsHandler.ToggleStream).Methods("POST") - - // OBS Recording - obs.HandleFunc("/recording/status", obsHandler.GetRecordingStatus).Methods("GET") - obs.HandleFunc("/recording/start", obsHandler.StartRecording).Methods("POST") - obs.HandleFunc("/recording/stop", obsHandler.StopRecording).Methods("POST") - obs.HandleFunc("/recording/pause", obsHandler.PauseRecording).Methods("POST") - obs.HandleFunc("/recording/resume", obsHandler.ResumeRecording).Methods("POST") - obs.HandleFunc("/recording/toggle", obsHandler.ToggleRecording).Methods("POST") - - // Webhook endpoints - webhooks := api.PathPrefix("/webhooks").Subrouter() - webhooks.HandleFunc("", webhookHandler.ListWebhooks).Methods("GET") - webhooks.HandleFunc("", webhookHandler.RegisterWebhook).Methods("POST") - webhooks.HandleFunc("/{id}", webhookHandler.RemoveWebhook).Methods("DELETE") - webhooks.HandleFunc("/{id}/test", webhookHandler.TestWebhook).Methods("POST") - - // WebSocket endpoint - g.router.HandleFunc("/ws", g.handleWebSocket).Methods("GET") - - g.logger.Info("Registered all gateway routes") -} diff --git a/Premium/Desktop/internal/gateway/websocket.go b/Premium/Desktop/internal/gateway/websocket.go deleted file mode 100644 index 357929c2c..000000000 --- a/Premium/Desktop/internal/gateway/websocket.go +++ /dev/null @@ -1,255 +0,0 @@ -package gateway - -import ( - "net/http" - "sync" - "time" - - "github.com/gorilla/websocket" - "github.com/sirupsen/logrus" -) - -// WebSocketHub manages WebSocket connections and broadcasts -type WebSocketHub struct { - clients map[*WebSocketClient]bool - broadcast chan WSMessage - register chan *WebSocketClient - unregister chan *WebSocketClient - logger *logrus.Logger - running bool - runningMux sync.RWMutex -} - -// WebSocketClient represents a connected WebSocket client -type WebSocketClient struct { - hub *WebSocketHub - conn *websocket.Conn - send chan WSMessage -} - -// WSMessage represents a WebSocket message -type WSMessage struct { - Type string `json:"type"` - Data interface{} `json:"data"` - Timestamp int64 `json:"timestamp"` -} - -var upgrader = websocket.Upgrader{ - ReadBufferSize: 1024, - WriteBufferSize: 1024, - CheckOrigin: func(r *http.Request) bool { - // Allow all origins for local gateway - return true - }, -} - -// NewWebSocketHub creates a new WebSocket hub -func NewWebSocketHub(logger *logrus.Logger) *WebSocketHub { - return &WebSocketHub{ - clients: make(map[*WebSocketClient]bool), - broadcast: make(chan WSMessage, 256), - register: make(chan *WebSocketClient), - unregister: make(chan *WebSocketClient), - logger: logger, - } -} - -// Run starts the WebSocket hub -func (h *WebSocketHub) Run() { - h.runningMux.Lock() - h.running = true - h.runningMux.Unlock() - - h.logger.Info("WebSocket hub started") - - for { - select { - case client := <-h.register: - h.clients[client] = true - h.logger.WithField("client_count", len(h.clients)).Debug("WebSocket client registered") - - case client := <-h.unregister: - if _, ok := h.clients[client]; ok { - delete(h.clients, client) - close(client.send) - h.logger.WithField("client_count", len(h.clients)).Debug("WebSocket client unregistered") - } - - case message := <-h.broadcast: - // Add timestamp if not set - if message.Timestamp == 0 { - message.Timestamp = time.Now().Unix() - } - - // Broadcast to all clients - for client := range h.clients { - select { - case client.send <- message: - default: - // Client send channel full, close connection - close(client.send) - delete(h.clients, client) - } - } - } - } -} - -// Stop stops the WebSocket hub -func (h *WebSocketHub) Stop() { - h.runningMux.Lock() - defer h.runningMux.Unlock() - - if !h.running { - return - } - - h.running = false - - // Close all client connections - for client := range h.clients { - client.conn.Close() - close(client.send) - } - - h.clients = make(map[*WebSocketClient]bool) - h.logger.Info("WebSocket hub stopped") -} - -// Broadcast sends a message to all connected clients -func (h *WebSocketHub) Broadcast(message WSMessage) { - h.runningMux.RLock() - defer h.runningMux.RUnlock() - - if !h.running { - return - } - - select { - case h.broadcast <- message: - default: - h.logger.Warn("WebSocket broadcast channel full, message dropped") - } -} - -// handleWebSocket handles WebSocket connection upgrade and lifecycle -func (g *Gateway) handleWebSocket(w http.ResponseWriter, r *http.Request) { - // Upgrade connection - conn, err := upgrader.Upgrade(w, r, nil) - if err != nil { - g.logger.WithError(err).Error("Failed to upgrade WebSocket connection") - return - } - - // Create client - client := &WebSocketClient{ - hub: g.wsHub, - conn: conn, - send: make(chan WSMessage, 256), - } - - // Register client - client.hub.register <- client - - // Start goroutines - go client.writePump() - go client.readPump() - - g.logger.WithField("remote_addr", r.RemoteAddr).Info("WebSocket connection established") -} - -const ( - // Time allowed to write a message to the peer - writeWait = 10 * time.Second - - // Time allowed to read the next pong message from the peer - pongWait = 60 * time.Second - - // Send pings to peer with this period (must be less than pongWait) - pingPeriod = (pongWait * 9) / 10 - - // Maximum message size allowed from peer - maxMessageSize = 512 * 1024 // 512KB -) - -// readPump pumps messages from the WebSocket connection to the hub -func (c *WebSocketClient) readPump() { - defer func() { - c.hub.unregister <- c - c.conn.Close() - }() - - c.conn.SetReadLimit(maxMessageSize) - c.conn.SetReadDeadline(time.Now().Add(pongWait)) - c.conn.SetPongHandler(func(string) error { - c.conn.SetReadDeadline(time.Now().Add(pongWait)) - return nil - }) - - for { - _, message, err := c.conn.ReadMessage() - if err != nil { - if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) { - c.hub.logger.WithError(err).Error("WebSocket read error") - } - break - } - - // Log received message (clients typically only send pings) - c.hub.logger.WithField("message", string(message)).Debug("WebSocket message received") - } -} - -// writePump pumps messages from the hub to the WebSocket connection -func (c *WebSocketClient) writePump() { - ticker := time.NewTicker(pingPeriod) - defer func() { - ticker.Stop() - c.conn.Close() - }() - - for { - select { - case message, ok := <-c.send: - c.conn.SetWriteDeadline(time.Now().Add(writeWait)) - if !ok { - // Hub closed the channel - c.conn.WriteMessage(websocket.CloseMessage, []byte{}) - return - } - - // Write JSON message - if err := c.conn.WriteJSON(message); err != nil { - c.hub.logger.WithError(err).Error("Failed to write WebSocket message") - return - } - - case <-ticker.C: - c.conn.SetWriteDeadline(time.Now().Add(writeWait)) - if err := c.conn.WriteMessage(websocket.PingMessage, nil); err != nil { - return - } - } - } -} - -// SendToClient sends a message to a specific client -func (c *WebSocketClient) SendMessage(message WSMessage) error { - if message.Timestamp == 0 { - message.Timestamp = time.Now().Unix() - } - - select { - case c.send <- message: - return nil - default: - return websocket.ErrCloseSent - } -} - -// GetConnectedClients returns the number of connected WebSocket clients -func (h *WebSocketHub) GetConnectedClients() int { - h.runningMux.RLock() - defer h.runningMux.RUnlock() - return len(h.clients) -} diff --git a/Premium/Desktop/internal/license/license.go b/Premium/Desktop/internal/license/license.go deleted file mode 100644 index 2a5dc00de..000000000 --- a/Premium/Desktop/internal/license/license.go +++ /dev/null @@ -1,193 +0,0 @@ -package license - -import ( - "crypto/sha256" - "encoding/hex" - "fmt" - "os" - "runtime" - "strings" - "time" -) - -const ( - // LicenseText is the premium license text that users must accept - LicenseText = ` -WaddleBot Premium Desktop Bridge License Agreement - -Copyright (c) 2024 WaddleBot - -PREMIUM SOFTWARE LICENSE - -This software is licensed exclusively to users with active WaddleBot Premium subscriptions. - -1. GRANT OF LICENSE - Subject to the terms and conditions of this Agreement, WaddleBot grants you a - non-exclusive, non-transferable license to use the WaddleBot Premium Desktop Bridge - software solely for your personal or business use with WaddleBot Premium services. - -2. RESTRICTIONS - - You may NOT distribute, sublicense, or share this software - - You may NOT reverse engineer, decompile, or disassemble this software - - You may NOT use this software without an active WaddleBot Premium subscription - - You may NOT use this software for commercial purposes without proper licensing - -3. SUBSCRIPTION REQUIREMENT - This software requires an active WaddleBot Premium subscription. Use of this software - without a valid subscription is strictly prohibited and constitutes a violation of - this license agreement. - -4. TERMINATION - This license terminates automatically if your WaddleBot Premium subscription expires - or is cancelled. You must immediately cease all use of the software upon termination. - -5. DISCLAIMER - This software is provided "AS IS" without warranty of any kind. WaddleBot disclaims - all warranties, express or implied, including but not limited to warranties of - merchantability and fitness for a particular purpose. - -6. LIMITATION OF LIABILITY - In no event shall WaddleBot be liable for any damages arising out of or in connection - with the use or performance of this software. - -By using this software, you acknowledge that you have read, understood, and agree to be -bound by the terms of this license agreement. - -WaddleBot Premium Desktop Bridge v1.0.0 -` - - // License acceptance marker - licenseAcceptanceFile = ".license-accepted" -) - -// ValidateLicense checks if the user has accepted the premium license -func ValidateLicense() bool { - // Check if license has been accepted - if !hasAcceptedLicense() { - return promptForLicenseAcceptance() - } - - // TODO: In production, this should verify the user's premium subscription status - // For now, we'll assume the license is valid if accepted - return true -} - -// hasAcceptedLicense checks if the user has previously accepted the license -func hasAcceptedLicense() bool { - homeDir, err := os.UserHomeDir() - if err != nil { - return false - } - - licenseFile := fmt.Sprintf("%s/.waddlebot-bridge/%s", homeDir, licenseAcceptanceFile) - - // Check if license acceptance file exists - if _, err := os.Stat(licenseFile); os.IsNotExist(err) { - return false - } - - // Read the acceptance file and verify the hash - content, err := os.ReadFile(licenseFile) - if err != nil { - return false - } - - expectedHash := generateLicenseHash() - return strings.TrimSpace(string(content)) == expectedHash -} - -// promptForLicenseAcceptance displays the license and prompts for acceptance -func promptForLicenseAcceptance() bool { - fmt.Println(LicenseText) - fmt.Println(strings.Repeat("=", 80)) - fmt.Println("WaddleBot Premium Desktop Bridge License Agreement") - fmt.Println(strings.Repeat("=", 80)) - - fmt.Print("\nDo you have an active WaddleBot Premium subscription? (y/N): ") - var hasSubscription string - fmt.Scanln(&hasSubscription) - - if strings.ToLower(hasSubscription) != "y" && strings.ToLower(hasSubscription) != "yes" { - fmt.Println("\nThis software requires an active WaddleBot Premium subscription.") - fmt.Println("Please visit https://waddlebot.io/premium to subscribe.") - return false - } - - fmt.Print("\nDo you accept the terms of the license agreement? (y/N): ") - var acceptance string - fmt.Scanln(&acceptance) - - if strings.ToLower(acceptance) != "y" && strings.ToLower(acceptance) != "yes" { - fmt.Println("\nYou must accept the license agreement to use this software.") - return false - } - - // Save license acceptance - if err := saveLicenseAcceptance(); err != nil { - fmt.Printf("Warning: Failed to save license acceptance: %v\n", err) - } - - fmt.Println("\nLicense accepted. Welcome to WaddleBot Premium Desktop Bridge!") - return true -} - -// saveLicenseAcceptance saves the license acceptance to disk -func saveLicenseAcceptance() error { - homeDir, err := os.UserHomeDir() - if err != nil { - return fmt.Errorf("failed to get user home directory: %w", err) - } - - bridgeDir := fmt.Sprintf("%s/.waddlebot-bridge", homeDir) - if err := os.MkdirAll(bridgeDir, 0755); err != nil { - return fmt.Errorf("failed to create bridge directory: %w", err) - } - - licenseFile := fmt.Sprintf("%s/%s", bridgeDir, licenseAcceptanceFile) - licenseHash := generateLicenseHash() - - if err := os.WriteFile(licenseFile, []byte(licenseHash), 0644); err != nil { - return fmt.Errorf("failed to write license acceptance file: %w", err) - } - - return nil -} - -// generateLicenseHash generates a hash of the license text and system info -func generateLicenseHash() string { - // Combine license text with system information for uniqueness - data := fmt.Sprintf("%s|%s|%s|%d", - LicenseText, - runtime.GOOS, - runtime.GOARCH, - time.Now().Unix()/86400, // Day-based timestamp - ) - - hash := sha256.Sum256([]byte(data)) - return hex.EncodeToString(hash[:]) -} - -// GetLicenseInfo returns information about the current license -func GetLicenseInfo() map[string]interface{} { - return map[string]interface{}{ - "accepted": hasAcceptedLicense(), - "version": "1.0.0", - "type": "Premium", - "platform": fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH), - "requirement": "Active WaddleBot Premium Subscription", - } -} - -// DisplayLicenseInfo displays the current license information -func DisplayLicenseInfo() { - info := GetLicenseInfo() - fmt.Println("\n" + strings.Repeat("=", 50)) - fmt.Println("WaddleBot Premium Desktop Bridge License Info") - fmt.Println(strings.Repeat("=", 50)) - fmt.Printf("Version: %s\n", info["version"]) - fmt.Printf("Type: %s\n", info["type"]) - fmt.Printf("Platform: %s\n", info["platform"]) - fmt.Printf("Requirement: %s\n", info["requirement"]) - fmt.Printf("Accepted: %t\n", info["accepted"]) - fmt.Println(strings.Repeat("=", 50)) -} diff --git a/Premium/Desktop/internal/license/license_test.go b/Premium/Desktop/internal/license/license_test.go deleted file mode 100644 index d2624f269..000000000 --- a/Premium/Desktop/internal/license/license_test.go +++ /dev/null @@ -1,361 +0,0 @@ -package license - -import ( - "os" - "path/filepath" - "strings" - "testing" -) - -func TestValidateLicense(t *testing.T) { - // Create temporary directory for testing - tmpDir := t.TempDir() - - // Override home directory for testing - originalHome := os.Getenv("HOME") - defer func() { - os.Setenv("HOME", originalHome) - }() - os.Setenv("HOME", tmpDir) - - // Test without license acceptance - result := ValidateLicense() - if result { - t.Error("Expected false for unaccepted license, but got true") - } - - // Test with license acceptance - bridgeDir := filepath.Join(tmpDir, ".waddlebot-bridge") - if err := os.MkdirAll(bridgeDir, 0755); err != nil { - t.Fatalf("Failed to create bridge directory: %v", err) - } - - licenseFile := filepath.Join(bridgeDir, licenseAcceptanceFile) - licenseHash := generateLicenseHash() - if err := os.WriteFile(licenseFile, []byte(licenseHash), 0644); err != nil { - t.Fatalf("Failed to write license file: %v", err) - } - - result = ValidateLicense() - if !result { - t.Error("Expected true for accepted license, but got false") - } -} - -func TestHasAcceptedLicense(t *testing.T) { - // Create temporary directory for testing - tmpDir := t.TempDir() - - // Override home directory for testing - originalHome := os.Getenv("HOME") - defer func() { - os.Setenv("HOME", originalHome) - }() - os.Setenv("HOME", tmpDir) - - // Test without license file - result := hasAcceptedLicense() - if result { - t.Error("Expected false for missing license file, but got true") - } - - // Test with invalid license file - bridgeDir := filepath.Join(tmpDir, ".waddlebot-bridge") - if err := os.MkdirAll(bridgeDir, 0755); err != nil { - t.Fatalf("Failed to create bridge directory: %v", err) - } - - licenseFile := filepath.Join(bridgeDir, licenseAcceptanceFile) - if err := os.WriteFile(licenseFile, []byte("invalid-hash"), 0644); err != nil { - t.Fatalf("Failed to write license file: %v", err) - } - - result = hasAcceptedLicense() - if result { - t.Error("Expected false for invalid license hash, but got true") - } - - // Test with valid license file - validHash := generateLicenseHash() - if err := os.WriteFile(licenseFile, []byte(validHash), 0644); err != nil { - t.Fatalf("Failed to write license file: %v", err) - } - - result = hasAcceptedLicense() - if !result { - t.Error("Expected true for valid license hash, but got false") - } -} - -func TestSaveLicenseAcceptance(t *testing.T) { - // Create temporary directory for testing - tmpDir := t.TempDir() - - // Override home directory for testing - originalHome := os.Getenv("HOME") - defer func() { - os.Setenv("HOME", originalHome) - }() - os.Setenv("HOME", tmpDir) - - // Test saving license acceptance - err := saveLicenseAcceptance() - if err != nil { - t.Fatalf("saveLicenseAcceptance failed: %v", err) - } - - // Verify file was created - bridgeDir := filepath.Join(tmpDir, ".waddlebot-bridge") - licenseFile := filepath.Join(bridgeDir, licenseAcceptanceFile) - - if _, err := os.Stat(licenseFile); os.IsNotExist(err) { - t.Error("License acceptance file was not created") - } - - // Verify content - content, err := os.ReadFile(licenseFile) - if err != nil { - t.Fatalf("Failed to read license file: %v", err) - } - - expectedHash := generateLicenseHash() - if strings.TrimSpace(string(content)) != expectedHash { - t.Error("License file content does not match expected hash") - } -} - -func TestGenerateLicenseHash(t *testing.T) { - // Generate hash twice to ensure consistency - hash1 := generateLicenseHash() - hash2 := generateLicenseHash() - - if hash1 != hash2 { - t.Error("generateLicenseHash should return consistent results") - } - - // Verify hash is not empty - if hash1 == "" { - t.Error("generateLicenseHash should not return empty string") - } - - // Verify hash is hexadecimal - if len(hash1) != 64 { // SHA256 hash should be 64 characters - t.Errorf("Expected hash length 64, got %d", len(hash1)) - } - - // Verify hash contains only hexadecimal characters - for _, char := range hash1 { - if !((char >= '0' && char <= '9') || (char >= 'a' && char <= 'f')) { - t.Errorf("Hash contains non-hexadecimal character: %c", char) - } - } -} - -func TestGetLicenseInfo(t *testing.T) { - // Create temporary directory for testing - tmpDir := t.TempDir() - - // Override home directory for testing - originalHome := os.Getenv("HOME") - defer func() { - os.Setenv("HOME", originalHome) - }() - os.Setenv("HOME", tmpDir) - - info := GetLicenseInfo() - - // Verify required fields - if info == nil { - t.Fatal("GetLicenseInfo returned nil") - } - - if info["version"] != "1.0.0" { - t.Errorf("Expected version '1.0.0', got %v", info["version"]) - } - - if info["type"] != "Premium" { - t.Errorf("Expected type 'Premium', got %v", info["type"]) - } - - if info["requirement"] != "Active WaddleBot Premium Subscription" { - t.Errorf("Expected requirement 'Active WaddleBot Premium Subscription', got %v", info["requirement"]) - } - - // Verify accepted field - accepted, ok := info["accepted"].(bool) - if !ok { - t.Error("Expected 'accepted' field to be boolean") - } - - // Should be false initially - if accepted { - t.Error("Expected 'accepted' to be false initially") - } - - // Save license acceptance and check again - saveLicenseAcceptance() - - info = GetLicenseInfo() - accepted, ok = info["accepted"].(bool) - if !ok { - t.Error("Expected 'accepted' field to be boolean") - } - - if !accepted { - t.Error("Expected 'accepted' to be true after saving acceptance") - } -} - -func TestLicenseConstants(t *testing.T) { - // Test that license text is not empty - if LicenseText == "" { - t.Error("LicenseText should not be empty") - } - - // Test that license text contains required components - requiredComponents := []string{ - "WaddleBot Premium Desktop Bridge License Agreement", - "Copyright (c) 2024 WaddleBot", - "PREMIUM SOFTWARE LICENSE", - "GRANT OF LICENSE", - "RESTRICTIONS", - "SUBSCRIPTION REQUIREMENT", - "TERMINATION", - "DISCLAIMER", - "LIMITATION OF LIABILITY", - } - - for _, component := range requiredComponents { - if !strings.Contains(LicenseText, component) { - t.Errorf("LicenseText should contain '%s'", component) - } - } - - // Test license acceptance file constant - if licenseAcceptanceFile == "" { - t.Error("licenseAcceptanceFile should not be empty") - } - - expectedFile := ".license-accepted" - if licenseAcceptanceFile != expectedFile { - t.Errorf("Expected licenseAcceptanceFile '%s', got '%s'", expectedFile, licenseAcceptanceFile) - } -} - -func TestLicenseTextContent(t *testing.T) { - // Test that license text mentions premium subscription - if !strings.Contains(LicenseText, "premium subscription") { - t.Error("LicenseText should mention 'premium subscription'") - } - - // Test that license text mentions WaddleBot Premium - if !strings.Contains(LicenseText, "WaddleBot Premium") { - t.Error("LicenseText should mention 'WaddleBot Premium'") - } - - // Test that license text includes version - if !strings.Contains(LicenseText, "v1.0.0") { - t.Error("LicenseText should include version 'v1.0.0'") - } - - // Test that license text includes restrictions - restrictions := []string{ - "may NOT distribute", - "may NOT reverse engineer", - "may NOT use this software without an active", - } - - for _, restriction := range restrictions { - if !strings.Contains(LicenseText, restriction) { - t.Errorf("LicenseText should contain restriction: '%s'", restriction) - } - } -} - -func TestDisplayLicenseInfo(t *testing.T) { - // This test mainly verifies the function doesn't panic - // In a real test environment, you might want to capture stdout - - // Create temporary directory for testing - tmpDir := t.TempDir() - - // Override home directory for testing - originalHome := os.Getenv("HOME") - defer func() { - os.Setenv("HOME", originalHome) - }() - os.Setenv("HOME", tmpDir) - - // Should not panic - defer func() { - if r := recover(); r != nil { - t.Errorf("DisplayLicenseInfo panicked: %v", r) - } - }() - - DisplayLicenseInfo() -} - -func TestLicenseValidationEdgeCases(t *testing.T) { - // Test with invalid home directory - originalHome := os.Getenv("HOME") - defer func() { - os.Setenv("HOME", originalHome) - }() - - // Set invalid home directory - os.Setenv("HOME", "/nonexistent/directory") - - result := hasAcceptedLicense() - if result { - t.Error("Expected false for invalid home directory, but got true") - } - - // Test saveLicenseAcceptance with invalid home directory - err := saveLicenseAcceptance() - if err == nil { - t.Error("Expected error for invalid home directory, but got none") - } -} - -func TestLicenseFilePermissions(t *testing.T) { - // Create temporary directory for testing - tmpDir := t.TempDir() - - // Override home directory for testing - originalHome := os.Getenv("HOME") - defer func() { - os.Setenv("HOME", originalHome) - }() - os.Setenv("HOME", tmpDir) - - // Save license acceptance - err := saveLicenseAcceptance() - if err != nil { - t.Fatalf("saveLicenseAcceptance failed: %v", err) - } - - // Check file permissions - licenseFile := filepath.Join(tmpDir, ".waddlebot-bridge", licenseAcceptanceFile) - info, err := os.Stat(licenseFile) - if err != nil { - t.Fatalf("Failed to stat license file: %v", err) - } - - expectedPerms := os.FileMode(0644) - if info.Mode() != expectedPerms { - t.Errorf("Expected file permissions %v, got %v", expectedPerms, info.Mode()) - } - - // Check directory permissions - bridgeDir := filepath.Join(tmpDir, ".waddlebot-bridge") - info, err = os.Stat(bridgeDir) - if err != nil { - t.Fatalf("Failed to stat bridge directory: %v", err) - } - - expectedDirPerms := os.FileMode(0755) | os.ModeDir - if info.Mode() != expectedDirPerms { - t.Errorf("Expected directory permissions %v, got %v", expectedDirPerms, info.Mode()) - } -} \ No newline at end of file diff --git a/Premium/Desktop/internal/logger/logger.go b/Premium/Desktop/internal/logger/logger.go deleted file mode 100644 index c42fbe9a7..000000000 --- a/Premium/Desktop/internal/logger/logger.go +++ /dev/null @@ -1,46 +0,0 @@ -package logger - -import ( - "os" - "strings" - - "github.com/sirupsen/logrus" -) - -var logger *logrus.Logger - -// Init initializes the logger with the specified level -func Init(level string) { - logger = logrus.New() - - // Set log level - switch strings.ToLower(level) { - case "debug": - logger.SetLevel(logrus.DebugLevel) - case "info": - logger.SetLevel(logrus.InfoLevel) - case "warn", "warning": - logger.SetLevel(logrus.WarnLevel) - case "error": - logger.SetLevel(logrus.ErrorLevel) - default: - logger.SetLevel(logrus.InfoLevel) - } - - // Set output format - logger.SetFormatter(&logrus.TextFormatter{ - FullTimestamp: true, - DisableColors: false, - }) - - // Set output - logger.SetOutput(os.Stdout) -} - -// GetLogger returns the configured logger instance -func GetLogger() *logrus.Logger { - if logger == nil { - Init("info") - } - return logger -} \ No newline at end of file diff --git a/Premium/Desktop/internal/models/auth.go b/Premium/Desktop/internal/models/auth.go deleted file mode 100644 index 165bdd275..000000000 --- a/Premium/Desktop/internal/models/auth.go +++ /dev/null @@ -1,13 +0,0 @@ -package models - -import "time" - -// AuthSession represents an active authentication session. -type AuthSession struct { - ID string `json:"id"` - UserID string `json:"user_id"` - CommunityID string `json:"community_id"` - IssuedAt time.Time `json:"issued_at"` - ExpiresAt time.Time `json:"expires_at"` - Credential []byte `json:"credential"` -} diff --git a/Premium/Desktop/internal/models/module.go b/Premium/Desktop/internal/models/module.go deleted file mode 100644 index 6cf2f7425..000000000 --- a/Premium/Desktop/internal/models/module.go +++ /dev/null @@ -1,28 +0,0 @@ -package models - -import "time" - -// ModuleInfo represents information about a module. -type ModuleInfo struct { - Name string `json:"name"` - Version string `json:"version"` - Description string `json:"description"` - Author string `json:"author"` - Actions []ActionInfo `json:"actions"` - Dependencies []string `json:"dependencies"` - Permissions []string `json:"permissions"` - Config map[string]string `json:"config"` - Enabled bool `json:"enabled"` - LoadedAt time.Time `json:"loaded_at"` - LastUsed time.Time `json:"last_used"` -} - -// ActionInfo represents information about an action. -type ActionInfo struct { - Name string `json:"name"` - Description string `json:"description"` - Parameters map[string]interface{} `json:"parameters"` - ReturnType string `json:"return_type"` - Timeout int `json:"timeout"` - Permissions []string `json:"permissions"` -} diff --git a/Premium/Desktop/internal/modules/errors.go b/Premium/Desktop/internal/modules/errors.go deleted file mode 100644 index 72fbe28d3..000000000 --- a/Premium/Desktop/internal/modules/errors.go +++ /dev/null @@ -1,16 +0,0 @@ -package modules - -import "fmt" - -// Common module errors -var ( - ErrModuleNotFound = fmt.Errorf("module not found") - ErrModuleDisabled = fmt.Errorf("module is disabled") - ErrActionNotFound = fmt.Errorf("action not found") - ErrActionFailed = fmt.Errorf("action execution failed") - ErrInvalidParameters = fmt.Errorf("invalid parameters") - ErrModuleInitFailed = fmt.Errorf("module initialization failed") - ErrModuleLoadFailed = fmt.Errorf("module load failed") - ErrPermissionDenied = fmt.Errorf("permission denied") - ErrTimeout = fmt.Errorf("operation timeout") -) \ No newline at end of file diff --git a/Premium/Desktop/internal/modules/examples/system/system.go b/Premium/Desktop/internal/modules/examples/system/system.go deleted file mode 100644 index da0dd2e65..000000000 --- a/Premium/Desktop/internal/modules/examples/system/system.go +++ /dev/null @@ -1,360 +0,0 @@ -package main - -import ( - "context" - "fmt" - "os/exec" - "runtime" - "strconv" - "strings" - "time" - - "github.com/shirou/gopsutil/cpu" - "github.com/shirou/gopsutil/disk" - "github.com/shirou/gopsutil/host" - "github.com/shirou/gopsutil/mem" - "github.com/shirou/gopsutil/process" - "waddlebot-bridge/internal/modules" -) - -// SystemModule provides system information and control -type SystemModule struct { - config map[string]string -} - -// NewModule creates a new system module instance -func NewModule() modules.ModuleInterface { - return &SystemModule{} -} - -// Initialize initializes the system module -func (m *SystemModule) Initialize(config map[string]string) error { - m.config = config - return nil -} - -// GetInfo returns module information -func (m *SystemModule) GetInfo() *modules.ModuleInfo { - return &modules.ModuleInfo{ - Name: "system", - Version: "1.0.0", - Description: "System information and control module", - Author: "WaddleBot", - Actions: []modules.ActionInfo{ - { - Name: "get_info", - Description: "Get system information", - Parameters: map[string]interface{}{}, - ReturnType: "object", - Timeout: 10, - Permissions: []string{"system.read"}, - }, - { - Name: "get_processes", - Description: "Get running processes", - Parameters: map[string]interface{}{ - "limit": "number", - }, - ReturnType: "array", - Timeout: 15, - Permissions: []string{"system.read"}, - }, - { - Name: "execute_command", - Description: "Execute a system command", - Parameters: map[string]interface{}{ - "command": "string", - "args": "array", - }, - ReturnType: "object", - Timeout: 30, - Permissions: []string{"system.execute"}, - }, - { - Name: "get_disk_usage", - Description: "Get disk usage information", - Parameters: map[string]interface{}{ - "path": "string", - }, - ReturnType: "object", - Timeout: 10, - Permissions: []string{"system.read"}, - }, - { - Name: "get_memory_info", - Description: "Get memory usage information", - Parameters: map[string]interface{}{}, - ReturnType: "object", - Timeout: 5, - Permissions: []string{"system.read"}, - }, - { - Name: "get_cpu_info", - Description: "Get CPU usage information", - Parameters: map[string]interface{}{}, - ReturnType: "object", - Timeout: 10, - Permissions: []string{"system.read"}, - }, - }, - Dependencies: []string{}, - Permissions: []string{"system.read", "system.execute"}, - Config: map[string]string{}, - Enabled: true, - LoadedAt: time.Now(), - } -} - -// ExecuteAction executes a specific action -func (m *SystemModule) ExecuteAction(ctx context.Context, action string, parameters map[string]string) (map[string]interface{}, error) { - switch action { - case "get_info": - return m.getSystemInfo(ctx) - case "get_processes": - return m.getProcesses(ctx, parameters) - case "execute_command": - return m.executeCommand(ctx, parameters) - case "get_disk_usage": - return m.getDiskUsage(ctx, parameters) - case "get_memory_info": - return m.getMemoryInfo(ctx) - case "get_cpu_info": - return m.getCPUInfo(ctx) - default: - return nil, fmt.Errorf("unknown action: %s", action) - } -} - -// GetActions returns available actions -func (m *SystemModule) GetActions() []modules.ActionInfo { - return m.GetInfo().Actions -} - -// Cleanup cleans up module resources -func (m *SystemModule) Cleanup() error { - return nil -} - -// getSystemInfo returns general system information -func (m *SystemModule) getSystemInfo(ctx context.Context) (map[string]interface{}, error) { - hostInfo, err := host.Info() - if err != nil { - return nil, fmt.Errorf("failed to get host info: %w", err) - } - - return map[string]interface{}{ - "hostname": hostInfo.Hostname, - "os": hostInfo.OS, - "platform": hostInfo.Platform, - "platform_family": hostInfo.PlatformFamily, - "platform_version": hostInfo.PlatformVersion, - "kernel_version": hostInfo.KernelVersion, - "kernel_arch": hostInfo.KernelArch, - "uptime": hostInfo.Uptime, - "boot_time": hostInfo.BootTime, - "processes": hostInfo.Procs, - "go_version": runtime.Version(), - "go_arch": runtime.GOARCH, - "go_os": runtime.GOOS, - "cpu_count": runtime.NumCPU(), - "timestamp": time.Now().Unix(), - }, nil -} - -// getProcesses returns running processes -func (m *SystemModule) getProcesses(ctx context.Context, parameters map[string]string) (map[string]interface{}, error) { - limit := 50 // default limit - if limitStr, ok := parameters["limit"]; ok { - if parsedLimit, err := strconv.Atoi(limitStr); err == nil && parsedLimit > 0 { - limit = parsedLimit - } - } - - pids, err := process.Pids() - if err != nil { - return nil, fmt.Errorf("failed to get process IDs: %w", err) - } - - processes := make([]map[string]interface{}, 0, limit) - count := 0 - - for _, pid := range pids { - if count >= limit { - break - } - - proc, err := process.NewProcess(pid) - if err != nil { - continue - } - - name, _ := proc.Name() - status, _ := proc.Status() - cpuPercent, _ := proc.CPUPercent() - memInfo, _ := proc.MemoryInfo() - createTime, _ := proc.CreateTime() - - processInfo := map[string]interface{}{ - "pid": pid, - "name": name, - "status": status, - "cpu_percent": cpuPercent, - "create_time": createTime, - } - - if memInfo != nil { - processInfo["memory_rss"] = memInfo.RSS - processInfo["memory_vms"] = memInfo.VMS - } - - processes = append(processes, processInfo) - count++ - } - - return map[string]interface{}{ - "processes": processes, - "total": len(pids), - "returned": count, - "limit": limit, - }, nil -} - -// executeCommand executes a system command -func (m *SystemModule) executeCommand(ctx context.Context, parameters map[string]string) (map[string]interface{}, error) { - command, ok := parameters["command"] - if !ok { - return nil, fmt.Errorf("command parameter is required") - } - - // Security check - only allow certain commands - allowedCommands := []string{"echo", "ls", "pwd", "date", "whoami", "uname"} - isAllowed := false - for _, allowed := range allowedCommands { - if command == allowed { - isAllowed = true - break - } - } - - if !isAllowed { - return nil, fmt.Errorf("command '%s' is not allowed", command) - } - - // Parse arguments - var args []string - if argsStr, ok := parameters["args"]; ok { - args = strings.Split(argsStr, " ") - } - - // Execute command - cmd := exec.CommandContext(ctx, command, args...) - output, err := cmd.CombinedOutput() - - result := map[string]interface{}{ - "command": command, - "args": args, - "output": string(output), - "success": err == nil, - } - - if err != nil { - result["error"] = err.Error() - } - - return result, nil -} - -// getDiskUsage returns disk usage information -func (m *SystemModule) getDiskUsage(ctx context.Context, parameters map[string]string) (map[string]interface{}, error) { - path := "/" - if pathParam, ok := parameters["path"]; ok { - path = pathParam - } - - usage, err := disk.Usage(path) - if err != nil { - return nil, fmt.Errorf("failed to get disk usage: %w", err) - } - - return map[string]interface{}{ - "path": path, - "total": usage.Total, - "free": usage.Free, - "used": usage.Used, - "used_percent": usage.UsedPercent, - "inodes_total": usage.InodesTotal, - "inodes_used": usage.InodesUsed, - "inodes_free": usage.InodesFree, - "inodes_used_percent": usage.InodesUsedPercent, - }, nil -} - -// getMemoryInfo returns memory usage information -func (m *SystemModule) getMemoryInfo(ctx context.Context) (map[string]interface{}, error) { - memInfo, err := mem.VirtualMemory() - if err != nil { - return nil, fmt.Errorf("failed to get memory info: %w", err) - } - - swapInfo, err := mem.SwapMemory() - if err != nil { - return nil, fmt.Errorf("failed to get swap info: %w", err) - } - - return map[string]interface{}{ - "virtual": map[string]interface{}{ - "total": memInfo.Total, - "available": memInfo.Available, - "used": memInfo.Used, - "used_percent": memInfo.UsedPercent, - "free": memInfo.Free, - "active": memInfo.Active, - "inactive": memInfo.Inactive, - "buffers": memInfo.Buffers, - "cached": memInfo.Cached, - }, - "swap": map[string]interface{}{ - "total": swapInfo.Total, - "used": swapInfo.Used, - "free": swapInfo.Free, - "used_percent": swapInfo.UsedPercent, - }, - }, nil -} - -// getCPUInfo returns CPU usage information -func (m *SystemModule) getCPUInfo(ctx context.Context) (map[string]interface{}, error) { - cpuInfo, err := cpu.Info() - if err != nil { - return nil, fmt.Errorf("failed to get CPU info: %w", err) - } - - cpuPercent, err := cpu.Percent(time.Second, false) - if err != nil { - return nil, fmt.Errorf("failed to get CPU percent: %w", err) - } - - cpuCount, err := cpu.Counts(true) - if err != nil { - return nil, fmt.Errorf("failed to get CPU counts: %w", err) - } - - result := map[string]interface{}{ - "count": cpuCount, - "percent": cpuPercent, - "info": []map[string]interface{}{}, - } - - for _, info := range cpuInfo { - result["info"] = append(result["info"].([]map[string]interface{}), map[string]interface{}{ - "model_name": info.ModelName, - "family": info.Family, - "speed": info.Mhz, - "cache_size": info.CacheSize, - "cores": info.Cores, - "vendor_id": info.VendorID, - }) - } - - return result, nil -} diff --git a/Premium/Desktop/internal/modules/manager.go b/Premium/Desktop/internal/modules/manager.go deleted file mode 100644 index fdfa5fc35..000000000 --- a/Premium/Desktop/internal/modules/manager.go +++ /dev/null @@ -1,424 +0,0 @@ -package modules - -import ( - "context" - "encoding/json" - "fmt" - "io/fs" - "os" - "path/filepath" - "plugin" - "strings" - "sync" - "time" - - "github.com/sirupsen/logrus" - "waddlebot-bridge/internal/config" - "waddlebot-bridge/internal/logger" - "waddlebot-bridge/internal/models" - "waddlebot-bridge/internal/storage" -) - -// Manager handles module loading and execution -type Manager struct { - config *config.Config - storage storage.Storage - logger *logrus.Logger - modules map[string]*Module - moduleInfos map[string]*models.ModuleInfo - mutex sync.RWMutex -} - -// ModuleInfo is an alias for models.ModuleInfo for backward compatibility -type ModuleInfo = models.ModuleInfo - -// ActionInfo is an alias for models.ActionInfo for backward compatibility -type ActionInfo = models.ActionInfo - -// Module represents a loaded module -type Module struct { - Info *ModuleInfo - Plugin *plugin.Plugin - Instance ModuleInterface - Config map[string]string - Enabled bool - LoadedAt time.Time -} - -// ModuleInterface defines the interface that all modules must implement -type ModuleInterface interface { - // Initialize initializes the module with configuration - Initialize(config map[string]string) error - - // GetInfo returns module information - GetInfo() *models.ModuleInfo - - // ExecuteAction executes a specific action - ExecuteAction(ctx context.Context, action string, parameters map[string]string) (map[string]interface{}, error) - - // GetActions returns available actions - GetActions() []models.ActionInfo - - // Cleanup cleans up module resources - Cleanup() error -} - -// NewManager creates a new module manager -func NewManager(cfg *config.Config, store storage.Storage) *Manager { - return &Manager{ - config: cfg, - storage: store, - logger: logger.GetLogger(), - modules: make(map[string]*Module), - moduleInfos: make(map[string]*ModuleInfo), - } -} - -// LoadModules loads all modules from the modules directory -func (m *Manager) LoadModules() error { - m.mutex.Lock() - defer m.mutex.Unlock() - - m.logger.WithField("modules_dir", m.config.ModulesDir).Info("Loading modules") - - // Walk through modules directory - err := filepath.WalkDir(m.config.ModulesDir, func(path string, d fs.DirEntry, err error) error { - if err != nil { - return err - } - - // Skip directories - if d.IsDir() { - return nil - } - - // Check if it's a .so file (plugin) - if strings.HasSuffix(path, ".so") { - if err := m.loadModule(path); err != nil { - m.logger.WithError(err).WithField("path", path).Error("Failed to load module") - // Continue loading other modules - } - } - - return nil - }) - - if err != nil { - return fmt.Errorf("failed to walk modules directory: %w", err) - } - - m.logger.WithField("loaded_modules", len(m.modules)).Info("Finished loading modules") - return nil -} - -// loadModule loads a single module from a plugin file -func (m *Manager) loadModule(path string) error { - m.logger.WithField("path", path).Debug("Loading module") - - // Load plugin - plug, err := plugin.Open(path) - if err != nil { - return fmt.Errorf("failed to open plugin: %w", err) - } - - // Look for the required symbol - symbol, err := plug.Lookup("NewModule") - if err != nil { - return fmt.Errorf("failed to find NewModule symbol: %w", err) - } - - // Assert the symbol is the correct type - newModuleFunc, ok := symbol.(func() ModuleInterface) - if !ok { - return fmt.Errorf("NewModule is not of type func() ModuleInterface") - } - - // Create module instance - instance := newModuleFunc() - - // Get module info - info := instance.GetInfo() - if info == nil { - return fmt.Errorf("module returned nil info") - } - - // Load module configuration - config, err := m.loadModuleConfig(info.Name) - if err != nil { - m.logger.WithError(err).WithField("module", info.Name).Warn("Failed to load module config, using defaults") - config = make(map[string]string) - } - - // Initialize module - if err := instance.Initialize(config); err != nil { - return fmt.Errorf("failed to initialize module: %w", err) - } - - // Create module wrapper - module := &Module{ - Info: info, - Plugin: plug, - Instance: instance, - Config: config, - Enabled: true, - LoadedAt: time.Now(), - } - - // Store module - m.modules[info.Name] = module - m.moduleInfos[info.Name] = info - - // Save module info to storage - if err := m.saveModuleInfo(info); err != nil { - m.logger.WithError(err).WithField("module", info.Name).Warn("Failed to save module info") - } - - m.logger.WithFields(logrus.Fields{ - "module": info.Name, - "version": info.Version, - "actions": len(info.Actions), - }).Info("Module loaded successfully") - - return nil -} - -// loadModuleConfig loads configuration for a module -func (m *Manager) loadModuleConfig(moduleName string) (map[string]string, error) { - configKey := fmt.Sprintf("module_config_%s", moduleName) - - data, err := m.storage.Get(configKey) - if err != nil { - return nil, err - } - - var config map[string]string - if err := json.Unmarshal(data, &config); err != nil { - return nil, fmt.Errorf("failed to unmarshal config: %w", err) - } - - return config, nil -} - -// saveModuleInfo saves module information to storage -func (m *Manager) saveModuleInfo(info *ModuleInfo) error { - data, err := json.Marshal(info) - if err != nil { - return fmt.Errorf("failed to marshal module info: %w", err) - } - - key := fmt.Sprintf("module_info_%s", info.Name) - return m.storage.Set(key, data) -} - -// ExecuteAction executes an action on a specific module -func (m *Manager) ExecuteAction(ctx context.Context, moduleName, action string, parameters map[string]string) (map[string]interface{}, error) { - m.mutex.RLock() - defer m.mutex.RUnlock() - - // Find module - module, exists := m.modules[moduleName] - if !exists { - return nil, fmt.Errorf("module %s not found", moduleName) - } - - // Check if module is enabled - if !module.Enabled { - return nil, fmt.Errorf("module %s is disabled", moduleName) - } - - // Update last used time - module.Info.LastUsed = time.Now() - - // Create timeout context - timeout := time.Duration(m.config.ModuleTimeout) * time.Second - if timeout == 0 { - timeout = 30 * time.Second - } - - actionCtx, cancel := context.WithTimeout(ctx, timeout) - defer cancel() - - // Execute action - result, err := module.Instance.ExecuteAction(actionCtx, action, parameters) - if err != nil { - return nil, fmt.Errorf("action execution failed: %w", err) - } - - // Update module info in storage - m.saveModuleInfo(module.Info) - - return result, nil -} - -// GetModule returns a module by name -func (m *Manager) GetModule(name string) (*Module, bool) { - m.mutex.RLock() - defer m.mutex.RUnlock() - - module, exists := m.modules[name] - return module, exists -} - -// GetModuleInfos returns information about all loaded modules -func (m *Manager) GetModuleInfos() []ModuleInfo { - m.mutex.RLock() - defer m.mutex.RUnlock() - - infos := make([]ModuleInfo, 0, len(m.moduleInfos)) - for _, info := range m.moduleInfos { - infos = append(infos, *info) - } - - return infos -} - -// GetModuleInfo returns information about a specific module -func (m *Manager) GetModuleInfo(name string) (*ModuleInfo, bool) { - m.mutex.RLock() - defer m.mutex.RUnlock() - - info, exists := m.moduleInfos[name] - return info, exists -} - -// EnableModule enables a module -func (m *Manager) EnableModule(name string) error { - m.mutex.Lock() - defer m.mutex.Unlock() - - module, exists := m.modules[name] - if !exists { - return fmt.Errorf("module %s not found", name) - } - - module.Enabled = true - module.Info.Enabled = true - - // Save to storage - if err := m.saveModuleInfo(module.Info); err != nil { - return fmt.Errorf("failed to save module info: %w", err) - } - - m.logger.WithField("module", name).Info("Module enabled") - return nil -} - -// DisableModule disables a module -func (m *Manager) DisableModule(name string) error { - m.mutex.Lock() - defer m.mutex.Unlock() - - module, exists := m.modules[name] - if !exists { - return fmt.Errorf("module %s not found", name) - } - - module.Enabled = false - module.Info.Enabled = false - - // Save to storage - if err := m.saveModuleInfo(module.Info); err != nil { - return fmt.Errorf("failed to save module info: %w", err) - } - - m.logger.WithField("module", name).Info("Module disabled") - return nil -} - -// ReloadModule reloads a module -func (m *Manager) ReloadModule(name string) error { - m.mutex.Lock() - defer m.mutex.Unlock() - - // Find existing module - module, exists := m.modules[name] - if !exists { - return fmt.Errorf("module %s not found", name) - } - - // Cleanup existing module - if err := module.Instance.Cleanup(); err != nil { - m.logger.WithError(err).WithField("module", name).Warn("Failed to cleanup module") - } - - // Remove from maps - delete(m.modules, name) - delete(m.moduleInfos, name) - - // Find and reload module file - modulePath := filepath.Join(m.config.ModulesDir, name+".so") - if _, err := os.Stat(modulePath); os.IsNotExist(err) { - return fmt.Errorf("module file %s not found", modulePath) - } - - // Load module - if err := m.loadModule(modulePath); err != nil { - return fmt.Errorf("failed to reload module: %w", err) - } - - m.logger.WithField("module", name).Info("Module reloaded") - return nil -} - -// UnloadModule unloads a module -func (m *Manager) UnloadModule(name string) error { - m.mutex.Lock() - defer m.mutex.Unlock() - - module, exists := m.modules[name] - if !exists { - return fmt.Errorf("module %s not found", name) - } - - // Cleanup module - if err := module.Instance.Cleanup(); err != nil { - m.logger.WithError(err).WithField("module", name).Warn("Failed to cleanup module") - } - - // Remove from maps - delete(m.modules, name) - delete(m.moduleInfos, name) - - m.logger.WithField("module", name).Info("Module unloaded") - return nil -} - -// GetStats returns module manager statistics -func (m *Manager) GetStats() map[string]interface{} { - m.mutex.RLock() - defer m.mutex.RUnlock() - - enabled := 0 - disabled := 0 - for _, module := range m.modules { - if module.Enabled { - enabled++ - } else { - disabled++ - } - } - - return map[string]interface{}{ - "total_modules": len(m.modules), - "enabled_modules": enabled, - "disabled_modules": disabled, - "modules_dir": m.config.ModulesDir, - } -} - -// Cleanup cleans up all modules -func (m *Manager) Cleanup() error { - m.mutex.Lock() - defer m.mutex.Unlock() - - for name, module := range m.modules { - if err := module.Instance.Cleanup(); err != nil { - m.logger.WithError(err).WithField("module", name).Error("Failed to cleanup module") - } - } - - m.modules = make(map[string]*Module) - m.moduleInfos = make(map[string]*ModuleInfo) - - return nil -} \ No newline at end of file diff --git a/Premium/Desktop/internal/modules/manager_test.go b/Premium/Desktop/internal/modules/manager_test.go deleted file mode 100644 index ad3ea7b79..000000000 --- a/Premium/Desktop/internal/modules/manager_test.go +++ /dev/null @@ -1,626 +0,0 @@ -package modules - -import ( - "context" - "encoding/json" - "fmt" - "os" - "path/filepath" - "testing" - "time" - - "waddlebot-bridge/internal/config" - "waddlebot-bridge/internal/testutils" -) - -func TestNewManager(t *testing.T) { - cfg := testutils.TestConfig() - storage := testutils.NewMockStorage() - - manager := NewManager(cfg, storage) - - if manager == nil { - t.Fatal("Expected non-nil manager") - } - - if manager.config != cfg { - t.Error("Expected config to be set") - } - - if manager.storage != storage { - t.Error("Expected storage to be set") - } - - if manager.modules == nil { - t.Error("Expected modules map to be initialized") - } - - if manager.moduleInfos == nil { - t.Error("Expected moduleInfos map to be initialized") - } -} - -func TestManager_GetModuleInfos(t *testing.T) { - cfg := testutils.TestConfig() - storage := testutils.NewMockStorage() - manager := NewManager(cfg, storage) - - // Test with no modules - infos := manager.GetModuleInfos() - if len(infos) != 0 { - t.Errorf("Expected 0 modules, got %d", len(infos)) - } - - // Add a test module - testModule := testutils.TestModule("test-module") - module := &Module{ - Info: testModule.GetInfo(), - Instance: testModule, - Config: make(map[string]string), - Enabled: true, - LoadedAt: time.Now(), - } - - manager.modules[testModule.GetInfo().Name] = module - manager.moduleInfos[testModule.GetInfo().Name] = testModule.GetInfo() - - // Test with one module - infos = manager.GetModuleInfos() - if len(infos) != 1 { - t.Errorf("Expected 1 module, got %d", len(infos)) - } - - if infos[0].Name != "test-module" { - t.Errorf("Expected module name 'test-module', got %s", infos[0].Name) - } -} - -func TestManager_GetModule(t *testing.T) { - cfg := testutils.TestConfig() - storage := testutils.NewMockStorage() - manager := NewManager(cfg, storage) - - // Test non-existent module - _, exists := manager.GetModule("nonexistent") - if exists { - t.Error("Expected false for non-existent module") - } - - // Add a test module - testModule := testutils.TestModule("test-module") - module := &Module{ - Info: testModule.GetInfo(), - Instance: testModule, - Config: make(map[string]string), - Enabled: true, - LoadedAt: time.Now(), - } - - manager.modules[testModule.GetInfo().Name] = module - - // Test existing module - retrievedModule, exists := manager.GetModule("test-module") - if !exists { - t.Error("Expected true for existing module") - } - - if retrievedModule.Info.Name != "test-module" { - t.Errorf("Expected module name 'test-module', got %s", retrievedModule.Info.Name) - } -} - -func TestManager_GetModuleInfo(t *testing.T) { - cfg := testutils.TestConfig() - storage := testutils.NewMockStorage() - manager := NewManager(cfg, storage) - - // Test non-existent module - _, exists := manager.GetModuleInfo("nonexistent") - if exists { - t.Error("Expected false for non-existent module") - } - - // Add a test module - testModule := testutils.TestModule("test-module") - manager.moduleInfos[testModule.GetInfo().Name] = testModule.GetInfo() - - // Test existing module - info, exists := manager.GetModuleInfo("test-module") - if !exists { - t.Error("Expected true for existing module") - } - - if info.Name != "test-module" { - t.Errorf("Expected module name 'test-module', got %s", info.Name) - } -} - -func TestManager_ExecuteAction(t *testing.T) { - cfg := testutils.TestConfig() - storage := testutils.NewMockStorage() - manager := NewManager(cfg, storage) - - // Test non-existent module - ctx, cancel := testutils.TestContext() - defer cancel() - - _, err := manager.ExecuteAction(ctx, "nonexistent", "ping", map[string]string{}) - if err == nil { - t.Error("Expected error for non-existent module") - } - - // Add a test module - testModule := testutils.TestModule("test-module") - module := &Module{ - Info: testModule.GetInfo(), - Instance: testModule, - Config: make(map[string]string), - Enabled: true, - LoadedAt: time.Now(), - } - - manager.modules[testModule.GetInfo().Name] = module - manager.moduleInfos[testModule.GetInfo().Name] = testModule.GetInfo() - - // Test successful action execution - result, err := manager.ExecuteAction(ctx, "test-module", "ping", map[string]string{}) - if err != nil { - t.Fatalf("ExecuteAction failed: %v", err) - } - - if result["message"] != "pong" { - t.Errorf("Expected message 'pong', got %v", result["message"]) - } - - // Test echo action with parameter - result, err = manager.ExecuteAction(ctx, "test-module", "echo", map[string]string{"message": "test"}) - if err != nil { - t.Fatalf("ExecuteAction failed: %v", err) - } - - if result["echo"] != "test" { - t.Errorf("Expected echo 'test', got %v", result["echo"]) - } - - // Test action failure - _, err = manager.ExecuteAction(ctx, "test-module", "fail", map[string]string{}) - if err == nil { - t.Error("Expected error for fail action") - } -} - -func TestManager_ExecuteAction_DisabledModule(t *testing.T) { - cfg := testutils.TestConfig() - storage := testutils.NewMockStorage() - manager := NewManager(cfg, storage) - - // Add a disabled test module - testModule := testutils.TestModule("test-module") - module := &Module{ - Info: testModule.GetInfo(), - Instance: testModule, - Config: make(map[string]string), - Enabled: false, // Disabled - LoadedAt: time.Now(), - } - - manager.modules[testModule.GetInfo().Name] = module - - // Test action execution on disabled module - ctx, cancel := testutils.TestContext() - defer cancel() - - _, err := manager.ExecuteAction(ctx, "test-module", "ping", map[string]string{}) - if err == nil { - t.Error("Expected error for disabled module") - } - - if err.Error() != "module test-module is disabled" { - t.Errorf("Expected 'module test-module is disabled' error, got %v", err) - } -} - -func TestManager_EnableModule(t *testing.T) { - cfg := testutils.TestConfig() - storage := testutils.NewMockStorage() - manager := NewManager(cfg, storage) - - // Test non-existent module - err := manager.EnableModule("nonexistent") - if err == nil { - t.Error("Expected error for non-existent module") - } - - // Add a disabled test module - testModule := testutils.TestModule("test-module") - module := &Module{ - Info: testModule.GetInfo(), - Instance: testModule, - Config: make(map[string]string), - Enabled: false, - LoadedAt: time.Now(), - } - - manager.modules[testModule.GetInfo().Name] = module - manager.moduleInfos[testModule.GetInfo().Name] = testModule.GetInfo() - - // Test enabling module - err = manager.EnableModule("test-module") - if err != nil { - t.Fatalf("EnableModule failed: %v", err) - } - - // Verify module is enabled - if !module.Enabled { - t.Error("Expected module to be enabled") - } - - if !module.Info.Enabled { - t.Error("Expected module info to be enabled") - } -} - -func TestManager_DisableModule(t *testing.T) { - cfg := testutils.TestConfig() - storage := testutils.NewMockStorage() - manager := NewManager(cfg, storage) - - // Test non-existent module - err := manager.DisableModule("nonexistent") - if err == nil { - t.Error("Expected error for non-existent module") - } - - // Add an enabled test module - testModule := testutils.TestModule("test-module") - module := &Module{ - Info: testModule.GetInfo(), - Instance: testModule, - Config: make(map[string]string), - Enabled: true, - LoadedAt: time.Now(), - } - - manager.modules[testModule.GetInfo().Name] = module - manager.moduleInfos[testModule.GetInfo().Name] = testModule.GetInfo() - - // Test disabling module - err = manager.DisableModule("test-module") - if err != nil { - t.Fatalf("DisableModule failed: %v", err) - } - - // Verify module is disabled - if module.Enabled { - t.Error("Expected module to be disabled") - } - - if module.Info.Enabled { - t.Error("Expected module info to be disabled") - } -} - -func TestManager_UnloadModule(t *testing.T) { - cfg := testutils.TestConfig() - storage := testutils.NewMockStorage() - manager := NewManager(cfg, storage) - - // Test non-existent module - err := manager.UnloadModule("nonexistent") - if err == nil { - t.Error("Expected error for non-existent module") - } - - // Add a test module - testModule := testutils.TestModule("test-module") - module := &Module{ - Info: testModule.GetInfo(), - Instance: testModule, - Config: make(map[string]string), - Enabled: true, - LoadedAt: time.Now(), - } - - manager.modules[testModule.GetInfo().Name] = module - manager.moduleInfos[testModule.GetInfo().Name] = testModule.GetInfo() - - // Verify module exists - _, exists := manager.GetModule("test-module") - if !exists { - t.Error("Expected module to exist before unloading") - } - - // Test unloading module - err = manager.UnloadModule("test-module") - if err != nil { - t.Fatalf("UnloadModule failed: %v", err) - } - - // Verify module no longer exists - _, exists = manager.GetModule("test-module") - if exists { - t.Error("Expected module to not exist after unloading") - } - - _, exists = manager.GetModuleInfo("test-module") - if exists { - t.Error("Expected module info to not exist after unloading") - } -} - -func TestManager_GetStats(t *testing.T) { - cfg := testutils.TestConfig() - storage := testutils.NewMockStorage() - manager := NewManager(cfg, storage) - - // Test with no modules - stats := manager.GetStats() - - if stats == nil { - t.Fatal("Expected non-nil stats") - } - - expectedTotal := 0 - expectedEnabled := 0 - expectedDisabled := 0 - - if stats["total_modules"] != expectedTotal { - t.Errorf("Expected total_modules %d, got %v", expectedTotal, stats["total_modules"]) - } - - if stats["enabled_modules"] != expectedEnabled { - t.Errorf("Expected enabled_modules %d, got %v", expectedEnabled, stats["enabled_modules"]) - } - - if stats["disabled_modules"] != expectedDisabled { - t.Errorf("Expected disabled_modules %d, got %v", expectedDisabled, stats["disabled_modules"]) - } - - // Add enabled and disabled modules - enabledModule := testutils.TestModule("enabled-module") - disabledModule := testutils.TestModule("disabled-module") - - manager.modules["enabled-module"] = &Module{ - Info: enabledModule.GetInfo(), - Instance: enabledModule, - Enabled: true, - LoadedAt: time.Now(), - } - - manager.modules["disabled-module"] = &Module{ - Info: disabledModule.GetInfo(), - Instance: disabledModule, - Enabled: false, - LoadedAt: time.Now(), - } - - // Test with modules - stats = manager.GetStats() - - expectedTotal = 2 - expectedEnabled = 1 - expectedDisabled = 1 - - if stats["total_modules"] != expectedTotal { - t.Errorf("Expected total_modules %d, got %v", expectedTotal, stats["total_modules"]) - } - - if stats["enabled_modules"] != expectedEnabled { - t.Errorf("Expected enabled_modules %d, got %v", expectedEnabled, stats["enabled_modules"]) - } - - if stats["disabled_modules"] != expectedDisabled { - t.Errorf("Expected disabled_modules %d, got %v", expectedDisabled, stats["disabled_modules"]) - } - - if stats["modules_dir"] != cfg.ModulesDir { - t.Errorf("Expected modules_dir %s, got %v", cfg.ModulesDir, stats["modules_dir"]) - } -} - -func TestManager_Cleanup(t *testing.T) { - cfg := testutils.TestConfig() - storage := testutils.NewMockStorage() - manager := NewManager(cfg, storage) - - // Add test modules - testModule1 := testutils.TestModule("test-module1") - testModule2 := testutils.TestModule("test-module2") - - manager.modules["test-module1"] = &Module{ - Info: testModule1.GetInfo(), - Instance: testModule1, - Enabled: true, - LoadedAt: time.Now(), - } - - manager.modules["test-module2"] = &Module{ - Info: testModule2.GetInfo(), - Instance: testModule2, - Enabled: true, - LoadedAt: time.Now(), - } - - manager.moduleInfos["test-module1"] = testModule1.GetInfo() - manager.moduleInfos["test-module2"] = testModule2.GetInfo() - - // Verify modules exist - if len(manager.modules) != 2 { - t.Errorf("Expected 2 modules before cleanup, got %d", len(manager.modules)) - } - - if len(manager.moduleInfos) != 2 { - t.Errorf("Expected 2 module infos before cleanup, got %d", len(manager.moduleInfos)) - } - - // Test cleanup - err := manager.Cleanup() - if err != nil { - t.Fatalf("Cleanup failed: %v", err) - } - - // Verify modules are cleaned up - if len(manager.modules) != 0 { - t.Errorf("Expected 0 modules after cleanup, got %d", len(manager.modules)) - } - - if len(manager.moduleInfos) != 0 { - t.Errorf("Expected 0 module infos after cleanup, got %d", len(manager.moduleInfos)) - } -} - -func TestManager_LoadModuleConfig(t *testing.T) { - cfg := testutils.TestConfig() - storage := testutils.NewMockStorage() - manager := NewManager(cfg, storage) - - moduleName := "test-module" - expectedConfig := map[string]string{ - "key1": "value1", - "key2": "value2", - } - - // Test non-existent config - _, err := manager.loadModuleConfig(moduleName) - if err == nil { - t.Error("Expected error for non-existent config") - } - - // Save config to storage - configData, _ := json.Marshal(expectedConfig) - storage.Set(fmt.Sprintf("module_config_%s", moduleName), configData) - - // Test loading config - config, err := manager.loadModuleConfig(moduleName) - if err != nil { - t.Fatalf("loadModuleConfig failed: %v", err) - } - - if len(config) != len(expectedConfig) { - t.Errorf("Expected %d config items, got %d", len(expectedConfig), len(config)) - } - - for key, expectedValue := range expectedConfig { - if config[key] != expectedValue { - t.Errorf("Expected config[%s] = %s, got %s", key, expectedValue, config[key]) - } - } -} - -func TestManager_SaveModuleInfo(t *testing.T) { - cfg := testutils.TestConfig() - storage := testutils.NewMockStorage() - manager := NewManager(cfg, storage) - - testModule := testutils.TestModule("test-module") - info := testModule.GetInfo() - - // Test saving module info - err := manager.saveModuleInfo(info) - if err != nil { - t.Fatalf("saveModuleInfo failed: %v", err) - } - - // Verify info was saved - key := fmt.Sprintf("module_info_%s", info.Name) - data, err := storage.Get(key) - if err != nil { - t.Fatalf("Failed to get saved module info: %v", err) - } - - var savedInfo ModuleInfo - err = json.Unmarshal(data, &savedInfo) - if err != nil { - t.Fatalf("Failed to unmarshal saved info: %v", err) - } - - if savedInfo.Name != info.Name { - t.Errorf("Expected saved name %s, got %s", info.Name, savedInfo.Name) - } - - if savedInfo.Version != info.Version { - t.Errorf("Expected saved version %s, got %s", info.Version, savedInfo.Version) - } -} - -func TestManager_ExecuteAction_Timeout(t *testing.T) { - cfg := testutils.TestConfig() - cfg.ModuleTimeout = 1 // 1 second timeout - storage := testutils.NewMockStorage() - manager := NewManager(cfg, storage) - - // Create a test module with a slow action - testModule := testutils.NewMockModule("slow-module") - testModule.AddAction("slow", func(ctx context.Context, parameters map[string]string) (map[string]interface{}, error) { - // Sleep longer than timeout - time.Sleep(2 * time.Second) - return map[string]interface{}{"result": "done"}, nil - }) - - module := &Module{ - Info: testModule.GetInfo(), - Instance: testModule, - Config: make(map[string]string), - Enabled: true, - LoadedAt: time.Now(), - } - - manager.modules[testModule.GetInfo().Name] = module - - // Test timeout - ctx, cancel := testutils.TestContext() - defer cancel() - - _, err := manager.ExecuteAction(ctx, "slow-module", "slow", map[string]string{}) - if err == nil { - t.Error("Expected timeout error") - } -} - -func TestManager_ConcurrentExecution(t *testing.T) { - cfg := testutils.TestConfig() - storage := testutils.NewMockStorage() - manager := NewManager(cfg, storage) - - // Add a test module - testModule := testutils.TestModule("test-module") - module := &Module{ - Info: testModule.GetInfo(), - Instance: testModule, - Config: make(map[string]string), - Enabled: true, - LoadedAt: time.Now(), - } - - manager.modules[testModule.GetInfo().Name] = module - manager.moduleInfos[testModule.GetInfo().Name] = testModule.GetInfo() - - // Test concurrent execution - numGoroutines := 10 - done := make(chan bool) - - for i := 0; i < numGoroutines; i++ { - go func() { - defer func() { done <- true }() - - ctx, cancel := testutils.TestContext() - defer cancel() - - result, err := manager.ExecuteAction(ctx, "test-module", "ping", map[string]string{}) - if err != nil { - t.Errorf("Concurrent ExecuteAction failed: %v", err) - return - } - - if result["message"] != "pong" { - t.Errorf("Expected message 'pong', got %v", result["message"]) - } - }() - } - - // Wait for all goroutines to complete - for i := 0; i < numGoroutines; i++ { - <-done - } -} - diff --git a/Premium/Desktop/internal/obs/client.go b/Premium/Desktop/internal/obs/client.go deleted file mode 100644 index 78128d899..000000000 --- a/Premium/Desktop/internal/obs/client.go +++ /dev/null @@ -1,456 +0,0 @@ -package obs - -import ( - "context" - "fmt" - "sync" - "time" - - "github.com/andreykaipov/goobs" - "github.com/google/uuid" - "github.com/sirupsen/logrus" -) - -// Client manages the OBS WebSocket connection -type Client struct { - config Config - client *goobs.Client - logger *logrus.Logger - state ConnectionState - stateMux sync.RWMutex - connInfo ConnectionInfo - connInfoMux sync.RWMutex - - // Event handling - eventCallbacks map[SubscriptionID]eventSubscription - callbackMux sync.RWMutex - - // Reconnection - reconnectChan chan struct{} - stopReconnect chan struct{} - - // Lifecycle - ctx context.Context - cancel context.CancelFunc - wg sync.WaitGroup -} - -// eventSubscription holds callback and filter info -type eventSubscription struct { - callback EventCallback - eventTypes []EventType // empty = all events -} - -// NewClient creates a new OBS client with the given configuration -func NewClient(cfg Config, logger *logrus.Logger) *Client { - if logger == nil { - logger = logrus.New() - } - - ctx, cancel := context.WithCancel(context.Background()) - - return &Client{ - config: cfg, - logger: logger, - state: StateDisconnected, - eventCallbacks: make(map[SubscriptionID]eventSubscription), - reconnectChan: make(chan struct{}, 1), - stopReconnect: make(chan struct{}), - ctx: ctx, - cancel: cancel, - connInfo: ConnectionInfo{ - State: StateDisconnected, - }, - } -} - -// Connect establishes a connection to OBS -func (c *Client) Connect(ctx context.Context) error { - c.stateMux.Lock() - if c.state == StateConnected { - c.stateMux.Unlock() - return nil - } - c.setState(StateConnecting) - c.stateMux.Unlock() - - c.logger.WithFields(logrus.Fields{ - "host": c.config.Host, - "port": c.config.Port, - }).Info("Connecting to OBS") - - // Build connection options - addr := fmt.Sprintf("%s:%d", c.config.Host, c.config.Port) - opts := []goobs.Option{} - - if c.config.Password != "" { - opts = append(opts, goobs.WithPassword(c.config.Password)) - } - - // Create connection with timeout - connectCtx, cancel := context.WithTimeout(ctx, c.config.Timeout) - defer cancel() - - // Channel to receive connection result - type connResult struct { - client *goobs.Client - err error - } - resultCh := make(chan connResult, 1) - - go func() { - client, err := goobs.New(addr, opts...) - resultCh <- connResult{client: client, err: err} - }() - - select { - case <-connectCtx.Done(): - c.setStateAndError(StateDisconnected, "connection timeout") - return ErrTimeout - case result := <-resultCh: - if result.err != nil { - c.setStateAndError(StateDisconnected, result.err.Error()) - return NewOBSError(ErrConnectionFailed, result.err.Error()) - } - c.client = result.client - } - - // Get version info - version, err := c.client.General.GetVersion() - if err != nil { - c.logger.WithError(err).Warn("Failed to get OBS version") - } else { - c.connInfoMux.Lock() - c.connInfo.OBSVersion = version.ObsVersion - c.connInfo.WebSocketVersion = version.ObsWebSocketVersion - c.connInfo.Platform = version.Platform - c.connInfoMux.Unlock() - } - - // Update connection state - now := time.Now() - c.connInfoMux.Lock() - c.connInfo.ConnectedAt = &now - c.connInfo.DisconnectedAt = nil - c.connInfo.ReconnectAttempts = 0 - c.connInfo.LastError = "" - c.connInfoMux.Unlock() - - c.setState(StateConnected) - c.logger.WithFields(logrus.Fields{ - "obs_version": c.connInfo.OBSVersion, - "ws_version": c.connInfo.WebSocketVersion, - }).Info("Connected to OBS") - - // Start event listener if auto-reconnect is enabled - if c.config.AutoReconnect { - c.wg.Add(1) - go c.monitorConnection() - } - - // Emit connected event - c.emitEvent(Event{ - Type: EventType("connected"), - Timestamp: time.Now(), - Data: map[string]interface{}{ - "obs_version": c.connInfo.OBSVersion, - "ws_version": c.connInfo.WebSocketVersion, - }, - }) - - return nil -} - -// Disconnect closes the connection to OBS -func (c *Client) Disconnect() error { - c.stateMux.Lock() - if c.state == StateDisconnected { - c.stateMux.Unlock() - return nil - } - c.stateMux.Unlock() - - c.logger.Info("Disconnecting from OBS") - - // Stop reconnection attempts - select { - case c.stopReconnect <- struct{}{}: - default: - } - - // Close the client - if c.client != nil { - if err := c.client.Disconnect(); err != nil { - c.logger.WithError(err).Warn("Error disconnecting from OBS") - } - c.client = nil - } - - // Update state - now := time.Now() - c.connInfoMux.Lock() - c.connInfo.DisconnectedAt = &now - c.connInfoMux.Unlock() - - c.setState(StateDisconnected) - - // Emit disconnected event - c.emitEvent(Event{ - Type: EventType("disconnected"), - Timestamp: time.Now(), - Data: map[string]interface{}{ - "reason": "manual_disconnect", - }, - }) - - return nil -} - -// Close shuts down the client completely -func (c *Client) Close() error { - c.cancel() - if err := c.Disconnect(); err != nil { - c.logger.WithError(err).Warn("Error during close disconnect") - } - c.wg.Wait() - return nil -} - -// GetState returns the current connection state -func (c *Client) GetState() ConnectionState { - c.stateMux.RLock() - defer c.stateMux.RUnlock() - return c.state -} - -// IsConnected returns true if connected to OBS -func (c *Client) IsConnected() bool { - return c.GetState() == StateConnected -} - -// GetConnectionInfo returns detailed connection information -func (c *Client) GetConnectionInfo() ConnectionInfo { - c.connInfoMux.RLock() - defer c.connInfoMux.RUnlock() - info := c.connInfo - info.State = c.GetState() - return info -} - -// GetClient returns the underlying goobs client (for advanced operations) -func (c *Client) GetClient() *goobs.Client { - c.stateMux.RLock() - defer c.stateMux.RUnlock() - return c.client -} - -// Subscribe registers a callback for OBS events -func (c *Client) Subscribe(callback EventCallback, eventTypes ...EventType) SubscriptionID { - c.callbackMux.Lock() - defer c.callbackMux.Unlock() - - id := SubscriptionID(uuid.New().String()) - c.eventCallbacks[id] = eventSubscription{ - callback: callback, - eventTypes: eventTypes, - } - - c.logger.WithFields(logrus.Fields{ - "subscription_id": id, - "event_types": eventTypes, - }).Debug("Registered event subscription") - - return id -} - -// Unsubscribe removes an event subscription -func (c *Client) Unsubscribe(id SubscriptionID) { - c.callbackMux.Lock() - defer c.callbackMux.Unlock() - - if _, exists := c.eventCallbacks[id]; exists { - delete(c.eventCallbacks, id) - c.logger.WithField("subscription_id", id).Debug("Removed event subscription") - } -} - -// setState updates the connection state -func (c *Client) setState(state ConnectionState) { - c.stateMux.Lock() - oldState := c.state - c.state = state - c.stateMux.Unlock() - - c.connInfoMux.Lock() - c.connInfo.State = state - c.connInfoMux.Unlock() - - if oldState != state { - c.logger.WithFields(logrus.Fields{ - "old_state": oldState.String(), - "new_state": state.String(), - }).Debug("Connection state changed") - } -} - -// setStateAndError updates the connection state and last error -func (c *Client) setStateAndError(state ConnectionState, errMsg string) { - c.setState(state) - c.connInfoMux.Lock() - c.connInfo.LastError = errMsg - c.connInfoMux.Unlock() -} - -// monitorConnection monitors the connection and triggers reconnection -func (c *Client) monitorConnection() { - defer c.wg.Done() - - ticker := time.NewTicker(5 * time.Second) - defer ticker.Stop() - - for { - select { - case <-c.ctx.Done(): - return - case <-c.stopReconnect: - return - case <-ticker.C: - if c.GetState() == StateConnected && c.client != nil { - // Ping to check connection - _, err := c.client.General.GetVersion() - if err != nil { - c.logger.WithError(err).Warn("Connection lost, attempting reconnect") - c.handleDisconnect() - } - } - } - } -} - -// handleDisconnect handles unexpected disconnection -func (c *Client) handleDisconnect() { - now := time.Now() - c.connInfoMux.Lock() - c.connInfo.DisconnectedAt = &now - c.connInfoMux.Unlock() - - c.setState(StateReconnecting) - - // Emit disconnected event - c.emitEvent(Event{ - Type: EventType("disconnected"), - Timestamp: time.Now(), - Data: map[string]interface{}{ - "reason": "connection_lost", - }, - }) - - // Start reconnection attempts - go c.attemptReconnect() -} - -// attemptReconnect tries to reconnect with exponential backoff -func (c *Client) attemptReconnect() { - interval := c.config.ReconnectInterval - attempts := 0 - - for { - select { - case <-c.ctx.Done(): - return - case <-c.stopReconnect: - return - default: - } - - attempts++ - c.connInfoMux.Lock() - c.connInfo.ReconnectAttempts = attempts - c.connInfoMux.Unlock() - - c.logger.WithFields(logrus.Fields{ - "attempt": attempts, - "interval": interval, - }).Info("Attempting to reconnect to OBS") - - // Try to connect - ctx, cancel := context.WithTimeout(c.ctx, c.config.Timeout) - err := c.Connect(ctx) - cancel() - - if err == nil { - c.logger.Info("Reconnected to OBS successfully") - c.emitEvent(Event{ - Type: EventType("reconnected"), - Timestamp: time.Now(), - Data: map[string]interface{}{ - "attempts": attempts, - }, - }) - return - } - - c.logger.WithError(err).WithField("attempt", attempts).Warn("Reconnection failed") - - // Wait before next attempt with exponential backoff - select { - case <-c.ctx.Done(): - return - case <-c.stopReconnect: - return - case <-time.After(interval): - } - - // Exponential backoff - interval = interval * 2 - if interval > c.config.MaxReconnectInterval { - interval = c.config.MaxReconnectInterval - } - } -} - -// emitEvent sends an event to all registered callbacks -func (c *Client) emitEvent(event Event) { - c.callbackMux.RLock() - defer c.callbackMux.RUnlock() - - for _, sub := range c.eventCallbacks { - // Check if subscription is for all events or specific event types - if len(sub.eventTypes) == 0 { - go sub.callback(event) - } else { - for _, et := range sub.eventTypes { - if et == event.Type { - go sub.callback(event) - break - } - } - } - } -} - -// GetStats returns current OBS statistics -func (c *Client) GetStats(ctx context.Context) (*OBSStats, error) { - if !c.IsConnected() { - return nil, ErrNotConnected - } - - stats, err := c.client.General.GetStats() - if err != nil { - return nil, NewOBSError(ErrOperationFailed, err.Error()) - } - - return &OBSStats{ - CPUUsage: stats.CpuUsage, - MemoryUsage: stats.MemoryUsage, - FreeDiskSpace: stats.AvailableDiskSpace, - ActiveFPS: stats.ActiveFps, - AverageFrameTime: stats.AverageFrameRenderTime, - RenderSkippedFrames: int64(stats.RenderSkippedFrames), - RenderTotalFrames: int64(stats.RenderTotalFrames), - OutputSkippedFrames: int64(stats.OutputSkippedFrames), - OutputTotalFrames: int64(stats.OutputTotalFrames), - WebSocketSessionIncomingMessages: int64(stats.WebSocketSessionIncomingMessages), - WebSocketSessionOutgoingMessages: int64(stats.WebSocketSessionOutgoingMessages), - }, nil -} diff --git a/Premium/Desktop/internal/obs/events.go b/Premium/Desktop/internal/obs/events.go deleted file mode 100644 index cb038f292..000000000 --- a/Premium/Desktop/internal/obs/events.go +++ /dev/null @@ -1,268 +0,0 @@ -package obs - -import ( - "time" - - "github.com/andreykaipov/goobs/api/events" -) - -// StartEventListener starts listening for OBS events -// This should be called after a successful connection -func (c *Client) StartEventListener() error { - if !c.IsConnected() { - return ErrNotConnected - } - - // Subscribe to all event categories using a callback - c.client.Listen(func(event any) { - c.handleOBSEvent(event) - }) - - c.logger.Info("Started OBS event listener") - return nil -} - - -// handleOBSEvent converts goobs events to our Event type and dispatches them -func (c *Client) handleOBSEvent(event interface{}) { - var ev Event - ev.Timestamp = time.Now() - ev.Data = make(map[string]interface{}) - - switch e := event.(type) { - // Scene events - case *events.CurrentProgramSceneChanged: - ev.Type = EventSceneChanged - ev.Data["scene_name"] = e.SceneName - case *events.SceneListChanged: - ev.Type = EventSceneListChanged - ev.Data["scenes"] = e.Scenes - case *events.SceneNameChanged: - ev.Type = EventSceneNameChanged - ev.Data["old_name"] = e.OldSceneName - ev.Data["new_name"] = e.SceneName - case *events.SceneCreated: - ev.Type = EventSceneCreated - ev.Data["scene_name"] = e.SceneName - ev.Data["is_group"] = e.IsGroup - case *events.SceneRemoved: - ev.Type = EventSceneRemoved - ev.Data["scene_name"] = e.SceneName - ev.Data["is_group"] = e.IsGroup - - // Source/Scene item events - case *events.SceneItemEnableStateChanged: - ev.Type = EventSourceVisibilityChanged - ev.Data["scene_name"] = e.SceneName - ev.Data["item_id"] = e.SceneItemId - ev.Data["enabled"] = e.SceneItemEnabled - case *events.SceneItemLockStateChanged: - ev.Type = EventSourceLockChanged - ev.Data["scene_name"] = e.SceneName - ev.Data["item_id"] = e.SceneItemId - ev.Data["locked"] = e.SceneItemLocked - case *events.SceneItemTransformChanged: - ev.Type = EventSourceTransformChanged - ev.Data["scene_name"] = e.SceneName - ev.Data["item_id"] = e.SceneItemId - ev.Data["transform"] = e.SceneItemTransform - case *events.SceneItemCreated: - ev.Type = EventSourceCreated - ev.Data["scene_name"] = e.SceneName - ev.Data["source_name"] = e.SourceName - ev.Data["item_id"] = e.SceneItemId - case *events.SceneItemRemoved: - ev.Type = EventSourceRemoved - ev.Data["scene_name"] = e.SceneName - ev.Data["source_name"] = e.SourceName - ev.Data["item_id"] = e.SceneItemId - case *events.InputNameChanged: - ev.Type = EventSourceRenamed - ev.Data["old_name"] = e.OldInputName - ev.Data["new_name"] = e.InputName - - // Filter events - case *events.SourceFilterEnableStateChanged: - if e.FilterEnabled { - ev.Type = EventFilterEnabled - } else { - ev.Type = EventFilterDisabled - } - ev.Data["source_name"] = e.SourceName - ev.Data["filter_name"] = e.FilterName - ev.Data["enabled"] = e.FilterEnabled - case *events.SourceFilterListReindexed: - ev.Type = EventFilterListChanged - ev.Data["source_name"] = e.SourceName - ev.Data["filters"] = e.Filters - case *events.SourceFilterNameChanged: - ev.Type = EventFilterNameChanged - ev.Data["source_name"] = e.SourceName - ev.Data["old_name"] = e.OldFilterName - ev.Data["new_name"] = e.FilterName - case *events.SourceFilterCreated: - ev.Type = EventFilterCreated - ev.Data["source_name"] = e.SourceName - ev.Data["filter_name"] = e.FilterName - ev.Data["filter_kind"] = e.FilterKind - case *events.SourceFilterRemoved: - ev.Type = EventFilterRemoved - ev.Data["source_name"] = e.SourceName - ev.Data["filter_name"] = e.FilterName - - // Stream events - case *events.StreamStateChanged: - if e.OutputActive { - ev.Type = EventStreamStarted - } else { - ev.Type = EventStreamStopped - } - ev.Data["active"] = e.OutputActive - ev.Data["state"] = e.OutputState - - // Recording events - case *events.RecordStateChanged: - switch e.OutputState { - case "OBS_WEBSOCKET_OUTPUT_STARTING": - ev.Type = EventRecordingStarting - case "OBS_WEBSOCKET_OUTPUT_STARTED": - ev.Type = EventRecordingStarted - case "OBS_WEBSOCKET_OUTPUT_STOPPING": - ev.Type = EventRecordingStopping - case "OBS_WEBSOCKET_OUTPUT_STOPPED": - ev.Type = EventRecordingStopped - case "OBS_WEBSOCKET_OUTPUT_PAUSED": - ev.Type = EventRecordingPaused - case "OBS_WEBSOCKET_OUTPUT_RESUMED": - ev.Type = EventRecordingResumed - default: - return // Unknown state, skip - } - ev.Data["active"] = e.OutputActive - ev.Data["state"] = e.OutputState - ev.Data["output_path"] = e.OutputPath - - // General events - case *events.ExitStarted: - ev.Type = EventExiting - case *events.StudioModeStateChanged: - ev.Type = EventStudioModeChanged - ev.Data["enabled"] = e.StudioModeEnabled - - default: - // Unknown event type, skip - return - } - - c.emitEvent(ev) -} - -// GetAvailableEventTypes returns all supported event types -func GetAvailableEventTypes() []EventType { - return []EventType{ - // Scene events - EventSceneChanged, - EventSceneListChanged, - EventSceneNameChanged, - EventSceneCreated, - EventSceneRemoved, - - // Source events - EventSourceVisibilityChanged, - EventSourceLockChanged, - EventSourceTransformChanged, - EventSourceCreated, - EventSourceRemoved, - EventSourceRenamed, - - // Filter events - EventFilterEnabled, - EventFilterDisabled, - EventFilterListChanged, - EventFilterNameChanged, - EventFilterCreated, - EventFilterRemoved, - - // Streaming events - EventStreamStarting, - EventStreamStarted, - EventStreamStopping, - EventStreamStopped, - EventStreamReconnect, - - // Recording events - EventRecordingStarting, - EventRecordingStarted, - EventRecordingStopping, - EventRecordingStopped, - EventRecordingPaused, - EventRecordingResumed, - - // General events - EventExiting, - EventStudioModeChanged, - } -} - -// SubscribeAll subscribes to all OBS events -func (c *Client) SubscribeAll(callback EventCallback) SubscriptionID { - return c.Subscribe(callback) -} - -// SubscribeSceneEvents subscribes to scene-related events -func (c *Client) SubscribeSceneEvents(callback EventCallback) SubscriptionID { - return c.Subscribe(callback, - EventSceneChanged, - EventSceneListChanged, - EventSceneNameChanged, - EventSceneCreated, - EventSceneRemoved, - ) -} - -// SubscribeSourceEvents subscribes to source-related events -func (c *Client) SubscribeSourceEvents(callback EventCallback) SubscriptionID { - return c.Subscribe(callback, - EventSourceVisibilityChanged, - EventSourceLockChanged, - EventSourceTransformChanged, - EventSourceCreated, - EventSourceRemoved, - EventSourceRenamed, - ) -} - -// SubscribeFilterEvents subscribes to filter-related events -func (c *Client) SubscribeFilterEvents(callback EventCallback) SubscriptionID { - return c.Subscribe(callback, - EventFilterEnabled, - EventFilterDisabled, - EventFilterListChanged, - EventFilterNameChanged, - EventFilterCreated, - EventFilterRemoved, - ) -} - -// SubscribeStreamEvents subscribes to streaming-related events -func (c *Client) SubscribeStreamEvents(callback EventCallback) SubscriptionID { - return c.Subscribe(callback, - EventStreamStarting, - EventStreamStarted, - EventStreamStopping, - EventStreamStopped, - EventStreamReconnect, - ) -} - -// SubscribeRecordingEvents subscribes to recording-related events -func (c *Client) SubscribeRecordingEvents(callback EventCallback) SubscriptionID { - return c.Subscribe(callback, - EventRecordingStarting, - EventRecordingStarted, - EventRecordingStopping, - EventRecordingStopped, - EventRecordingPaused, - EventRecordingResumed, - ) -} diff --git a/Premium/Desktop/internal/obs/filters.go b/Premium/Desktop/internal/obs/filters.go deleted file mode 100644 index 1bc3e7bbd..000000000 --- a/Premium/Desktop/internal/obs/filters.go +++ /dev/null @@ -1,220 +0,0 @@ -package obs - -import ( - "context" - - "github.com/andreykaipov/goobs/api/requests/filters" -) - -// GetSourceFilters returns all filters for a source -func (c *Client) GetSourceFilters(ctx context.Context, sourceName string) ([]FilterInfo, error) { - if !c.IsConnected() { - return nil, ErrNotConnected - } - - resp, err := c.client.Filters.GetSourceFilterList(&filters.GetSourceFilterListParams{ - SourceName: &sourceName, - }) - if err != nil { - return nil, NewOBSError(ErrOperationFailed, err.Error()) - } - - filterList := make([]FilterInfo, len(resp.Filters)) - for i, f := range resp.Filters { - filterList[i] = FilterInfo{ - Name: f.FilterName, - Type: f.FilterKind, - Index: f.FilterIndex, - Enabled: f.FilterEnabled, - Settings: f.FilterSettings, - } - } - - return filterList, nil -} - -// GetFilter returns a specific filter on a source -func (c *Client) GetFilter(ctx context.Context, sourceName, filterName string) (*FilterInfo, error) { - if !c.IsConnected() { - return nil, ErrNotConnected - } - - resp, err := c.client.Filters.GetSourceFilter(&filters.GetSourceFilterParams{ - SourceName: &sourceName, - FilterName: &filterName, - }) - if err != nil { - return nil, NewOBSError(ErrFilterNotFound, err.Error()) - } - - return &FilterInfo{ - Name: filterName, - Type: resp.FilterKind, - Index: resp.FilterIndex, - Enabled: resp.FilterEnabled, - Settings: resp.FilterSettings, - }, nil -} - -// SetFilterEnabled enables or disables a filter -func (c *Client) SetFilterEnabled(ctx context.Context, sourceName, filterName string, enabled bool) error { - if !c.IsConnected() { - return ErrNotConnected - } - - _, err := c.client.Filters.SetSourceFilterEnabled(&filters.SetSourceFilterEnabledParams{ - SourceName: &sourceName, - FilterName: &filterName, - FilterEnabled: &enabled, - }) - if err != nil { - return NewOBSError(ErrOperationFailed, err.Error()) - } - - c.logger.WithFields(map[string]interface{}{ - "source": sourceName, - "filter": filterName, - "enabled": enabled, - }).Debug("Set filter enabled state") - - return nil -} - -// SetFilterSettings updates the settings of a filter -func (c *Client) SetFilterSettings(ctx context.Context, sourceName, filterName string, settings map[string]interface{}) error { - if !c.IsConnected() { - return ErrNotConnected - } - - overlay := true - _, err := c.client.Filters.SetSourceFilterSettings(&filters.SetSourceFilterSettingsParams{ - SourceName: &sourceName, - FilterName: &filterName, - FilterSettings: settings, - Overlay: &overlay, - }) - if err != nil { - return NewOBSError(ErrOperationFailed, err.Error()) - } - - c.logger.WithFields(map[string]interface{}{ - "source": sourceName, - "filter": filterName, - "settings": settings, - }).Debug("Updated filter settings") - - return nil -} - -// SetFilterIndex changes the order/index of a filter -func (c *Client) SetFilterIndex(ctx context.Context, sourceName, filterName string, index int) error { - if !c.IsConnected() { - return ErrNotConnected - } - - _, err := c.client.Filters.SetSourceFilterIndex(&filters.SetSourceFilterIndexParams{ - SourceName: &sourceName, - FilterName: &filterName, - FilterIndex: &index, - }) - if err != nil { - return NewOBSError(ErrOperationFailed, err.Error()) - } - - c.logger.WithFields(map[string]interface{}{ - "source": sourceName, - "filter": filterName, - "index": index, - }).Debug("Set filter index") - - return nil -} - -// CreateFilter creates a new filter on a source -func (c *Client) CreateFilter(ctx context.Context, sourceName, filterName, filterKind string, settings map[string]interface{}) error { - if !c.IsConnected() { - return ErrNotConnected - } - - _, err := c.client.Filters.CreateSourceFilter(&filters.CreateSourceFilterParams{ - SourceName: &sourceName, - FilterName: &filterName, - FilterKind: &filterKind, - FilterSettings: settings, - }) - if err != nil { - return NewOBSError(ErrOperationFailed, err.Error()) - } - - c.logger.WithFields(map[string]interface{}{ - "source": sourceName, - "filter": filterName, - "kind": filterKind, - }).Info("Created filter") - - return nil -} - -// RemoveFilter removes a filter from a source -func (c *Client) RemoveFilter(ctx context.Context, sourceName, filterName string) error { - if !c.IsConnected() { - return ErrNotConnected - } - - _, err := c.client.Filters.RemoveSourceFilter(&filters.RemoveSourceFilterParams{ - SourceName: &sourceName, - FilterName: &filterName, - }) - if err != nil { - return NewOBSError(ErrOperationFailed, err.Error()) - } - - c.logger.WithFields(map[string]interface{}{ - "source": sourceName, - "filter": filterName, - }).Info("Removed filter") - - return nil -} - -// RenameFilter renames a filter -func (c *Client) RenameFilter(ctx context.Context, sourceName, oldFilterName, newFilterName string) error { - if !c.IsConnected() { - return ErrNotConnected - } - - _, err := c.client.Filters.SetSourceFilterName(&filters.SetSourceFilterNameParams{ - SourceName: &sourceName, - FilterName: &oldFilterName, - NewFilterName: &newFilterName, - }) - if err != nil { - return NewOBSError(ErrOperationFailed, err.Error()) - } - - c.logger.WithFields(map[string]interface{}{ - "source": sourceName, - "old_name": oldFilterName, - "new_name": newFilterName, - }).Info("Renamed filter") - - return nil -} - -// ToggleFilter toggles the enabled state of a filter -func (c *Client) ToggleFilter(ctx context.Context, sourceName, filterName string) (bool, error) { - // Get current state - filter, err := c.GetFilter(ctx, sourceName, filterName) - if err != nil { - return false, err - } - - // Toggle it - newState := !filter.Enabled - err = c.SetFilterEnabled(ctx, sourceName, filterName, newState) - if err != nil { - return false, err - } - - return newState, nil -} diff --git a/Premium/Desktop/internal/obs/recording.go b/Premium/Desktop/internal/obs/recording.go deleted file mode 100644 index b790329f5..000000000 --- a/Premium/Desktop/internal/obs/recording.go +++ /dev/null @@ -1,191 +0,0 @@ -package obs - -import ( - "context" - "time" -) - -// GetRecordingStatus returns the current recording status -func (c *Client) GetRecordingStatus(ctx context.Context) (*RecordingStatus, error) { - if !c.IsConnected() { - return nil, ErrNotConnected - } - - resp, err := c.client.Record.GetRecordStatus() - if err != nil { - return nil, NewOBSError(ErrOperationFailed, err.Error()) - } - - return &RecordingStatus{ - Active: resp.OutputActive, - Paused: resp.OutputPaused, - TimecodeString: resp.OutputTimecode, - Duration: time.Duration(resp.OutputDuration) * time.Millisecond, - BytesWritten: int64(resp.OutputBytes), - OutputPath: "", // Not returned by GetRecordStatus - }, nil -} - -// StartRecording starts recording -func (c *Client) StartRecording(ctx context.Context) error { - if !c.IsConnected() { - return ErrNotConnected - } - - _, err := c.client.Record.StartRecord() - if err != nil { - return NewOBSError(ErrOperationFailed, err.Error()) - } - - c.logger.Info("Started recording") - - // Emit event - c.emitEvent(Event{ - Type: EventRecordingStarted, - Timestamp: time.Now(), - }) - - return nil -} - -// StopRecording stops recording and returns the output path -func (c *Client) StopRecording(ctx context.Context) (string, error) { - if !c.IsConnected() { - return "", ErrNotConnected - } - - resp, err := c.client.Record.StopRecord() - if err != nil { - return "", NewOBSError(ErrOperationFailed, err.Error()) - } - - c.logger.WithField("output_path", resp.OutputPath).Info("Stopped recording") - - // Emit event - c.emitEvent(Event{ - Type: EventRecordingStopped, - Timestamp: time.Now(), - Data: map[string]interface{}{ - "output_path": resp.OutputPath, - }, - }) - - return resp.OutputPath, nil -} - -// ToggleRecording toggles the recording state -func (c *Client) ToggleRecording(ctx context.Context) error { - if !c.IsConnected() { - return ErrNotConnected - } - - _, err := c.client.Record.ToggleRecord() - if err != nil { - return NewOBSError(ErrOperationFailed, err.Error()) - } - - c.logger.Info("Toggled recording") - - return nil -} - -// PauseRecording pauses the current recording -func (c *Client) PauseRecording(ctx context.Context) error { - if !c.IsConnected() { - return ErrNotConnected - } - - _, err := c.client.Record.PauseRecord() - if err != nil { - return NewOBSError(ErrOperationFailed, err.Error()) - } - - c.logger.Info("Paused recording") - - // Emit event - c.emitEvent(Event{ - Type: EventRecordingPaused, - Timestamp: time.Now(), - }) - - return nil -} - -// ResumeRecording resumes a paused recording -func (c *Client) ResumeRecording(ctx context.Context) error { - if !c.IsConnected() { - return ErrNotConnected - } - - _, err := c.client.Record.ResumeRecord() - if err != nil { - return NewOBSError(ErrOperationFailed, err.Error()) - } - - c.logger.Info("Resumed recording") - - // Emit event - c.emitEvent(Event{ - Type: EventRecordingResumed, - Timestamp: time.Now(), - }) - - return nil -} - -// ToggleRecordingPause toggles the recording pause state -func (c *Client) ToggleRecordingPause(ctx context.Context) (bool, error) { - if !c.IsConnected() { - return false, ErrNotConnected - } - - resp, err := c.client.Record.ToggleRecordPause() - if err != nil { - return false, NewOBSError(ErrOperationFailed, err.Error()) - } - - c.logger.WithField("paused", resp.OutputPaused).Info("Toggled recording pause") - - return resp.OutputPaused, nil -} - -// IsRecording returns true if currently recording -func (c *Client) IsRecording(ctx context.Context) (bool, error) { - status, err := c.GetRecordingStatus(ctx) - if err != nil { - return false, err - } - return status.Active, nil -} - -// IsRecordingPaused returns true if recording is paused -func (c *Client) IsRecordingPaused(ctx context.Context) (bool, error) { - status, err := c.GetRecordingStatus(ctx) - if err != nil { - return false, err - } - return status.Paused, nil -} - -// GetRecordingDuration returns the current recording duration -func (c *Client) GetRecordingDuration(ctx context.Context) (time.Duration, error) { - status, err := c.GetRecordingStatus(ctx) - if err != nil { - return 0, err - } - return status.Duration, nil -} - -// GetRecordDirectory returns the recording output directory -func (c *Client) GetRecordDirectory(ctx context.Context) (string, error) { - if !c.IsConnected() { - return "", ErrNotConnected - } - - resp, err := c.client.Config.GetRecordDirectory() - if err != nil { - return "", NewOBSError(ErrOperationFailed, err.Error()) - } - - return resp.RecordDirectory, nil -} diff --git a/Premium/Desktop/internal/obs/scenes.go b/Premium/Desktop/internal/obs/scenes.go deleted file mode 100644 index 7013409c8..000000000 --- a/Premium/Desktop/internal/obs/scenes.go +++ /dev/null @@ -1,236 +0,0 @@ -package obs - -import ( - "context" - - "github.com/andreykaipov/goobs/api/requests/scenes" - "github.com/andreykaipov/goobs/api/requests/ui" -) - -// GetScenes returns the list of all scenes -func (c *Client) GetScenes(ctx context.Context) ([]SceneInfo, error) { - if !c.IsConnected() { - return nil, ErrNotConnected - } - - // Get scene list - resp, err := c.client.Scenes.GetSceneList() - if err != nil { - return nil, NewOBSError(ErrOperationFailed, err.Error()) - } - - scenes := make([]SceneInfo, len(resp.Scenes)) - for i, s := range resp.Scenes { - scenes[i] = SceneInfo{ - Name: s.SceneName, - Index: i, - IsCurrent: s.SceneName == resp.CurrentProgramSceneName, - IsPreview: s.SceneName == resp.CurrentPreviewSceneName, - } - } - - return scenes, nil -} - -// GetCurrentScene returns the current program scene -func (c *Client) GetCurrentScene(ctx context.Context) (*SceneInfo, error) { - if !c.IsConnected() { - return nil, ErrNotConnected - } - - resp, err := c.client.Scenes.GetCurrentProgramScene() - if err != nil { - return nil, NewOBSError(ErrOperationFailed, err.Error()) - } - - return &SceneInfo{ - Name: resp.CurrentProgramSceneName, - IsCurrent: true, - }, nil -} - -// GetPreviewScene returns the current preview scene (studio mode only) -func (c *Client) GetPreviewScene(ctx context.Context) (*SceneInfo, error) { - if !c.IsConnected() { - return nil, ErrNotConnected - } - - resp, err := c.client.Scenes.GetCurrentPreviewScene() - if err != nil { - return nil, NewOBSError(ErrOperationFailed, err.Error()) - } - - return &SceneInfo{ - Name: resp.CurrentPreviewSceneName, - IsPreview: true, - }, nil -} - -// SetCurrentScene switches to the specified scene -func (c *Client) SetCurrentScene(ctx context.Context, sceneName string) error { - if !c.IsConnected() { - return ErrNotConnected - } - - _, err := c.client.Scenes.SetCurrentProgramScene(&scenes.SetCurrentProgramSceneParams{ - SceneName: &sceneName, - }) - if err != nil { - return NewOBSError(ErrOperationFailed, err.Error()) - } - - c.logger.WithField("scene", sceneName).Info("Switched to scene") - return nil -} - -// SetPreviewScene sets the preview scene (studio mode only) -func (c *Client) SetPreviewScene(ctx context.Context, sceneName string) error { - if !c.IsConnected() { - return ErrNotConnected - } - - _, err := c.client.Scenes.SetCurrentPreviewScene(&scenes.SetCurrentPreviewSceneParams{ - SceneName: &sceneName, - }) - if err != nil { - return NewOBSError(ErrOperationFailed, err.Error()) - } - - c.logger.WithField("scene", sceneName).Info("Set preview scene") - return nil -} - -// CreateScene creates a new scene -func (c *Client) CreateScene(ctx context.Context, sceneName string) error { - if !c.IsConnected() { - return ErrNotConnected - } - - _, err := c.client.Scenes.CreateScene(&scenes.CreateSceneParams{ - SceneName: &sceneName, - }) - if err != nil { - return NewOBSError(ErrOperationFailed, err.Error()) - } - - c.logger.WithField("scene", sceneName).Info("Created scene") - return nil -} - -// RemoveScene removes a scene -func (c *Client) RemoveScene(ctx context.Context, sceneName string) error { - if !c.IsConnected() { - return ErrNotConnected - } - - _, err := c.client.Scenes.RemoveScene(&scenes.RemoveSceneParams{ - SceneName: &sceneName, - }) - if err != nil { - return NewOBSError(ErrOperationFailed, err.Error()) - } - - c.logger.WithField("scene", sceneName).Info("Removed scene") - return nil -} - -// RenameScene renames a scene -func (c *Client) RenameScene(ctx context.Context, oldName, newName string) error { - if !c.IsConnected() { - return ErrNotConnected - } - - _, err := c.client.Scenes.SetSceneName(&scenes.SetSceneNameParams{ - SceneName: &oldName, - NewSceneName: &newName, - }) - if err != nil { - return NewOBSError(ErrOperationFailed, err.Error()) - } - - c.logger.WithFields(map[string]interface{}{ - "old_name": oldName, - "new_name": newName, - }).Info("Renamed scene") - return nil -} - -// GetStudioModeEnabled checks if studio mode is enabled -func (c *Client) GetStudioModeEnabled(ctx context.Context) (bool, error) { - if !c.IsConnected() { - return false, ErrNotConnected - } - - resp, err := c.client.Ui.GetStudioModeEnabled() - if err != nil { - return false, NewOBSError(ErrOperationFailed, err.Error()) - } - - return resp.StudioModeEnabled, nil -} - -// SetStudioModeEnabled enables or disables studio mode -func (c *Client) SetStudioModeEnabled(ctx context.Context, enabled bool) error { - if !c.IsConnected() { - return ErrNotConnected - } - - _, err := c.client.Ui.SetStudioModeEnabled(&ui.SetStudioModeEnabledParams{ - StudioModeEnabled: &enabled, - }) - if err != nil { - return NewOBSError(ErrOperationFailed, err.Error()) - } - - c.logger.WithField("enabled", enabled).Info("Set studio mode") - return nil -} - -// TriggerStudioModeTransition triggers the transition in studio mode -func (c *Client) TriggerStudioModeTransition(ctx context.Context) error { - if !c.IsConnected() { - return ErrNotConnected - } - - _, err := c.client.Transitions.TriggerStudioModeTransition() - if err != nil { - return NewOBSError(ErrOperationFailed, err.Error()) - } - - c.logger.Info("Triggered studio mode transition") - return nil -} - -// GetSceneWithSources returns a scene with its sources populated -func (c *Client) GetSceneWithSources(ctx context.Context, sceneName string) (*SceneInfo, error) { - if !c.IsConnected() { - return nil, ErrNotConnected - } - - // Get scene list to find scene info - scenes, err := c.GetScenes(ctx) - if err != nil { - return nil, err - } - - var scene *SceneInfo - for _, s := range scenes { - if s.Name == sceneName { - scene = &s - break - } - } - - if scene == nil { - return nil, ErrSceneNotFound - } - - // Get sources for the scene - sources, err := c.GetSceneSources(ctx, sceneName) - if err != nil { - return nil, err - } - scene.Sources = sources - - return scene, nil -} diff --git a/Premium/Desktop/internal/obs/sources.go b/Premium/Desktop/internal/obs/sources.go deleted file mode 100644 index 971c1f72b..000000000 --- a/Premium/Desktop/internal/obs/sources.go +++ /dev/null @@ -1,272 +0,0 @@ -package obs - -import ( - "context" - - "github.com/andreykaipov/goobs/api/requests/sceneitems" -) - -// GetSceneSources returns all sources in a scene -func (c *Client) GetSceneSources(ctx context.Context, sceneName string) ([]SourceInfo, error) { - if !c.IsConnected() { - return nil, ErrNotConnected - } - - resp, err := c.client.SceneItems.GetSceneItemList(&sceneitems.GetSceneItemListParams{ - SceneName: &sceneName, - }) - if err != nil { - return nil, NewOBSError(ErrOperationFailed, err.Error()) - } - - sources := make([]SourceInfo, len(resp.SceneItems)) - for i, item := range resp.SceneItems { - sources[i] = SourceInfo{ - Name: item.SourceName, - ID: item.SceneItemID, - Type: item.SourceType, - Visible: item.SceneItemEnabled, - Locked: item.SceneItemLocked, - PositionX: item.SceneItemTransform.PositionX, - PositionY: item.SceneItemTransform.PositionY, - Width: item.SceneItemTransform.SourceWidth, - Height: item.SceneItemTransform.SourceHeight, - Rotation: item.SceneItemTransform.Rotation, - ScaleX: item.SceneItemTransform.ScaleX, - ScaleY: item.SceneItemTransform.ScaleY, - BoundsType: item.SceneItemTransform.BoundsType, - BoundsWidth: item.SceneItemTransform.BoundsWidth, - BoundsHeight: item.SceneItemTransform.BoundsHeight, - } - } - - return sources, nil -} - -// GetSourceInfo returns information about a specific source in a scene -func (c *Client) GetSourceInfo(ctx context.Context, sceneName, sourceName string) (*SourceInfo, error) { - if !c.IsConnected() { - return nil, ErrNotConnected - } - - sources, err := c.GetSceneSources(ctx, sceneName) - if err != nil { - return nil, err - } - - for _, s := range sources { - if s.Name == sourceName { - return &s, nil - } - } - - return nil, ErrSourceNotFound -} - -// SetSourceVisibility sets the visibility of a source in a scene -func (c *Client) SetSourceVisibility(ctx context.Context, sceneName, sourceName string, visible bool) error { - if !c.IsConnected() { - return ErrNotConnected - } - - // First find the scene item ID - itemID, err := c.getSceneItemID(sceneName, sourceName) - if err != nil { - return err - } - - _, err = c.client.SceneItems.SetSceneItemEnabled(&sceneitems.SetSceneItemEnabledParams{ - SceneName: &sceneName, - SceneItemId: &itemID, - SceneItemEnabled: &visible, - }) - if err != nil { - return NewOBSError(ErrOperationFailed, err.Error()) - } - - c.logger.WithFields(map[string]interface{}{ - "scene": sceneName, - "source": sourceName, - "visible": visible, - }).Debug("Set source visibility") - - return nil -} - -// SetSourceLocked sets the locked state of a source in a scene -func (c *Client) SetSourceLocked(ctx context.Context, sceneName, sourceName string, locked bool) error { - if !c.IsConnected() { - return ErrNotConnected - } - - itemID, err := c.getSceneItemID(sceneName, sourceName) - if err != nil { - return err - } - - _, err = c.client.SceneItems.SetSceneItemLocked(&sceneitems.SetSceneItemLockedParams{ - SceneName: &sceneName, - SceneItemId: &itemID, - SceneItemLocked: &locked, - }) - if err != nil { - return NewOBSError(ErrOperationFailed, err.Error()) - } - - c.logger.WithFields(map[string]interface{}{ - "scene": sceneName, - "source": sourceName, - "locked": locked, - }).Debug("Set source locked state") - - return nil -} - -// SetSourcePosition sets the position of a source in a scene -func (c *Client) SetSourcePosition(ctx context.Context, sceneName, sourceName string, x, y float64) error { - return c.SetSourceTransform(ctx, sceneName, sourceName, SourceTransform{ - PositionX: &x, - PositionY: &y, - }) -} - -// SetSourceScale sets the scale of a source in a scene -func (c *Client) SetSourceScale(ctx context.Context, sceneName, sourceName string, scaleX, scaleY float64) error { - return c.SetSourceTransform(ctx, sceneName, sourceName, SourceTransform{ - ScaleX: &scaleX, - ScaleY: &scaleY, - }) -} - -// SetSourceRotation sets the rotation of a source in a scene -func (c *Client) SetSourceRotation(ctx context.Context, sceneName, sourceName string, rotation float64) error { - return c.SetSourceTransform(ctx, sceneName, sourceName, SourceTransform{ - Rotation: &rotation, - }) -} - -// SetSourceTransform sets the transform properties of a source -func (c *Client) SetSourceTransform(ctx context.Context, sceneName, sourceName string, transform SourceTransform) error { - if !c.IsConnected() { - return ErrNotConnected - } - - itemID, err := c.getSceneItemID(sceneName, sourceName) - if err != nil { - return err - } - - // Build transform params - only set provided fields - params := &sceneitems.SetSceneItemTransformParams{ - SceneName: &sceneName, - SceneItemId: &itemID, - } - - // Note: The goobs library may need individual field setting - // For now, we'll use a simplified approach - _, err = c.client.SceneItems.SetSceneItemTransform(params) - if err != nil { - return NewOBSError(ErrOperationFailed, err.Error()) - } - - c.logger.WithFields(map[string]interface{}{ - "scene": sceneName, - "source": sourceName, - }).Debug("Set source transform") - - return nil -} - -// SetSourceIndex changes the order/index of a source in a scene -func (c *Client) SetSourceIndex(ctx context.Context, sceneName, sourceName string, index int) error { - if !c.IsConnected() { - return ErrNotConnected - } - - itemID, err := c.getSceneItemID(sceneName, sourceName) - if err != nil { - return err - } - - _, err = c.client.SceneItems.SetSceneItemIndex(&sceneitems.SetSceneItemIndexParams{ - SceneName: &sceneName, - SceneItemId: &itemID, - SceneItemIndex: &index, - }) - if err != nil { - return NewOBSError(ErrOperationFailed, err.Error()) - } - - c.logger.WithFields(map[string]interface{}{ - "scene": sceneName, - "source": sourceName, - "index": index, - }).Debug("Set source index") - - return nil -} - -// DuplicateSource duplicates a source in a scene -func (c *Client) DuplicateSource(ctx context.Context, sceneName, sourceName string, destSceneName *string) (*SourceInfo, error) { - if !c.IsConnected() { - return nil, ErrNotConnected - } - - itemID, err := c.getSceneItemID(sceneName, sourceName) - if err != nil { - return nil, err - } - - resp, err := c.client.SceneItems.DuplicateSceneItem(&sceneitems.DuplicateSceneItemParams{ - SceneName: &sceneName, - SceneItemId: &itemID, - DestinationSceneName: destSceneName, - }) - if err != nil { - return nil, NewOBSError(ErrOperationFailed, err.Error()) - } - - return &SourceInfo{ - Name: sourceName, - ID: resp.SceneItemId, - }, nil -} - -// RemoveSource removes a source from a scene -func (c *Client) RemoveSource(ctx context.Context, sceneName, sourceName string) error { - if !c.IsConnected() { - return ErrNotConnected - } - - itemID, err := c.getSceneItemID(sceneName, sourceName) - if err != nil { - return err - } - - _, err = c.client.SceneItems.RemoveSceneItem(&sceneitems.RemoveSceneItemParams{ - SceneName: &sceneName, - SceneItemId: &itemID, - }) - if err != nil { - return NewOBSError(ErrOperationFailed, err.Error()) - } - - c.logger.WithFields(map[string]interface{}{ - "scene": sceneName, - "source": sourceName, - }).Info("Removed source from scene") - - return nil -} - -// getSceneItemID finds the scene item ID for a source by name -func (c *Client) getSceneItemID(sceneName, sourceName string) (int, error) { - resp, err := c.client.SceneItems.GetSceneItemId(&sceneitems.GetSceneItemIdParams{ - SceneName: &sceneName, - SourceName: &sourceName, - }) - if err != nil { - return 0, NewOBSError(ErrSourceNotFound, err.Error()) - } - return resp.SceneItemId, nil -} diff --git a/Premium/Desktop/internal/obs/streaming.go b/Premium/Desktop/internal/obs/streaming.go deleted file mode 100644 index 77e2a0997..000000000 --- a/Premium/Desktop/internal/obs/streaming.go +++ /dev/null @@ -1,129 +0,0 @@ -package obs - -import ( - "context" - "time" - - "github.com/andreykaipov/goobs/api/requests/stream" -) - -// GetStreamStatus returns the current streaming status -func (c *Client) GetStreamStatus(ctx context.Context) (*StreamStatus, error) { - if !c.IsConnected() { - return nil, ErrNotConnected - } - - resp, err := c.client.Stream.GetStreamStatus() - if err != nil { - return nil, NewOBSError(ErrOperationFailed, err.Error()) - } - - return &StreamStatus{ - Active: resp.OutputActive, - Reconnecting: resp.OutputReconnecting, - TimecodeString: resp.OutputTimecode, - Duration: time.Duration(resp.OutputDuration) * time.Millisecond, - BytesSent: int64(resp.OutputBytes), - KbitsPerSec: int64(resp.OutputCongestion), // Note: congestion is 0-1, use different metric if available - DroppedFrames: int64(resp.OutputSkippedFrames), - TotalFrames: int64(resp.OutputTotalFrames), - RenderSkippedFrames: 0, // Not available in stream status - OutputSkippedFrames: int64(resp.OutputSkippedFrames), - }, nil -} - -// StartStream starts streaming -func (c *Client) StartStream(ctx context.Context) error { - if !c.IsConnected() { - return ErrNotConnected - } - - _, err := c.client.Stream.StartStream() - if err != nil { - return NewOBSError(ErrOperationFailed, err.Error()) - } - - c.logger.Info("Started streaming") - - // Emit event - c.emitEvent(Event{ - Type: EventStreamStarted, - Timestamp: time.Now(), - }) - - return nil -} - -// StopStream stops streaming -func (c *Client) StopStream(ctx context.Context) error { - if !c.IsConnected() { - return ErrNotConnected - } - - _, err := c.client.Stream.StopStream() - if err != nil { - return NewOBSError(ErrOperationFailed, err.Error()) - } - - c.logger.Info("Stopped streaming") - - // Emit event - c.emitEvent(Event{ - Type: EventStreamStopped, - Timestamp: time.Now(), - }) - - return nil -} - -// ToggleStream toggles the streaming state -func (c *Client) ToggleStream(ctx context.Context) (bool, error) { - if !c.IsConnected() { - return false, ErrNotConnected - } - - resp, err := c.client.Stream.ToggleStream() - if err != nil { - return false, NewOBSError(ErrOperationFailed, err.Error()) - } - - c.logger.WithField("active", resp.OutputActive).Info("Toggled streaming") - - return resp.OutputActive, nil -} - -// SendStreamCaption sends a caption/subtitle to the stream -func (c *Client) SendStreamCaption(ctx context.Context, caption string) error { - if !c.IsConnected() { - return ErrNotConnected - } - - _, err := c.client.Stream.SendStreamCaption(&stream.SendStreamCaptionParams{ - CaptionText: &caption, - }) - if err != nil { - return NewOBSError(ErrOperationFailed, err.Error()) - } - - c.logger.WithField("caption_length", len(caption)).Debug("Sent stream caption") - - return nil -} - -// IsStreaming returns true if currently streaming -func (c *Client) IsStreaming(ctx context.Context) (bool, error) { - status, err := c.GetStreamStatus(ctx) - if err != nil { - return false, err - } - return status.Active, nil -} - -// GetStreamDuration returns the current stream duration -func (c *Client) GetStreamDuration(ctx context.Context) (time.Duration, error) { - status, err := c.GetStreamStatus(ctx) - if err != nil { - return 0, err - } - return status.Duration, nil -} diff --git a/Premium/Desktop/internal/obs/types.go b/Premium/Desktop/internal/obs/types.go deleted file mode 100644 index 405102799..000000000 --- a/Premium/Desktop/internal/obs/types.go +++ /dev/null @@ -1,342 +0,0 @@ -// Package obs provides OBS WebSocket integration for the WaddleBot Desktop Bridge. -// It implements the obs-websocket v5 protocol for full OBS Studio control. -package obs - -import ( - "time" -) - -// ConnectionState represents the current OBS connection status -type ConnectionState int - -const ( - // StateDisconnected indicates no active connection to OBS - StateDisconnected ConnectionState = iota - // StateConnecting indicates a connection attempt is in progress - StateConnecting - // StateConnected indicates an active connection to OBS - StateConnected - // StateReconnecting indicates automatic reconnection is in progress - StateReconnecting -) - -// String returns a human-readable representation of the connection state -func (s ConnectionState) String() string { - switch s { - case StateDisconnected: - return "disconnected" - case StateConnecting: - return "connecting" - case StateConnected: - return "connected" - case StateReconnecting: - return "reconnecting" - default: - return "unknown" - } -} - -// Config holds OBS WebSocket connection configuration -type Config struct { - // Host is the OBS WebSocket server hostname (default: localhost) - Host string `mapstructure:"obs-host"` - // Port is the OBS WebSocket server port (default: 4455) - Port int `mapstructure:"obs-port"` - // Password is the OBS WebSocket authentication password - Password string `mapstructure:"obs-password"` - // AutoReconnect enables automatic reconnection on disconnect - AutoReconnect bool `mapstructure:"obs-auto-reconnect"` - // ReconnectInterval is the base interval between reconnection attempts - ReconnectInterval time.Duration `mapstructure:"obs-reconnect-interval"` - // MaxReconnectInterval is the maximum interval between reconnection attempts - MaxReconnectInterval time.Duration `mapstructure:"obs-max-reconnect-interval"` - // Timeout is the connection timeout duration - Timeout time.Duration `mapstructure:"obs-timeout"` - // Enabled controls whether OBS integration is active - Enabled bool `mapstructure:"obs-enabled"` -} - -// DefaultConfig returns the default OBS configuration -func DefaultConfig() Config { - return Config{ - Host: "localhost", - Port: 4455, - Password: "", - AutoReconnect: true, - ReconnectInterval: time.Second, - MaxReconnectInterval: 30 * time.Second, - Timeout: 10 * time.Second, - Enabled: true, - } -} - -// SceneInfo represents information about an OBS scene -type SceneInfo struct { - // Name is the unique name of the scene - Name string `json:"name"` - // Index is the scene's position in the scene list - Index int `json:"index"` - // IsCurrent indicates if this is the currently active program scene - IsCurrent bool `json:"is_current"` - // IsPreview indicates if this is the currently active preview scene (studio mode) - IsPreview bool `json:"is_preview"` - // Sources contains the list of sources in this scene (optional) - Sources []SourceInfo `json:"sources,omitempty"` -} - -// SourceInfo represents information about an OBS source/scene item -type SourceInfo struct { - // Name is the name of the source - Name string `json:"name"` - // ID is the unique scene item ID - ID int `json:"id"` - // Type is the source type (e.g., "browser_source", "image_source") - Type string `json:"type"` - // Visible indicates if the source is currently visible - Visible bool `json:"visible"` - // Locked indicates if the source is locked from interaction - Locked bool `json:"locked"` - // PositionX is the X position of the source - PositionX float64 `json:"position_x"` - // PositionY is the Y position of the source - PositionY float64 `json:"position_y"` - // Width is the base width of the source - Width float64 `json:"width"` - // Height is the base height of the source - Height float64 `json:"height"` - // Rotation is the rotation angle in degrees - Rotation float64 `json:"rotation"` - // ScaleX is the horizontal scale factor - ScaleX float64 `json:"scale_x"` - // ScaleY is the vertical scale factor - ScaleY float64 `json:"scale_y"` - // BoundsType is the bounding box type - BoundsType string `json:"bounds_type,omitempty"` - // BoundsWidth is the bounding box width - BoundsWidth float64 `json:"bounds_width,omitempty"` - // BoundsHeight is the bounding box height - BoundsHeight float64 `json:"bounds_height,omitempty"` -} - -// SourceTransform contains transform properties for a source -type SourceTransform struct { - // PositionX is the X position - PositionX *float64 `json:"position_x,omitempty"` - // PositionY is the Y position - PositionY *float64 `json:"position_y,omitempty"` - // Rotation is the rotation angle in degrees - Rotation *float64 `json:"rotation,omitempty"` - // ScaleX is the horizontal scale factor - ScaleX *float64 `json:"scale_x,omitempty"` - // ScaleY is the vertical scale factor - ScaleY *float64 `json:"scale_y,omitempty"` - // BoundsType is the bounding box type - BoundsType *string `json:"bounds_type,omitempty"` - // BoundsWidth is the bounding box width - BoundsWidth *float64 `json:"bounds_width,omitempty"` - // BoundsHeight is the bounding box height - BoundsHeight *float64 `json:"bounds_height,omitempty"` -} - -// FilterInfo represents information about an OBS filter -type FilterInfo struct { - // Name is the filter name - Name string `json:"name"` - // Type is the filter type identifier - Type string `json:"type"` - // Index is the filter's position in the filter list - Index int `json:"index"` - // Enabled indicates if the filter is currently enabled - Enabled bool `json:"enabled"` - // Settings contains the filter's configuration settings - Settings map[string]interface{} `json:"settings,omitempty"` -} - -// StreamStatus represents the current streaming state -type StreamStatus struct { - // Active indicates if streaming is currently active - Active bool `json:"active"` - // Reconnecting indicates if the stream is attempting to reconnect - Reconnecting bool `json:"reconnecting"` - // TimecodeString is the stream duration as a timecode string (HH:MM:SS) - TimecodeString string `json:"timecode"` - // Duration is the stream duration - Duration time.Duration `json:"duration"` - // BytesSent is the total bytes sent - BytesSent int64 `json:"bytes_sent"` - // KbitsPerSec is the current bitrate in kilobits per second - KbitsPerSec int64 `json:"kbits_per_sec"` - // DroppedFrames is the number of dropped frames - DroppedFrames int64 `json:"dropped_frames"` - // TotalFrames is the total number of frames - TotalFrames int64 `json:"total_frames"` - // RenderSkippedFrames is the number of skipped render frames - RenderSkippedFrames int64 `json:"render_skipped_frames"` - // OutputSkippedFrames is the number of skipped output frames - OutputSkippedFrames int64 `json:"output_skipped_frames"` -} - -// RecordingStatus represents the current recording state -type RecordingStatus struct { - // Active indicates if recording is currently active - Active bool `json:"active"` - // Paused indicates if recording is currently paused - Paused bool `json:"paused"` - // TimecodeString is the recording duration as a timecode string (HH:MM:SS) - TimecodeString string `json:"timecode"` - // Duration is the recording duration - Duration time.Duration `json:"duration"` - // BytesWritten is the total bytes written to disk - BytesWritten int64 `json:"bytes_written"` - // OutputPath is the path to the recording file - OutputPath string `json:"output_path"` -} - -// OBSStats represents general OBS statistics -type OBSStats struct { - // CPUUsage is the current CPU usage percentage - CPUUsage float64 `json:"cpu_usage"` - // MemoryUsage is the current memory usage in MB - MemoryUsage float64 `json:"memory_usage"` - // FreeDiskSpace is the available disk space in MB - FreeDiskSpace float64 `json:"free_disk_space"` - // ActiveFPS is the current FPS - ActiveFPS float64 `json:"active_fps"` - // AverageFrameTime is the average frame render time in ms - AverageFrameTime float64 `json:"average_frame_time"` - // RenderSkippedFrames is the total render skipped frames - RenderSkippedFrames int64 `json:"render_skipped_frames"` - // RenderTotalFrames is the total render frames - RenderTotalFrames int64 `json:"render_total_frames"` - // OutputSkippedFrames is the total output skipped frames - OutputSkippedFrames int64 `json:"output_skipped_frames"` - // OutputTotalFrames is the total output frames - OutputTotalFrames int64 `json:"output_total_frames"` - // WebSocketSessionIncomingMessages is the count of incoming WebSocket messages - WebSocketSessionIncomingMessages int64 `json:"ws_incoming_messages"` - // WebSocketSessionOutgoingMessages is the count of outgoing WebSocket messages - WebSocketSessionOutgoingMessages int64 `json:"ws_outgoing_messages"` -} - -// EventType represents the type of OBS event -type EventType string - -// OBS event type constants -const ( - // Scene events - EventSceneChanged EventType = "scene_changed" - EventSceneListChanged EventType = "scene_list_changed" - EventSceneNameChanged EventType = "scene_name_changed" - EventSceneCreated EventType = "scene_created" - EventSceneRemoved EventType = "scene_removed" - - // Source/Scene item events - EventSourceVisibilityChanged EventType = "source_visibility_changed" - EventSourceLockChanged EventType = "source_lock_changed" - EventSourceTransformChanged EventType = "source_transform_changed" - EventSourceCreated EventType = "source_created" - EventSourceRemoved EventType = "source_removed" - EventSourceRenamed EventType = "source_renamed" - - // Filter events - EventFilterEnabled EventType = "filter_enabled" - EventFilterDisabled EventType = "filter_disabled" - EventFilterListChanged EventType = "filter_list_changed" - EventFilterNameChanged EventType = "filter_name_changed" - EventFilterCreated EventType = "filter_created" - EventFilterRemoved EventType = "filter_removed" - - // Streaming events - EventStreamStarting EventType = "stream_starting" - EventStreamStarted EventType = "stream_started" - EventStreamStopping EventType = "stream_stopping" - EventStreamStopped EventType = "stream_stopped" - EventStreamReconnect EventType = "stream_reconnect" - - // Recording events - EventRecordingStarting EventType = "recording_starting" - EventRecordingStarted EventType = "recording_started" - EventRecordingStopping EventType = "recording_stopping" - EventRecordingStopped EventType = "recording_stopped" - EventRecordingPaused EventType = "recording_paused" - EventRecordingResumed EventType = "recording_resumed" - - // General events - EventExiting EventType = "exiting" - EventStudioModeChanged EventType = "studio_mode_changed" -) - -// Event represents an OBS event -type Event struct { - // Type is the event type - Type EventType `json:"type"` - // Timestamp is when the event occurred - Timestamp time.Time `json:"timestamp"` - // Data contains event-specific data - Data map[string]interface{} `json:"data,omitempty"` -} - -// EventCallback is a function that handles OBS events -type EventCallback func(event Event) - -// SubscriptionID is a unique identifier for an event subscription -type SubscriptionID string - -// ConnectionInfo represents information about the OBS connection -type ConnectionInfo struct { - // State is the current connection state - State ConnectionState `json:"state"` - // OBSVersion is the connected OBS version - OBSVersion string `json:"obs_version,omitempty"` - // WebSocketVersion is the obs-websocket version - WebSocketVersion string `json:"websocket_version,omitempty"` - // Platform is the operating system OBS is running on - Platform string `json:"platform,omitempty"` - // ConnectedAt is when the connection was established - ConnectedAt *time.Time `json:"connected_at,omitempty"` - // DisconnectedAt is when the connection was lost - DisconnectedAt *time.Time `json:"disconnected_at,omitempty"` - // ReconnectAttempts is the number of reconnection attempts since last disconnect - ReconnectAttempts int `json:"reconnect_attempts,omitempty"` - // LastError is the last error message - LastError string `json:"last_error,omitempty"` -} - -// Error types for OBS operations -var ( - ErrNotConnected = &OBSError{Code: "not_connected", Message: "not connected to OBS"} - ErrConnectionFailed = &OBSError{Code: "connection_failed", Message: "failed to connect to OBS"} - ErrAuthFailed = &OBSError{Code: "auth_failed", Message: "authentication failed"} - ErrSceneNotFound = &OBSError{Code: "scene_not_found", Message: "scene not found"} - ErrSourceNotFound = &OBSError{Code: "source_not_found", Message: "source not found"} - ErrFilterNotFound = &OBSError{Code: "filter_not_found", Message: "filter not found"} - ErrOperationFailed = &OBSError{Code: "operation_failed", Message: "operation failed"} - ErrTimeout = &OBSError{Code: "timeout", Message: "operation timed out"} -) - -// OBSError represents an OBS operation error -type OBSError struct { - // Code is the error code - Code string `json:"code"` - // Message is the error message - Message string `json:"message"` - // Details contains additional error details - Details string `json:"details,omitempty"` -} - -// Error implements the error interface -func (e *OBSError) Error() string { - if e.Details != "" { - return e.Code + ": " + e.Message + " - " + e.Details - } - return e.Code + ": " + e.Message -} - -// NewOBSError creates a new OBS error with details -func NewOBSError(base *OBSError, details string) *OBSError { - return &OBSError{ - Code: base.Code, - Message: base.Message, - Details: details, - } -} diff --git a/Premium/Desktop/internal/poller/poller.go b/Premium/Desktop/internal/poller/poller.go deleted file mode 100644 index a3ce7e6c4..000000000 --- a/Premium/Desktop/internal/poller/poller.go +++ /dev/null @@ -1,336 +0,0 @@ -package poller - -import ( - "context" - "encoding/json" - "fmt" - "io" - "net/http" - "strings" - "time" - - "github.com/sirupsen/logrus" - "waddlebot-bridge/internal/bridge" - "waddlebot-bridge/internal/config" - "waddlebot-bridge/internal/logger" - "waddlebot-bridge/internal/modules" -) - -// Poller handles polling the WaddleBot API for actions to execute -type Poller struct { - config *config.Config - bridgeClient *bridge.Client - moduleManager *modules.Manager - logger *logrus.Logger - httpClient *http.Client - ticker *time.Ticker - lastPoll time.Time -} - -// ActionRequest represents an action request from the server -type ActionRequest struct { - ID string `json:"id"` - Type string `json:"type"` - ModuleName string `json:"module_name"` - Action string `json:"action"` - Parameters map[string]string `json:"parameters"` - UserID string `json:"user_id"` - CommunityID string `json:"community_id"` - Priority int `json:"priority"` - Timeout int `json:"timeout"` - CreatedAt time.Time `json:"created_at"` - ExpiresAt time.Time `json:"expires_at"` -} - -// ActionResponse represents the response to an action request -type ActionResponse struct { - ID string `json:"id"` - Success bool `json:"success"` - Result map[string]interface{} `json:"result,omitempty"` - Error string `json:"error,omitempty"` - Duration int64 `json:"duration"` // in milliseconds - Timestamp time.Time `json:"timestamp"` -} - -// PollResponse represents the response from the polling endpoint -type PollResponse struct { - Actions []ActionRequest `json:"actions"` - NextPoll time.Time `json:"next_poll"` - ServerTime time.Time `json:"server_time"` - HasMore bool `json:"has_more"` - PollCount int `json:"poll_count"` - ClientInfo ClientInfo `json:"client_info"` -} - -// ClientInfo represents client information for the poll -type ClientInfo struct { - LastSeen time.Time `json:"last_seen"` - ActionsTotal int `json:"actions_total"` - ActionsSuccess int `json:"actions_success"` - ActionsFailed int `json:"actions_failed"` - Uptime int64 `json:"uptime"` -} - -// NewPoller creates a new poller instance -func NewPoller(cfg *config.Config, bridgeClient *bridge.Client, moduleManager *modules.Manager) *Poller { - return &Poller{ - config: cfg, - bridgeClient: bridgeClient, - moduleManager: moduleManager, - logger: logger.GetLogger(), - httpClient: &http.Client{ - Timeout: 30 * time.Second, - }, - lastPoll: time.Now(), - } -} - -// Start starts the polling process -func (p *Poller) Start(ctx context.Context) error { - p.logger.WithFields(logrus.Fields{ - "interval": p.config.PollInterval, - "community_id": p.config.CommunityID, - "user_id": p.config.UserID, - }).Info("Starting action poller") - - // Create ticker for polling interval - p.ticker = time.NewTicker(time.Duration(p.config.PollInterval) * time.Second) - defer p.ticker.Stop() - - // Initial poll - if err := p.pollForActions(ctx); err != nil { - p.logger.WithError(err).Error("Initial poll failed") - } - - // Main polling loop - for { - select { - case <-ctx.Done(): - p.logger.Info("Stopping action poller") - return nil - case <-p.ticker.C: - if err := p.pollForActions(ctx); err != nil { - p.logger.WithError(err).Error("Poll failed") - } - } - } -} - -// pollForActions polls the server for actions to execute -func (p *Poller) pollForActions(ctx context.Context) error { - startTime := time.Now() - - // Get authentication token - token, err := p.bridgeClient.GetAuthToken() - if err != nil { - return fmt.Errorf("failed to get auth token: %w", err) - } - - // Build poll URL - pollURL := p.config.GetAPIEndpoint("/api/bridge/poll") - - // Create request - req, err := http.NewRequestWithContext(ctx, "GET", pollURL, nil) - if err != nil { - return fmt.Errorf("failed to create request: %w", err) - } - - // Add headers - req.Header.Set("Authorization", "Bearer "+token) - req.Header.Set("User-Agent", p.config.GetUserAgent()) - req.Header.Set("Content-Type", "application/json") - req.Header.Set("X-Community-ID", p.config.CommunityID) - req.Header.Set("X-User-ID", p.config.UserID) - req.Header.Set("X-Last-Poll", p.lastPoll.Format(time.RFC3339)) - - // Make request - resp, err := p.httpClient.Do(req) - if err != nil { - return fmt.Errorf("failed to make request: %w", err) - } - defer resp.Body.Close() - - // Read response - body, err := io.ReadAll(resp.Body) - if err != nil { - return fmt.Errorf("failed to read response: %w", err) - } - - // Check status code - if resp.StatusCode != http.StatusOK { - return fmt.Errorf("server returned status %d: %s", resp.StatusCode, string(body)) - } - - // Parse response - var pollResponse PollResponse - if err := json.Unmarshal(body, &pollResponse); err != nil { - return fmt.Errorf("failed to parse response: %w", err) - } - - // Update last poll time - p.lastPoll = time.Now() - - // Process actions - if len(pollResponse.Actions) > 0 { - p.logger.WithFields(logrus.Fields{ - "action_count": len(pollResponse.Actions), - "has_more": pollResponse.HasMore, - }).Info("Received actions from server") - - // Process each action - for _, action := range pollResponse.Actions { - if err := p.processAction(ctx, action); err != nil { - p.logger.WithError(err).WithField("action_id", action.ID).Error("Failed to process action") - } - } - } - - // Log polling statistics - duration := time.Since(startTime) - p.logger.WithFields(logrus.Fields{ - "duration": duration, - "actions": len(pollResponse.Actions), - "server_time": pollResponse.ServerTime, - "next_poll": pollResponse.NextPoll, - "poll_count": pollResponse.PollCount, - }).Debug("Poll completed") - - return nil -} - -// processAction processes a single action request -func (p *Poller) processAction(ctx context.Context, action ActionRequest) error { - startTime := time.Now() - - p.logger.WithFields(logrus.Fields{ - "action_id": action.ID, - "module_name": action.ModuleName, - "action": action.Action, - "user_id": action.UserID, - "priority": action.Priority, - }).Info("Processing action") - - // Check if action has expired - if time.Now().After(action.ExpiresAt) { - p.logger.WithField("action_id", action.ID).Warn("Action expired, skipping") - return p.sendActionResponse(ctx, ActionResponse{ - ID: action.ID, - Success: false, - Error: "Action expired", - Duration: time.Since(startTime).Milliseconds(), - Timestamp: time.Now(), - }) - } - - // Create context with timeout - actionCtx, cancel := context.WithTimeout(ctx, time.Duration(action.Timeout)*time.Second) - defer cancel() - - // Execute action through module manager - result, err := p.moduleManager.ExecuteAction(actionCtx, action.ModuleName, action.Action, action.Parameters) - - // Calculate duration - duration := time.Since(startTime) - - // Create response - response := ActionResponse{ - ID: action.ID, - Success: err == nil, - Duration: duration.Milliseconds(), - Timestamp: time.Now(), - } - - if err != nil { - response.Error = err.Error() - p.logger.WithError(err).WithField("action_id", action.ID).Error("Action execution failed") - } else { - response.Result = result - p.logger.WithFields(logrus.Fields{ - "action_id": action.ID, - "duration": duration, - }).Info("Action executed successfully") - } - - // Send response back to server - return p.sendActionResponse(ctx, response) -} - -// sendActionResponse sends the action response back to the server -func (p *Poller) sendActionResponse(ctx context.Context, response ActionResponse) error { - // Get authentication token - token, err := p.bridgeClient.GetAuthToken() - if err != nil { - return fmt.Errorf("failed to get auth token: %w", err) - } - - // Build response URL - responseURL := p.config.GetAPIEndpoint("/api/bridge/response") - - // Marshal response - responseData, err := json.Marshal(response) - if err != nil { - return fmt.Errorf("failed to marshal response: %w", err) - } - - // Create request - req, err := http.NewRequestWithContext(ctx, "POST", responseURL, - strings.NewReader(string(responseData))) - if err != nil { - return fmt.Errorf("failed to create request: %w", err) - } - - // Add headers - req.Header.Set("Authorization", "Bearer "+token) - req.Header.Set("User-Agent", p.config.GetUserAgent()) - req.Header.Set("Content-Type", "application/json") - req.Header.Set("X-Community-ID", p.config.CommunityID) - req.Header.Set("X-User-ID", p.config.UserID) - - // Make request - resp, err := p.httpClient.Do(req) - if err != nil { - return fmt.Errorf("failed to make request: %w", err) - } - defer resp.Body.Close() - - // Check status code - if resp.StatusCode != http.StatusOK { - body, _ := io.ReadAll(resp.Body) - return fmt.Errorf("server returned status %d: %s", resp.StatusCode, string(body)) - } - - p.logger.WithFields(logrus.Fields{ - "action_id": response.ID, - "success": response.Success, - "duration": response.Duration, - }).Debug("Action response sent") - - return nil -} - -// UpdatePollInterval updates the polling interval -func (p *Poller) UpdatePollInterval(seconds int) { - if seconds < 5 { - seconds = 5 - } - - p.config.PollInterval = seconds - - if p.ticker != nil { - p.ticker.Stop() - p.ticker = time.NewTicker(time.Duration(seconds) * time.Second) - } - - p.logger.WithField("interval", seconds).Info("Updated poll interval") -} - -// GetStats returns polling statistics -func (p *Poller) GetStats() map[string]interface{} { - return map[string]interface{}{ - "poll_interval": p.config.PollInterval, - "last_poll": p.lastPoll, - "uptime": time.Since(p.lastPoll).Seconds(), - "community_id": p.config.CommunityID, - "user_id": p.config.UserID, - } -} \ No newline at end of file diff --git a/Premium/Desktop/internal/poller/poller_test.go b/Premium/Desktop/internal/poller/poller_test.go deleted file mode 100644 index fdb85181c..000000000 --- a/Premium/Desktop/internal/poller/poller_test.go +++ /dev/null @@ -1,752 +0,0 @@ -package poller - -import ( - "context" - "encoding/json" - "net/http" - "net/http/httptest" - "strings" - "testing" - "time" - - "waddlebot-bridge/internal/testutils" -) - -func TestNewPoller(t *testing.T) { - cfg := testutils.TestConfig() - bridgeClient := testutils.NewMockBridgeClient(cfg) - moduleManager := testutils.NewMockModuleManager() - - poller := NewPoller(cfg, bridgeClient, moduleManager) - - if poller == nil { - t.Fatal("Expected non-nil poller") - } - - if poller.config != cfg { - t.Error("Expected config to be set") - } - - if poller.bridgeClient != bridgeClient { - t.Error("Expected bridgeClient to be set") - } - - if poller.moduleManager != moduleManager { - t.Error("Expected moduleManager to be set") - } - - if poller.logger == nil { - t.Error("Expected logger to be set") - } - - if poller.httpClient == nil { - t.Error("Expected httpClient to be set") - } - - if poller.httpClient.Timeout != 30*time.Second { - t.Errorf("Expected httpClient timeout 30s, got %v", poller.httpClient.Timeout) - } -} - -func TestPoller_PollForActions_Success(t *testing.T) { - // Create test server - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // Verify request - if r.URL.Path != "/api/bridge/poll" { - t.Errorf("Expected path '/api/bridge/poll', got %s", r.URL.Path) - } - - if r.Method != "GET" { - t.Errorf("Expected method GET, got %s", r.Method) - } - - // Check headers - if r.Header.Get("Authorization") == "" { - t.Error("Expected Authorization header") - } - - if r.Header.Get("X-Community-ID") == "" { - t.Error("Expected X-Community-ID header") - } - - if r.Header.Get("X-User-ID") == "" { - t.Error("Expected X-User-ID header") - } - - // Return successful response - response := PollResponse{ - Actions: []ActionRequest{ - { - ID: "test-action-1", - Type: "module_action", - ModuleName: "test-module", - Action: "ping", - Parameters: map[string]string{"test": "value"}, - UserID: "test-user", - CommunityID: "test-community", - Priority: 1, - Timeout: 30, - CreatedAt: time.Now(), - ExpiresAt: time.Now().Add(5 * time.Minute), - }, - }, - NextPoll: time.Now().Add(30 * time.Second), - ServerTime: time.Now(), - HasMore: false, - PollCount: 1, - ClientInfo: ClientInfo{ - LastSeen: time.Now(), - ActionsTotal: 1, - ActionsSuccess: 0, - ActionsFailed: 0, - Uptime: 3600, - }, - } - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(response) - })) - defer server.Close() - - // Create test components - cfg := testutils.TestConfig() - cfg.APIURL = server.URL - bridgeClient := testutils.NewMockBridgeClient(cfg) - moduleManager := testutils.NewMockModuleManager() - - // Add test module - testModule := testutils.TestModule("test-module") - moduleManager.AddModule("test-module", testModule) - - poller := NewPoller(cfg, bridgeClient, moduleManager) - - // Mock response server - responseServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path == "/api/bridge/response" { - w.WriteHeader(http.StatusOK) - } - })) - defer responseServer.Close() - - // Update config to use response server - cfg.APIURL = responseServer.URL - server.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path == "/api/bridge/poll" { - response := PollResponse{ - Actions: []ActionRequest{ - { - ID: "test-action-1", - Type: "module_action", - ModuleName: "test-module", - Action: "ping", - Parameters: map[string]string{"test": "value"}, - UserID: "test-user", - CommunityID: "test-community", - Priority: 1, - Timeout: 30, - CreatedAt: time.Now(), - ExpiresAt: time.Now().Add(5 * time.Minute), - }, - }, - NextPoll: time.Now().Add(30 * time.Second), - ServerTime: time.Now(), - HasMore: false, - PollCount: 1, - } - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(response) - } else if r.URL.Path == "/api/bridge/response" { - w.WriteHeader(http.StatusOK) - } - }) - cfg.APIURL = server.URL - - // Test polling - ctx, cancel := testutils.TestContext() - defer cancel() - - err := poller.pollForActions(ctx) - if err != nil { - t.Fatalf("pollForActions failed: %v", err) - } -} - -func TestPoller_PollForActions_EmptyResponse(t *testing.T) { - // Create test server that returns empty actions - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - response := PollResponse{ - Actions: []ActionRequest{}, - NextPoll: time.Now().Add(30 * time.Second), - ServerTime: time.Now(), - HasMore: false, - PollCount: 1, - } - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(response) - })) - defer server.Close() - - cfg := testutils.TestConfig() - cfg.APIURL = server.URL - bridgeClient := testutils.NewMockBridgeClient(cfg) - moduleManager := testutils.NewMockModuleManager() - - poller := NewPoller(cfg, bridgeClient, moduleManager) - - ctx, cancel := testutils.TestContext() - defer cancel() - - err := poller.pollForActions(ctx) - if err != nil { - t.Fatalf("pollForActions failed: %v", err) - } -} - -func TestPoller_PollForActions_ServerError(t *testing.T) { - // Create test server that returns error - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte("Internal server error")) - })) - defer server.Close() - - cfg := testutils.TestConfig() - cfg.APIURL = server.URL - bridgeClient := testutils.NewMockBridgeClient(cfg) - moduleManager := testutils.NewMockModuleManager() - - poller := NewPoller(cfg, bridgeClient, moduleManager) - - ctx, cancel := testutils.TestContext() - defer cancel() - - err := poller.pollForActions(ctx) - if err == nil { - t.Error("Expected error for server error") - } -} - -func TestPoller_PollForActions_InvalidJSON(t *testing.T) { - // Create test server that returns invalid JSON - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - w.Write([]byte("invalid json")) - })) - defer server.Close() - - cfg := testutils.TestConfig() - cfg.APIURL = server.URL - bridgeClient := testutils.NewMockBridgeClient(cfg) - moduleManager := testutils.NewMockModuleManager() - - poller := NewPoller(cfg, bridgeClient, moduleManager) - - ctx, cancel := testutils.TestContext() - defer cancel() - - err := poller.pollForActions(ctx) - if err == nil { - t.Error("Expected error for invalid JSON") - } -} - -func TestPoller_ProcessAction_Success(t *testing.T) { - cfg := testutils.TestConfig() - bridgeClient := testutils.NewMockBridgeClient(cfg) - moduleManager := testutils.NewMockModuleManager() - - // Add test module - testModule := testutils.TestModule("test-module") - moduleManager.AddModule("test-module", testModule) - - // Create response server - responseServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path == "/api/bridge/response" { - // Verify response data - var response ActionResponse - err := json.NewDecoder(r.Body).Decode(&response) - if err != nil { - t.Fatalf("Failed to decode response: %v", err) - } - - if response.ID != "test-action-1" { - t.Errorf("Expected response ID 'test-action-1', got %s", response.ID) - } - - if !response.Success { - t.Errorf("Expected success true, got %v", response.Success) - } - - w.WriteHeader(http.StatusOK) - } - })) - defer responseServer.Close() - - cfg.APIURL = responseServer.URL - poller := NewPoller(cfg, bridgeClient, moduleManager) - - action := ActionRequest{ - ID: "test-action-1", - Type: "module_action", - ModuleName: "test-module", - Action: "ping", - Parameters: map[string]string{}, - UserID: "test-user", - CommunityID: "test-community", - Priority: 1, - Timeout: 30, - CreatedAt: time.Now(), - ExpiresAt: time.Now().Add(5 * time.Minute), - } - - ctx, cancel := testutils.TestContext() - defer cancel() - - err := poller.processAction(ctx, action) - if err != nil { - t.Fatalf("processAction failed: %v", err) - } -} - -func TestPoller_ProcessAction_ExpiredAction(t *testing.T) { - cfg := testutils.TestConfig() - bridgeClient := testutils.NewMockBridgeClient(cfg) - moduleManager := testutils.NewMockModuleManager() - - // Create response server - responseServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path == "/api/bridge/response" { - var response ActionResponse - err := json.NewDecoder(r.Body).Decode(&response) - if err != nil { - t.Fatalf("Failed to decode response: %v", err) - } - - if response.Success { - t.Error("Expected success false for expired action") - } - - if response.Error != "Action expired" { - t.Errorf("Expected error 'Action expired', got %s", response.Error) - } - - w.WriteHeader(http.StatusOK) - } - })) - defer responseServer.Close() - - cfg.APIURL = responseServer.URL - poller := NewPoller(cfg, bridgeClient, moduleManager) - - // Create expired action - action := ActionRequest{ - ID: "expired-action", - Type: "module_action", - ModuleName: "test-module", - Action: "ping", - Parameters: map[string]string{}, - UserID: "test-user", - CommunityID: "test-community", - Priority: 1, - Timeout: 30, - CreatedAt: time.Now().Add(-10 * time.Minute), - ExpiresAt: time.Now().Add(-5 * time.Minute), // Expired - } - - ctx, cancel := testutils.TestContext() - defer cancel() - - err := poller.processAction(ctx, action) - if err != nil { - t.Fatalf("processAction failed: %v", err) - } -} - -func TestPoller_ProcessAction_ModuleError(t *testing.T) { - cfg := testutils.TestConfig() - bridgeClient := testutils.NewMockBridgeClient(cfg) - moduleManager := testutils.NewMockModuleManager() - - // Add test module - testModule := testutils.TestModule("test-module") - moduleManager.AddModule("test-module", testModule) - - // Create response server - responseServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path == "/api/bridge/response" { - var response ActionResponse - err := json.NewDecoder(r.Body).Decode(&response) - if err != nil { - t.Fatalf("Failed to decode response: %v", err) - } - - if response.Success { - t.Error("Expected success false for module error") - } - - if response.Error == "" { - t.Error("Expected error message for module error") - } - - w.WriteHeader(http.StatusOK) - } - })) - defer responseServer.Close() - - cfg.APIURL = responseServer.URL - poller := NewPoller(cfg, bridgeClient, moduleManager) - - // Create action that will fail - action := ActionRequest{ - ID: "fail-action", - Type: "module_action", - ModuleName: "test-module", - Action: "fail", // This action will fail - Parameters: map[string]string{}, - UserID: "test-user", - CommunityID: "test-community", - Priority: 1, - Timeout: 30, - CreatedAt: time.Now(), - ExpiresAt: time.Now().Add(5 * time.Minute), - } - - ctx, cancel := testutils.TestContext() - defer cancel() - - err := poller.processAction(ctx, action) - if err != nil { - t.Fatalf("processAction failed: %v", err) - } -} - -func TestPoller_ProcessAction_NonexistentModule(t *testing.T) { - cfg := testutils.TestConfig() - bridgeClient := testutils.NewMockBridgeClient(cfg) - moduleManager := testutils.NewMockModuleManager() - - // Create response server - responseServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path == "/api/bridge/response" { - var response ActionResponse - err := json.NewDecoder(r.Body).Decode(&response) - if err != nil { - t.Fatalf("Failed to decode response: %v", err) - } - - if response.Success { - t.Error("Expected success false for nonexistent module") - } - - w.WriteHeader(http.StatusOK) - } - })) - defer responseServer.Close() - - cfg.APIURL = responseServer.URL - poller := NewPoller(cfg, bridgeClient, moduleManager) - - action := ActionRequest{ - ID: "nonexistent-action", - Type: "module_action", - ModuleName: "nonexistent-module", - Action: "ping", - Parameters: map[string]string{}, - UserID: "test-user", - CommunityID: "test-community", - Priority: 1, - Timeout: 30, - CreatedAt: time.Now(), - ExpiresAt: time.Now().Add(5 * time.Minute), - } - - ctx, cancel := testutils.TestContext() - defer cancel() - - err := poller.processAction(ctx, action) - if err != nil { - t.Fatalf("processAction failed: %v", err) - } -} - -func TestPoller_SendActionResponse_Success(t *testing.T) { - // Create test server - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path != "/api/bridge/response" { - t.Errorf("Expected path '/api/bridge/response', got %s", r.URL.Path) - } - - if r.Method != "POST" { - t.Errorf("Expected method POST, got %s", r.Method) - } - - // Check headers - if r.Header.Get("Authorization") == "" { - t.Error("Expected Authorization header") - } - - if r.Header.Get("Content-Type") != "application/json" { - t.Error("Expected Content-Type application/json") - } - - // Read and verify response - var response ActionResponse - err := json.NewDecoder(r.Body).Decode(&response) - if err != nil { - t.Fatalf("Failed to decode response: %v", err) - } - - if response.ID != "test-action-1" { - t.Errorf("Expected response ID 'test-action-1', got %s", response.ID) - } - - w.WriteHeader(http.StatusOK) - })) - defer server.Close() - - cfg := testutils.TestConfig() - cfg.APIURL = server.URL - bridgeClient := testutils.NewMockBridgeClient(cfg) - moduleManager := testutils.NewMockModuleManager() - - poller := NewPoller(cfg, bridgeClient, moduleManager) - - response := ActionResponse{ - ID: "test-action-1", - Success: true, - Result: map[string]interface{}{"message": "pong"}, - Duration: 100, - Timestamp: time.Now(), - } - - ctx, cancel := testutils.TestContext() - defer cancel() - - err := poller.sendActionResponse(ctx, response) - if err != nil { - t.Fatalf("sendActionResponse failed: %v", err) - } -} - -func TestPoller_SendActionResponse_ServerError(t *testing.T) { - // Create test server that returns error - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte("Internal server error")) - })) - defer server.Close() - - cfg := testutils.TestConfig() - cfg.APIURL = server.URL - bridgeClient := testutils.NewMockBridgeClient(cfg) - moduleManager := testutils.NewMockModuleManager() - - poller := NewPoller(cfg, bridgeClient, moduleManager) - - response := ActionResponse{ - ID: "test-action-1", - Success: true, - Result: map[string]interface{}{"message": "pong"}, - Duration: 100, - Timestamp: time.Now(), - } - - ctx, cancel := testutils.TestContext() - defer cancel() - - err := poller.sendActionResponse(ctx, response) - if err == nil { - t.Error("Expected error for server error") - } -} - -func TestPoller_UpdatePollInterval(t *testing.T) { - cfg := testutils.TestConfig() - bridgeClient := testutils.NewMockBridgeClient(cfg) - moduleManager := testutils.NewMockModuleManager() - - poller := NewPoller(cfg, bridgeClient, moduleManager) - - // Test normal update - poller.UpdatePollInterval(60) - if poller.config.PollInterval != 60 { - t.Errorf("Expected poll interval 60, got %d", poller.config.PollInterval) - } - - // Test minimum value enforcement - poller.UpdatePollInterval(3) - if poller.config.PollInterval != 5 { - t.Errorf("Expected poll interval 5 (minimum), got %d", poller.config.PollInterval) - } - - // Test with active ticker - poller.ticker = time.NewTicker(30 * time.Second) - poller.UpdatePollInterval(45) - if poller.config.PollInterval != 45 { - t.Errorf("Expected poll interval 45, got %d", poller.config.PollInterval) - } -} - -func TestPoller_GetStats(t *testing.T) { - cfg := testutils.TestConfig() - bridgeClient := testutils.NewMockBridgeClient(cfg) - moduleManager := testutils.NewMockModuleManager() - - poller := NewPoller(cfg, bridgeClient, moduleManager) - - stats := poller.GetStats() - - if stats == nil { - t.Fatal("Expected non-nil stats") - } - - if stats["poll_interval"] != cfg.PollInterval { - t.Errorf("Expected poll_interval %d, got %v", cfg.PollInterval, stats["poll_interval"]) - } - - if stats["community_id"] != cfg.CommunityID { - t.Errorf("Expected community_id %s, got %v", cfg.CommunityID, stats["community_id"]) - } - - if stats["user_id"] != cfg.UserID { - t.Errorf("Expected user_id %s, got %v", cfg.UserID, stats["user_id"]) - } - - if stats["last_poll"] == nil { - t.Error("Expected last_poll to be set") - } - - if stats["uptime"] == nil { - t.Error("Expected uptime to be set") - } -} - -func TestPoller_Start_ContextCancellation(t *testing.T) { - cfg := testutils.TestConfig() - bridgeClient := testutils.NewMockBridgeClient(cfg) - moduleManager := testutils.NewMockModuleManager() - - // Create test server - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - response := PollResponse{ - Actions: []ActionRequest{}, - NextPoll: time.Now().Add(30 * time.Second), - ServerTime: time.Now(), - HasMore: false, - PollCount: 1, - } - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(response) - })) - defer server.Close() - - cfg.APIURL = server.URL - cfg.PollInterval = 1 // Short interval for testing - poller := NewPoller(cfg, bridgeClient, moduleManager) - - // Start poller with short-lived context - ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) - defer cancel() - - err := poller.Start(ctx) - if err != nil { - t.Fatalf("Start failed: %v", err) - } -} - -func TestPoller_Start_PollError(t *testing.T) { - cfg := testutils.TestConfig() - bridgeClient := testutils.NewMockBridgeClient(cfg) - moduleManager := testutils.NewMockModuleManager() - - // Create test server that returns error - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte("Internal server error")) - })) - defer server.Close() - - cfg.APIURL = server.URL - cfg.PollInterval = 1 // Short interval for testing - poller := NewPoller(cfg, bridgeClient, moduleManager) - - // Start poller with short-lived context - ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) - defer cancel() - - // Should not fail even with poll errors - err := poller.Start(ctx) - if err != nil { - t.Fatalf("Start failed: %v", err) - } -} - -func TestPoller_PollForActions_AuthError(t *testing.T) { - cfg := testutils.TestConfig() - bridgeClient := testutils.NewMockBridgeClient(cfg) - moduleManager := testutils.NewMockModuleManager() - - // Configure bridge client to return auth error - bridgeClient.SetAuthError(true) - - poller := NewPoller(cfg, bridgeClient, moduleManager) - - ctx, cancel := testutils.TestContext() - defer cancel() - - err := poller.pollForActions(ctx) - if err == nil { - t.Error("Expected error for auth failure") - } - - if !strings.Contains(err.Error(), "failed to get auth token") { - t.Errorf("Expected auth error, got %v", err) - } -} - -func TestPoller_ProcessAction_Timeout(t *testing.T) { - cfg := testutils.TestConfig() - bridgeClient := testutils.NewMockBridgeClient(cfg) - moduleManager := testutils.NewMockModuleManager() - - // Add slow module - slowModule := testutils.NewMockModule("slow-module") - slowModule.AddAction("slow", func(ctx context.Context, parameters map[string]string) (map[string]interface{}, error) { - time.Sleep(2 * time.Second) - return map[string]interface{}{"result": "done"}, nil - }) - moduleManager.AddModule("slow-module", slowModule) - - // Create response server - responseServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - })) - defer responseServer.Close() - - cfg.APIURL = responseServer.URL - poller := NewPoller(cfg, bridgeClient, moduleManager) - - // Create action with short timeout - action := ActionRequest{ - ID: "timeout-action", - Type: "module_action", - ModuleName: "slow-module", - Action: "slow", - Parameters: map[string]string{}, - UserID: "test-user", - CommunityID: "test-community", - Priority: 1, - Timeout: 1, // 1 second timeout - CreatedAt: time.Now(), - ExpiresAt: time.Now().Add(5 * time.Minute), - } - - ctx, cancel := testutils.TestContext() - defer cancel() - - err := poller.processAction(ctx, action) - if err != nil { - t.Fatalf("processAction failed: %v", err) - } -} \ No newline at end of file diff --git a/Premium/Desktop/internal/scripting/common/types.go b/Premium/Desktop/internal/scripting/common/types.go deleted file mode 100644 index 3d0e67e88..000000000 --- a/Premium/Desktop/internal/scripting/common/types.go +++ /dev/null @@ -1,42 +0,0 @@ -package common - -import ( - "context" - "time" -) - -// ScriptType represents the type of script -type ScriptType string - -const ( - ScriptTypeLua ScriptType = "lua" - ScriptTypePython ScriptType = "python" - ScriptTypePowerShell ScriptType = "powershell" - ScriptTypeBash ScriptType = "bash" -) - -// ScriptConfig represents configuration for script execution -type ScriptConfig struct { - Type ScriptType - Source string - Timeout time.Duration - MaxMemoryMB int - AllowNetwork bool - AllowFileSystem bool - Environment map[string]string -} - -// ScriptResult represents the result of script execution -type ScriptResult struct { - Output string - Error string - ExitCode int - Duration time.Duration -} - -// ScriptEngine defines the interface for script execution -type ScriptEngine interface { - Execute(ctx context.Context, config ScriptConfig) (*ScriptResult, error) - Validate(config ScriptConfig) error - GetType() ScriptType -} diff --git a/Premium/Desktop/internal/scripting/engine.go b/Premium/Desktop/internal/scripting/engine.go deleted file mode 100644 index e3df37135..000000000 --- a/Premium/Desktop/internal/scripting/engine.go +++ /dev/null @@ -1,133 +0,0 @@ -package scripting - -import ( - "context" - "fmt" - "sync" - - "github.com/sirupsen/logrus" - - "waddlebot-bridge/internal/config" - "waddlebot-bridge/internal/scripting/external" - "waddlebot-bridge/internal/scripting/lua" -) - -// Manager manages script execution across different engines -type Manager struct { - config config.ScriptingConfig - engines map[ScriptType]ScriptEngine - logger *logrus.Logger - mu sync.RWMutex -} - -// NewManager creates a new script manager -func NewManager(cfg config.ScriptingConfig, logger *logrus.Logger) (*Manager, error) { - m := &Manager{ - config: cfg, - engines: make(map[ScriptType]ScriptEngine), - logger: logger, - } - - // Initialize Lua engine if enabled - if cfg.EnableLua { - luaEngine := lua.NewEngine(cfg, logger) - m.engines[ScriptTypeLua] = luaEngine - logger.Info("Lua scripting engine enabled") - } - - // Initialize Python engine if enabled - if cfg.EnablePython { - pythonEngine := external.NewPythonEngine(cfg, logger) - m.engines[ScriptTypePython] = pythonEngine - logger.Info("Python scripting engine enabled") - } - - // Initialize PowerShell engine if enabled - if cfg.EnablePowerShell { - psEngine := external.NewPowerShellEngine(cfg, logger) - m.engines[ScriptTypePowerShell] = psEngine - logger.Info("PowerShell scripting engine enabled") - } - - // Initialize Bash engine if enabled - if cfg.EnableBash { - bashEngine := external.NewBashEngine(cfg, logger) - m.engines[ScriptTypeBash] = bashEngine - logger.Info("Bash scripting engine enabled") - } - - if len(m.engines) == 0 { - return nil, fmt.Errorf("no scripting engines enabled") - } - - return m, nil -} - -// Execute executes a script with the appropriate engine -func (m *Manager) Execute(ctx context.Context, config ScriptConfig) (*ScriptResult, error) { - m.mu.RLock() - engine, exists := m.engines[config.Type] - m.mu.RUnlock() - - if !exists { - return nil, fmt.Errorf("script type %s not enabled", config.Type) - } - - // Validate script before execution - if err := engine.Validate(config); err != nil { - return nil, fmt.Errorf("script validation failed: %w", err) - } - - // Execute script - result, err := engine.Execute(ctx, config) - if err != nil { - m.logger.WithFields(logrus.Fields{ - "type": config.Type, - "error": err.Error(), - }).Error("Script execution failed") - return nil, err - } - - m.logger.WithFields(logrus.Fields{ - "type": config.Type, - "duration": result.Duration, - "exit_code": result.ExitCode, - }).Info("Script executed successfully") - - return result, nil -} - -// Validate validates a script configuration -func (m *Manager) Validate(config ScriptConfig) error { - m.mu.RLock() - engine, exists := m.engines[config.Type] - m.mu.RUnlock() - - if !exists { - return fmt.Errorf("script type %s not enabled", config.Type) - } - - return engine.Validate(config) -} - -// GetEnabledTypes returns the list of enabled script types -func (m *Manager) GetEnabledTypes() []ScriptType { - m.mu.RLock() - defer m.mu.RUnlock() - - types := make([]ScriptType, 0, len(m.engines)) - for t := range m.engines { - types = append(types, t) - } - - return types -} - -// IsTypeEnabled checks if a script type is enabled -func (m *Manager) IsTypeEnabled(scriptType ScriptType) bool { - m.mu.RLock() - defer m.mu.RUnlock() - - _, exists := m.engines[scriptType] - return exists -} diff --git a/Premium/Desktop/internal/scripting/external/bash.go b/Premium/Desktop/internal/scripting/external/bash.go deleted file mode 100644 index e734d6599..000000000 --- a/Premium/Desktop/internal/scripting/external/bash.go +++ /dev/null @@ -1,31 +0,0 @@ -package external - -import ( - "github.com/sirupsen/logrus" - - "waddlebot-bridge/internal/config" -) - -// BashEngine implements ScriptEngine for Bash -type BashEngine struct { - *BaseEngine -} - -// NewBashEngine creates a new Bash engine -func NewBashEngine(cfg config.ScriptingConfig, logger *logrus.Logger) *BashEngine { - executable := cfg.BashPath - if executable == "" { - executable = "bash" - } - - return &BashEngine{ - BaseEngine: &BaseEngine{ - config: cfg, - logger: logger, - scriptType: "bash", - executable: executable, - args: []string{"-s"}, // Read from stdin - fileExt: ".sh", - }, - } -} diff --git a/Premium/Desktop/internal/scripting/external/powershell.go b/Premium/Desktop/internal/scripting/external/powershell.go deleted file mode 100644 index cff161f29..000000000 --- a/Premium/Desktop/internal/scripting/external/powershell.go +++ /dev/null @@ -1,36 +0,0 @@ -package external - -import ( - "github.com/sirupsen/logrus" - - "waddlebot-bridge/internal/config" -) - -// PowerShellEngine implements ScriptEngine for PowerShell -type PowerShellEngine struct { - *BaseEngine -} - -// NewPowerShellEngine creates a new PowerShell engine -func NewPowerShellEngine(cfg config.ScriptingConfig, logger *logrus.Logger) *PowerShellEngine { - executable := cfg.PowerShellPath - if executable == "" { - executable = "pwsh" // PowerShell Core - } - - return &PowerShellEngine{ - BaseEngine: &BaseEngine{ - config: cfg, - logger: logger, - scriptType: "powershell", - executable: executable, - args: []string{ - "-NoProfile", - "-NonInteractive", - "-Command", - "-", - }, - fileExt: ".ps1", - }, - } -} diff --git a/Premium/Desktop/internal/scripting/external/python.go b/Premium/Desktop/internal/scripting/external/python.go deleted file mode 100644 index 13dd7fc07..000000000 --- a/Premium/Desktop/internal/scripting/external/python.go +++ /dev/null @@ -1,31 +0,0 @@ -package external - -import ( - "github.com/sirupsen/logrus" - - "waddlebot-bridge/internal/config" -) - -// PythonEngine implements ScriptEngine for Python -type PythonEngine struct { - *BaseEngine -} - -// NewPythonEngine creates a new Python engine -func NewPythonEngine(cfg config.ScriptingConfig, logger *logrus.Logger) *PythonEngine { - executable := cfg.PythonPath - if executable == "" { - executable = "python3" - } - - return &PythonEngine{ - BaseEngine: &BaseEngine{ - config: cfg, - logger: logger, - scriptType: "python", - executable: executable, - args: []string{"-u"}, // Unbuffered output - fileExt: ".py", - }, - } -} diff --git a/Premium/Desktop/internal/scripting/external/runner.go b/Premium/Desktop/internal/scripting/external/runner.go deleted file mode 100644 index 3c3e8467b..000000000 --- a/Premium/Desktop/internal/scripting/external/runner.go +++ /dev/null @@ -1,98 +0,0 @@ -package external - -import ( - "bytes" - "context" - "fmt" - "os/exec" - "time" - - "github.com/sirupsen/logrus" - - "waddlebot-bridge/internal/config" - "waddlebot-bridge/internal/scripting/common" -) - -// BaseEngine provides common functionality for external script engines -type BaseEngine struct { - config config.ScriptingConfig - logger *logrus.Logger - scriptType string - executable string - args []string - fileExt string -} - -// Execute executes an external script -func (e *BaseEngine) Execute(ctx context.Context, config common.ScriptConfig) (*common.ScriptResult, error) { - start := time.Now() - - // Set timeout - timeout := config.Timeout - if timeout == 0 { - timeout = time.Duration(e.config.DefaultTimeout) * time.Second - } - - ctx, cancel := context.WithTimeout(ctx, timeout) - defer cancel() - - // Build command - cmd := exec.CommandContext(ctx, e.executable, e.args...) - - // Set up environment - if config.Environment != nil { - env := make([]string, 0, len(config.Environment)) - for k, v := range config.Environment { - env = append(env, fmt.Sprintf("%s=%s", k, v)) - } - cmd.Env = env - } - - // Capture output - var stdout, stderr bytes.Buffer - cmd.Stdout = &stdout - cmd.Stderr = &stderr - - // Pass script via stdin - cmd.Stdin = bytes.NewBufferString(config.Source) - - // Execute - err := cmd.Run() - - result := &common.ScriptResult{ - Output: stdout.String(), - Error: stderr.String(), - Duration: time.Since(start), - } - - if err != nil { - if exitErr, ok := err.(*exec.ExitError); ok { - result.ExitCode = exitErr.ExitCode() - } else { - result.ExitCode = 1 - } - return result, err - } - - result.ExitCode = 0 - return result, nil -} - -// Validate validates an external script -func (e *BaseEngine) Validate(config common.ScriptConfig) error { - if config.Source == "" { - return fmt.Errorf("script source is empty") - } - - // Check if executable exists - if _, err := exec.LookPath(e.executable); err != nil { - return fmt.Errorf("executable %s not found: %w", e.executable, err) - } - - return nil -} - -// GetType returns the engine type -func (e *BaseEngine) GetType() common.ScriptType { - return common.ScriptType(e.scriptType) -} diff --git a/Premium/Desktop/internal/scripting/lua/api.go b/Premium/Desktop/internal/scripting/lua/api.go deleted file mode 100644 index 05771d818..000000000 --- a/Premium/Desktop/internal/scripting/lua/api.go +++ /dev/null @@ -1,195 +0,0 @@ -package lua - -import ( - "time" - - lua "github.com/yuin/gopher-lua" -) - -// loadWaddleBotAPI loads WaddleBot-specific API functions into Lua -func (e *Engine) loadWaddleBotAPI(L *lua.LState) { - // Create log module - logModule := L.NewTable() - L.SetFuncs(logModule, map[string]lua.LGFunction{ - "info": e.luaLogInfo, - "warn": e.luaLogWarn, - "error": e.luaLogError, - "debug": e.luaLogDebug, - }) - L.SetGlobal("log", logModule) - - // Create storage module (simple key-value) - storageModule := L.NewTable() - L.SetFuncs(storageModule, map[string]lua.LGFunction{ - "get": e.luaStorageGet, - "set": e.luaStorageSet, - }) - L.SetGlobal("storage", storageModule) - - // Create utility functions - L.SetGlobal("sleep", L.NewFunction(e.luaSleep)) - L.SetGlobal("time", L.NewFunction(e.luaTime)) - - // Create OBS module (if available) - obsModule := L.NewTable() - L.SetFuncs(obsModule, map[string]lua.LGFunction{ - "connect": e.luaOBSConnect, - "switch_scene": e.luaOBSSwitchScene, - "set_source_visible": e.luaOBSSetSourceVisible, - "start_stream": e.luaOBSStartStream, - "stop_stream": e.luaOBSStopStream, - "start_recording": e.luaOBSStartRecording, - "stop_recording": e.luaOBSStopRecording, - }) - L.SetGlobal("obs", obsModule) - - // Create bridge module - bridgeModule := L.NewTable() - L.SetFuncs(bridgeModule, map[string]lua.LGFunction{ - "send_response": e.luaBridgeSendResponse, - "trigger": e.luaBridgeTrigger, - }) - L.SetGlobal("bridge", bridgeModule) -} - -// Logging functions - -func (e *Engine) luaLogInfo(L *lua.LState) int { - msg := L.ToString(1) - e.logger.Info("[Lua] " + msg) - return 0 -} - -func (e *Engine) luaLogWarn(L *lua.LState) int { - msg := L.ToString(1) - e.logger.Warn("[Lua] " + msg) - return 0 -} - -func (e *Engine) luaLogError(L *lua.LState) int { - msg := L.ToString(1) - e.logger.Error("[Lua] " + msg) - return 0 -} - -func (e *Engine) luaLogDebug(L *lua.LState) int { - msg := L.ToString(1) - e.logger.Debug("[Lua] " + msg) - return 0 -} - -// Storage functions (in-memory for now) - -var scriptStorage = make(map[string]string) - -func (e *Engine) luaStorageGet(L *lua.LState) int { - key := L.ToString(1) - value, exists := scriptStorage[key] - if !exists { - L.Push(lua.LNil) - return 1 - } - L.Push(lua.LString(value)) - return 1 -} - -func (e *Engine) luaStorageSet(L *lua.LState) int { - key := L.ToString(1) - value := L.ToString(2) - scriptStorage[key] = value - return 0 -} - -// Utility functions - -func (e *Engine) luaSleep(L *lua.LState) int { - ms := L.ToInt(1) - time.Sleep(time.Duration(ms) * time.Millisecond) - return 0 -} - -func (e *Engine) luaTime(L *lua.LState) int { - L.Push(lua.LNumber(time.Now().Unix())) - return 1 -} - -// OBS functions (stubs - will be connected to actual OBS client) - -func (e *Engine) luaOBSConnect(L *lua.LState) int { - // TODO: Connect to OBS client - e.logger.Debug("[Lua] OBS connect called") - L.Push(lua.LBool(true)) - return 1 -} - -func (e *Engine) luaOBSSwitchScene(L *lua.LState) int { - sceneName := L.ToString(1) - // TODO: Call OBS client - e.logger.WithField("scene", sceneName).Debug("[Lua] OBS switch scene called") - L.Push(lua.LBool(true)) - return 1 -} - -func (e *Engine) luaOBSSetSourceVisible(L *lua.LState) int { - sceneName := L.ToString(1) - sourceName := L.ToString(2) - visible := L.ToBool(3) - // TODO: Call OBS client - e.logger.WithField("scene", sceneName). - WithField("source", sourceName). - WithField("visible", visible). - Debug("[Lua] OBS set source visible called") - L.Push(lua.LBool(true)) - return 1 -} - -func (e *Engine) luaOBSStartStream(L *lua.LState) int { - // TODO: Call OBS client - e.logger.Debug("[Lua] OBS start stream called") - L.Push(lua.LBool(true)) - return 1 -} - -func (e *Engine) luaOBSStopStream(L *lua.LState) int { - // TODO: Call OBS client - e.logger.Debug("[Lua] OBS stop stream called") - L.Push(lua.LBool(true)) - return 1 -} - -func (e *Engine) luaOBSStartRecording(L *lua.LState) int { - // TODO: Call OBS client - e.logger.Debug("[Lua] OBS start recording called") - L.Push(lua.LBool(true)) - return 1 -} - -func (e *Engine) luaOBSStopRecording(L *lua.LState) int { - // TODO: Call OBS client - e.logger.Debug("[Lua] OBS stop recording called") - L.Push(lua.LBool(true)) - return 1 -} - -// Bridge functions (stubs - will be connected to bridge client) - -func (e *Engine) luaBridgeSendResponse(L *lua.LState) int { - data := L.ToString(1) - // TODO: Send via bridge client - e.logger.WithField("data", data).Debug("[Lua] Bridge send response called") - L.Push(lua.LBool(true)) - return 1 -} - -func (e *Engine) luaBridgeTrigger(L *lua.LState) int { - module := L.ToString(1) - action := L.ToString(2) - params := L.ToString(3) - // TODO: Trigger via bridge client - e.logger.WithField("module", module). - WithField("action", action). - WithField("params", params). - Debug("[Lua] Bridge trigger called") - L.Push(lua.LBool(true)) - return 1 -} diff --git a/Premium/Desktop/internal/scripting/lua/runtime.go b/Premium/Desktop/internal/scripting/lua/runtime.go deleted file mode 100644 index 9c798c296..000000000 --- a/Premium/Desktop/internal/scripting/lua/runtime.go +++ /dev/null @@ -1,146 +0,0 @@ -package lua - -import ( - "context" - "fmt" - "time" - - lua "github.com/yuin/gopher-lua" - "github.com/sirupsen/logrus" - - "waddlebot-bridge/internal/config" - "waddlebot-bridge/internal/scripting/common" -) - -// Engine implements ScriptEngine for Lua -type Engine struct { - config config.ScriptingConfig - logger *logrus.Logger -} - -// NewEngine creates a new Lua engine -func NewEngine(cfg config.ScriptingConfig, logger *logrus.Logger) *Engine { - return &Engine{ - config: cfg, - logger: logger, - } -} - -// Execute executes a Lua script -func (e *Engine) Execute(ctx context.Context, config common.ScriptConfig) (*common.ScriptResult, error) { - start := time.Now() - - // Create new Lua state with memory limit - L := lua.NewState(lua.Options{ - CallStackSize: 120, - RegistrySize: 1024, - SkipOpenLibs: false, - IncludeGoStackTrace: false, - }) - defer L.Close() - - // Load safe libraries - e.loadSafeLibraries(L) - - // Load WaddleBot API - e.loadWaddleBotAPI(L) - - // Set timeout - timeout := config.Timeout - if timeout == 0 { - timeout = time.Duration(e.config.DefaultTimeout) * time.Second - } - - ctx, cancel := context.WithTimeout(ctx, timeout) - defer cancel() - - // Set up context cancellation - L.SetContext(ctx) - - // Execute script - result := &common.ScriptResult{} - - if err := L.DoString(config.Source); err != nil { - result.Error = err.Error() - result.ExitCode = 1 - result.Duration = time.Since(start) - return result, err - } - - // Capture output from global _OUTPUT variable if set - if output := L.GetGlobal("_OUTPUT"); output != lua.LNil { - result.Output = output.String() - } - - result.ExitCode = 0 - result.Duration = time.Since(start) - - return result, nil -} - -// Validate validates a Lua script -func (e *Engine) Validate(config common.ScriptConfig) error { - if config.Source == "" { - return fmt.Errorf("script source is empty") - } - - // Try to compile the script - L := lua.NewState() - defer L.Close() - - _, err := L.LoadString(config.Source) - if err != nil { - return fmt.Errorf("syntax error: %w", err) - } - - return nil -} - -// GetType returns the engine type -func (e *Engine) GetType() common.ScriptType { - return common.ScriptTypeLua -} - -// loadSafeLibraries loads only safe Lua standard libraries -func (e *Engine) loadSafeLibraries(L *lua.LState) { - // Load safe base functions - for _, pair := range []struct { - n string - f lua.LGFunction - }{ - {lua.LoadLibName, lua.OpenPackage}, - {lua.BaseLibName, lua.OpenBase}, - {lua.TabLibName, lua.OpenTable}, - {lua.StringLibName, lua.OpenString}, - {lua.MathLibName, lua.OpenMath}, - } { - if err := L.CallByParam(lua.P{ - Fn: L.NewFunction(pair.f), - NRet: 0, - Protect: true, - }, lua.LString(pair.n)); err != nil { - e.logger.WithError(err).Error("Failed to load Lua library") - } - } - - // Remove unsafe functions - unsafeFunctions := []string{ - "dofile", - "loadfile", - "load", - "loadstring", - } - - for _, fn := range unsafeFunctions { - L.SetGlobal(fn, lua.LNil) - } - - // Optionally load IO/OS with restrictions - if e.config.AllowFileSystem { - lua.OpenIo(L) - } - - if e.config.AllowNetwork { - // Network access would be through custom API, not standard library - } -} diff --git a/Premium/Desktop/internal/scripting/types.go b/Premium/Desktop/internal/scripting/types.go deleted file mode 100644 index 3c1a74e9e..000000000 --- a/Premium/Desktop/internal/scripting/types.go +++ /dev/null @@ -1,21 +0,0 @@ -package scripting - -import ( - "waddlebot-bridge/internal/scripting/common" -) - -// Re-export types from common to maintain API -type ( - ScriptType = common.ScriptType - ScriptConfig = common.ScriptConfig - ScriptResult = common.ScriptResult - ScriptEngine = common.ScriptEngine -) - -// Re-export constants -const ( - ScriptTypeLua = common.ScriptTypeLua - ScriptTypePython = common.ScriptTypePython - ScriptTypePowerShell = common.ScriptTypePowerShell - ScriptTypeBash = common.ScriptTypeBash -) diff --git a/Premium/Desktop/internal/server/server.go b/Premium/Desktop/internal/server/server.go deleted file mode 100644 index 744a3a00f..000000000 --- a/Premium/Desktop/internal/server/server.go +++ /dev/null @@ -1,488 +0,0 @@ -package server - -import ( - "context" - "encoding/json" - "fmt" - "html/template" - "net/http" - "time" - - "github.com/gorilla/mux" - "github.com/sirupsen/logrus" - "waddlebot-bridge/internal/auth" - "waddlebot-bridge/internal/bridge" - "waddlebot-bridge/internal/config" - "waddlebot-bridge/internal/logger" -) - -// WebServer handles the web interface for authentication -type WebServer struct { - config *config.Config - authenticator *auth.WebAuthnManager - bridgeClient *bridge.Client - logger *logrus.Logger - server *http.Server -} - -// NewWebServer creates a new web server -func NewWebServer(cfg *config.Config, authenticator *auth.WebAuthnManager, bridgeClient *bridge.Client) *WebServer { - return &WebServer{ - config: cfg, - authenticator: authenticator, - bridgeClient: bridgeClient, - logger: logger.GetLogger(), - } -} - -// Start starts the web server -func (s *WebServer) Start(ctx context.Context) error { - router := mux.NewRouter() - - // Static files - router.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir("./web/static/")))) - - // Authentication routes - router.HandleFunc("/", s.handleIndex).Methods("GET") - router.HandleFunc("/auth/register/start", s.handleRegisterStart).Methods("POST") - router.HandleFunc("/auth/register/complete", s.handleRegisterComplete).Methods("POST") - router.HandleFunc("/auth/login/start", s.handleLoginStart).Methods("POST") - router.HandleFunc("/auth/login/complete", s.handleLoginComplete).Methods("POST") - router.HandleFunc("/auth/logout", s.handleLogout).Methods("POST") - - // Status routes - router.HandleFunc("/status", s.handleStatus).Methods("GET") - router.HandleFunc("/health", s.handleHealth).Methods("GET") - - // Create server - s.server = &http.Server{ - Addr: fmt.Sprintf("%s:%d", s.config.WebHost, s.config.WebPort), - Handler: router, - } - - s.logger.WithFields(logrus.Fields{ - "host": s.config.WebHost, - "port": s.config.WebPort, - }).Info("Starting web server") - - // Start server in goroutine - go func() { - if err := s.server.ListenAndServe(); err != nil && err != http.ErrServerClosed { - s.logger.WithError(err).Error("Web server error") - } - }() - - // Wait for context cancellation - <-ctx.Done() - - // Graceful shutdown - shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - - return s.server.Shutdown(shutdownCtx) -} - -// handleIndex serves the main page -func (s *WebServer) handleIndex(w http.ResponseWriter, r *http.Request) { - tmpl := ` - - - - WaddleBot Premium Desktop Bridge - - - -
-
- -

Local System Integration Platform

-
- -
- Status: Checking authentication... -
- -
-
-

Authentication Required

-

Please authenticate using WebAuthn to connect your bridge to WaddleBot.

- -
- - -
- -
- - -
- - - -
- - -
- -
-

Configuration

-
- API URL: {{.APIURL}}
- Poll Interval: {{.PollInterval}} seconds
- Web Port: {{.WebPort}}
- Data Directory: {{.DataDir}} -
-
- - -
- - - - - ` - - t, err := template.New("index").Parse(tmpl) - if err != nil { - http.Error(w, "Internal Server Error", http.StatusInternalServerError) - return - } - - data := struct { - APIURL string - PollInterval int - WebPort int - DataDir string - }{ - APIURL: s.config.APIURL, - PollInterval: s.config.PollInterval, - WebPort: s.config.WebPort, - DataDir: s.config.DataDir, - } - - w.Header().Set("Content-Type", "text/html") - t.Execute(w, data) -} - -// handleRegisterStart handles the start of WebAuthn registration -func (s *WebServer) handleRegisterStart(w http.ResponseWriter, r *http.Request) { - var req struct { - UserID string `json:"user_id"` - CommunityID string `json:"community_id"` - } - - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(w, "Invalid request", http.StatusBadRequest) - return - } - - creation, err := s.authenticator.StartRegistration(req.UserID, req.CommunityID) - if err != nil { - s.logger.WithError(err).Error("Failed to start registration") - http.Error(w, fmt.Sprintf("Registration failed: %v", err), http.StatusInternalServerError) - return - } - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]interface{}{ - "credentialCreationOptions": creation, - }) -} - -// handleRegisterComplete handles the completion of WebAuthn registration -func (s *WebServer) handleRegisterComplete(w http.ResponseWriter, r *http.Request) { - var req struct { - UserID string `json:"user_id"` - Credential interface{} `json:"credential"` - } - - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(w, "Invalid request", http.StatusBadRequest) - return - } - - credentialData, err := json.Marshal(req.Credential) - if err != nil { - http.Error(w, "Invalid credential format", http.StatusBadRequest) - return - } - - session, err := s.authenticator.CompleteRegistration(req.UserID, credentialData) - if err != nil { - s.logger.WithError(err).Error("Failed to complete registration") - http.Error(w, fmt.Sprintf("Registration failed: %v", err), http.StatusInternalServerError) - return - } - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]interface{}{ - "success": true, - "session_id": session.ID, - }) -} - -// handleLoginStart handles the start of WebAuthn login -func (s *WebServer) handleLoginStart(w http.ResponseWriter, r *http.Request) { - var req struct { - UserID string `json:"user_id"` - } - - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(w, "Invalid request", http.StatusBadRequest) - return - } - - assertion, err := s.authenticator.StartAuthentication(req.UserID) - if err != nil { - s.logger.WithError(err).Error("Failed to start authentication") - http.Error(w, fmt.Sprintf("Authentication failed: %v", err), http.StatusInternalServerError) - return - } - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]interface{}{ - "credentialRequestOptions": assertion, - }) -} - -// handleLoginComplete handles the completion of WebAuthn login -func (s *WebServer) handleLoginComplete(w http.ResponseWriter, r *http.Request) { - var req struct { - UserID string `json:"user_id"` - Credential interface{} `json:"credential"` - } - - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(w, "Invalid request", http.StatusBadRequest) - return - } - - credentialData, err := json.Marshal(req.Credential) - if err != nil { - http.Error(w, "Invalid credential format", http.StatusBadRequest) - return - } - - session, err := s.authenticator.CompleteAuthentication(req.UserID, credentialData) - if err != nil { - s.logger.WithError(err).Error("Failed to complete authentication") - http.Error(w, fmt.Sprintf("Authentication failed: %v", err), http.StatusInternalServerError) - return - } - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]interface{}{ - "success": true, - "session_id": session.ID, - }) -} - -// handleLogout handles user logout -func (s *WebServer) handleLogout(w http.ResponseWriter, r *http.Request) { - session := s.authenticator.GetCurrentSession() - if session != nil { - s.authenticator.RevokeSession(session.ID) - } - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]interface{}{ - "success": true, - }) -} - -// handleStatus returns the current authentication status -func (s *WebServer) handleStatus(w http.ResponseWriter, r *http.Request) { - session := s.authenticator.GetCurrentSession() - authenticated := session != nil - - status := map[string]interface{}{ - "authenticated": authenticated, - "bridge_status": "running", - } - - if authenticated { - status["user_id"] = session.UserID - status["community_id"] = session.CommunityID - status["session_expires"] = session.ExpiresAt - } - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(status) -} - -// handleHealth returns the health status -func (s *WebServer) handleHealth(w http.ResponseWriter, r *http.Request) { - health := map[string]interface{}{ - "status": "healthy", - "timestamp": time.Now(), - "version": "1.0.0", - } - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(health) -} \ No newline at end of file diff --git a/Premium/Desktop/internal/server/server_test.go b/Premium/Desktop/internal/server/server_test.go deleted file mode 100644 index fd3258720..000000000 --- a/Premium/Desktop/internal/server/server_test.go +++ /dev/null @@ -1,736 +0,0 @@ -package server - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "net/http" - "net/http/httptest" - "strings" - "testing" - "time" - - "waddlebot-bridge/internal/testutils" -) - -func TestNewWebServer(t *testing.T) { - cfg := testutils.TestConfig() - authenticator := testutils.NewMockWebAuthnManager() - bridgeClient := testutils.NewMockBridgeClient(cfg) - - server := NewWebServer(cfg, authenticator, bridgeClient) - - if server == nil { - t.Fatal("Expected non-nil server") - } - - if server.config != cfg { - t.Error("Expected config to be set") - } - - if server.authenticator != authenticator { - t.Error("Expected authenticator to be set") - } - - if server.bridgeClient != bridgeClient { - t.Error("Expected bridgeClient to be set") - } - - if server.logger == nil { - t.Error("Expected logger to be set") - } -} - -func TestWebServer_HandleIndex(t *testing.T) { - cfg := testutils.TestConfig() - authenticator := testutils.NewMockWebAuthnManager() - bridgeClient := testutils.NewMockBridgeClient(cfg) - - server := NewWebServer(cfg, authenticator, bridgeClient) - - req := httptest.NewRequest("GET", "/", nil) - w := httptest.NewRecorder() - - server.handleIndex(w, req) - - if w.Code != http.StatusOK { - t.Errorf("Expected status 200, got %d", w.Code) - } - - contentType := w.Header().Get("Content-Type") - if contentType != "text/html" { - t.Errorf("Expected Content-Type 'text/html', got %s", contentType) - } - - body := w.Body.String() - if !strings.Contains(body, "WaddleBot Premium Desktop Bridge") { - t.Error("Expected body to contain title") - } - - if !strings.Contains(body, cfg.APIURL) { - t.Error("Expected body to contain API URL") - } - - if !strings.Contains(body, fmt.Sprintf("%d", cfg.PollInterval)) { - t.Error("Expected body to contain poll interval") - } -} - -func TestWebServer_HandleRegisterStart(t *testing.T) { - cfg := testutils.TestConfig() - authenticator := testutils.NewMockWebAuthnManager() - bridgeClient := testutils.NewMockBridgeClient(cfg) - - server := NewWebServer(cfg, authenticator, bridgeClient) - - // Test valid request - reqBody := map[string]string{ - "user_id": "test-user", - "community_id": "test-community", - } - reqJSON, _ := json.Marshal(reqBody) - - req := httptest.NewRequest("POST", "/auth/register/start", bytes.NewBuffer(reqJSON)) - req.Header.Set("Content-Type", "application/json") - w := httptest.NewRecorder() - - server.handleRegisterStart(w, req) - - if w.Code != http.StatusOK { - t.Errorf("Expected status 200, got %d", w.Code) - } - - contentType := w.Header().Get("Content-Type") - if contentType != "application/json" { - t.Errorf("Expected Content-Type 'application/json', got %s", contentType) - } - - var response map[string]interface{} - err := json.NewDecoder(w.Body).Decode(&response) - if err != nil { - t.Fatalf("Failed to decode response: %v", err) - } - - if _, exists := response["credentialCreationOptions"]; !exists { - t.Error("Expected credentialCreationOptions in response") - } -} - -func TestWebServer_HandleRegisterStart_InvalidRequest(t *testing.T) { - cfg := testutils.TestConfig() - authenticator := testutils.NewMockWebAuthnManager() - bridgeClient := testutils.NewMockBridgeClient(cfg) - - server := NewWebServer(cfg, authenticator, bridgeClient) - - // Test invalid JSON - req := httptest.NewRequest("POST", "/auth/register/start", bytes.NewBuffer([]byte("invalid json"))) - req.Header.Set("Content-Type", "application/json") - w := httptest.NewRecorder() - - server.handleRegisterStart(w, req) - - if w.Code != http.StatusBadRequest { - t.Errorf("Expected status 400, got %d", w.Code) - } -} - -func TestWebServer_HandleRegisterStart_RegistrationError(t *testing.T) { - cfg := testutils.TestConfig() - authenticator := testutils.NewMockWebAuthnManager() - bridgeClient := testutils.NewMockBridgeClient(cfg) - - // Configure authenticator to return error - authenticator.SetRegistrationError(true) - - server := NewWebServer(cfg, authenticator, bridgeClient) - - reqBody := map[string]string{ - "user_id": "test-user", - "community_id": "test-community", - } - reqJSON, _ := json.Marshal(reqBody) - - req := httptest.NewRequest("POST", "/auth/register/start", bytes.NewBuffer(reqJSON)) - req.Header.Set("Content-Type", "application/json") - w := httptest.NewRecorder() - - server.handleRegisterStart(w, req) - - if w.Code != http.StatusInternalServerError { - t.Errorf("Expected status 500, got %d", w.Code) - } -} - -func TestWebServer_HandleRegisterComplete(t *testing.T) { - cfg := testutils.TestConfig() - authenticator := testutils.NewMockWebAuthnManager() - bridgeClient := testutils.NewMockBridgeClient(cfg) - - server := NewWebServer(cfg, authenticator, bridgeClient) - - // Test valid request - reqBody := map[string]interface{}{ - "user_id": "test-user", - "credential": map[string]interface{}{ - "id": "test-credential-id", - "type": "public-key", - }, - } - reqJSON, _ := json.Marshal(reqBody) - - req := httptest.NewRequest("POST", "/auth/register/complete", bytes.NewBuffer(reqJSON)) - req.Header.Set("Content-Type", "application/json") - w := httptest.NewRecorder() - - server.handleRegisterComplete(w, req) - - if w.Code != http.StatusOK { - t.Errorf("Expected status 200, got %d", w.Code) - } - - var response map[string]interface{} - err := json.NewDecoder(w.Body).Decode(&response) - if err != nil { - t.Fatalf("Failed to decode response: %v", err) - } - - if success, exists := response["success"]; !exists || !success.(bool) { - t.Error("Expected success to be true") - } - - if _, exists := response["session_id"]; !exists { - t.Error("Expected session_id in response") - } -} - -func TestWebServer_HandleRegisterComplete_InvalidRequest(t *testing.T) { - cfg := testutils.TestConfig() - authenticator := testutils.NewMockWebAuthnManager() - bridgeClient := testutils.NewMockBridgeClient(cfg) - - server := NewWebServer(cfg, authenticator, bridgeClient) - - // Test invalid JSON - req := httptest.NewRequest("POST", "/auth/register/complete", bytes.NewBuffer([]byte("invalid json"))) - req.Header.Set("Content-Type", "application/json") - w := httptest.NewRecorder() - - server.handleRegisterComplete(w, req) - - if w.Code != http.StatusBadRequest { - t.Errorf("Expected status 400, got %d", w.Code) - } -} - -func TestWebServer_HandleLoginStart(t *testing.T) { - cfg := testutils.TestConfig() - authenticator := testutils.NewMockWebAuthnManager() - bridgeClient := testutils.NewMockBridgeClient(cfg) - - server := NewWebServer(cfg, authenticator, bridgeClient) - - // Test valid request - reqBody := map[string]string{ - "user_id": "test-user", - } - reqJSON, _ := json.Marshal(reqBody) - - req := httptest.NewRequest("POST", "/auth/login/start", bytes.NewBuffer(reqJSON)) - req.Header.Set("Content-Type", "application/json") - w := httptest.NewRecorder() - - server.handleLoginStart(w, req) - - if w.Code != http.StatusOK { - t.Errorf("Expected status 200, got %d", w.Code) - } - - var response map[string]interface{} - err := json.NewDecoder(w.Body).Decode(&response) - if err != nil { - t.Fatalf("Failed to decode response: %v", err) - } - - if _, exists := response["credentialRequestOptions"]; !exists { - t.Error("Expected credentialRequestOptions in response") - } -} - -func TestWebServer_HandleLoginStart_InvalidRequest(t *testing.T) { - cfg := testutils.TestConfig() - authenticator := testutils.NewMockWebAuthnManager() - bridgeClient := testutils.NewMockBridgeClient(cfg) - - server := NewWebServer(cfg, authenticator, bridgeClient) - - // Test invalid JSON - req := httptest.NewRequest("POST", "/auth/login/start", bytes.NewBuffer([]byte("invalid json"))) - req.Header.Set("Content-Type", "application/json") - w := httptest.NewRecorder() - - server.handleLoginStart(w, req) - - if w.Code != http.StatusBadRequest { - t.Errorf("Expected status 400, got %d", w.Code) - } -} - -func TestWebServer_HandleLoginComplete(t *testing.T) { - cfg := testutils.TestConfig() - authenticator := testutils.NewMockWebAuthnManager() - bridgeClient := testutils.NewMockBridgeClient(cfg) - - server := NewWebServer(cfg, authenticator, bridgeClient) - - // Test valid request - reqBody := map[string]interface{}{ - "user_id": "test-user", - "credential": map[string]interface{}{ - "id": "test-credential-id", - "type": "public-key", - }, - } - reqJSON, _ := json.Marshal(reqBody) - - req := httptest.NewRequest("POST", "/auth/login/complete", bytes.NewBuffer(reqJSON)) - req.Header.Set("Content-Type", "application/json") - w := httptest.NewRecorder() - - server.handleLoginComplete(w, req) - - if w.Code != http.StatusOK { - t.Errorf("Expected status 200, got %d", w.Code) - } - - var response map[string]interface{} - err := json.NewDecoder(w.Body).Decode(&response) - if err != nil { - t.Fatalf("Failed to decode response: %v", err) - } - - if success, exists := response["success"]; !exists || !success.(bool) { - t.Error("Expected success to be true") - } - - if _, exists := response["session_id"]; !exists { - t.Error("Expected session_id in response") - } -} - -func TestWebServer_HandleLoginComplete_InvalidRequest(t *testing.T) { - cfg := testutils.TestConfig() - authenticator := testutils.NewMockWebAuthnManager() - bridgeClient := testutils.NewMockBridgeClient(cfg) - - server := NewWebServer(cfg, authenticator, bridgeClient) - - // Test invalid JSON - req := httptest.NewRequest("POST", "/auth/login/complete", bytes.NewBuffer([]byte("invalid json"))) - req.Header.Set("Content-Type", "application/json") - w := httptest.NewRecorder() - - server.handleLoginComplete(w, req) - - if w.Code != http.StatusBadRequest { - t.Errorf("Expected status 400, got %d", w.Code) - } -} - -func TestWebServer_HandleLogout(t *testing.T) { - cfg := testutils.TestConfig() - authenticator := testutils.NewMockWebAuthnManager() - bridgeClient := testutils.NewMockBridgeClient(cfg) - - server := NewWebServer(cfg, authenticator, bridgeClient) - - // Add a session - authenticator.AddSession("test-session", "test-user", "test-community") - - req := httptest.NewRequest("POST", "/auth/logout", nil) - w := httptest.NewRecorder() - - server.handleLogout(w, req) - - if w.Code != http.StatusOK { - t.Errorf("Expected status 200, got %d", w.Code) - } - - var response map[string]interface{} - err := json.NewDecoder(w.Body).Decode(&response) - if err != nil { - t.Fatalf("Failed to decode response: %v", err) - } - - if success, exists := response["success"]; !exists || !success.(bool) { - t.Error("Expected success to be true") - } -} - -func TestWebServer_HandleLogout_NoSession(t *testing.T) { - cfg := testutils.TestConfig() - authenticator := testutils.NewMockWebAuthnManager() - bridgeClient := testutils.NewMockBridgeClient(cfg) - - server := NewWebServer(cfg, authenticator, bridgeClient) - - req := httptest.NewRequest("POST", "/auth/logout", nil) - w := httptest.NewRecorder() - - server.handleLogout(w, req) - - if w.Code != http.StatusOK { - t.Errorf("Expected status 200, got %d", w.Code) - } - - var response map[string]interface{} - err := json.NewDecoder(w.Body).Decode(&response) - if err != nil { - t.Fatalf("Failed to decode response: %v", err) - } - - if success, exists := response["success"]; !exists || !success.(bool) { - t.Error("Expected success to be true even with no session") - } -} - -func TestWebServer_HandleStatus_Authenticated(t *testing.T) { - cfg := testutils.TestConfig() - authenticator := testutils.NewMockWebAuthnManager() - bridgeClient := testutils.NewMockBridgeClient(cfg) - - server := NewWebServer(cfg, authenticator, bridgeClient) - - // Add a session - authenticator.AddSession("test-session", "test-user", "test-community") - - req := httptest.NewRequest("GET", "/status", nil) - w := httptest.NewRecorder() - - server.handleStatus(w, req) - - if w.Code != http.StatusOK { - t.Errorf("Expected status 200, got %d", w.Code) - } - - var response map[string]interface{} - err := json.NewDecoder(w.Body).Decode(&response) - if err != nil { - t.Fatalf("Failed to decode response: %v", err) - } - - if authenticated, exists := response["authenticated"]; !exists || !authenticated.(bool) { - t.Error("Expected authenticated to be true") - } - - if bridgeStatus, exists := response["bridge_status"]; !exists || bridgeStatus != "running" { - t.Error("Expected bridge_status to be 'running'") - } - - if userID, exists := response["user_id"]; !exists || userID != "test-user" { - t.Error("Expected user_id to be 'test-user'") - } - - if communityID, exists := response["community_id"]; !exists || communityID != "test-community" { - t.Error("Expected community_id to be 'test-community'") - } - - if _, exists := response["session_expires"]; !exists { - t.Error("Expected session_expires to be present") - } -} - -func TestWebServer_HandleStatus_NotAuthenticated(t *testing.T) { - cfg := testutils.TestConfig() - authenticator := testutils.NewMockWebAuthnManager() - bridgeClient := testutils.NewMockBridgeClient(cfg) - - server := NewWebServer(cfg, authenticator, bridgeClient) - - req := httptest.NewRequest("GET", "/status", nil) - w := httptest.NewRecorder() - - server.handleStatus(w, req) - - if w.Code != http.StatusOK { - t.Errorf("Expected status 200, got %d", w.Code) - } - - var response map[string]interface{} - err := json.NewDecoder(w.Body).Decode(&response) - if err != nil { - t.Fatalf("Failed to decode response: %v", err) - } - - if authenticated, exists := response["authenticated"]; !exists || authenticated.(bool) { - t.Error("Expected authenticated to be false") - } - - if bridgeStatus, exists := response["bridge_status"]; !exists || bridgeStatus != "running" { - t.Error("Expected bridge_status to be 'running'") - } - - // These should not be present when not authenticated - if _, exists := response["user_id"]; exists { - t.Error("Expected user_id to not be present when not authenticated") - } - - if _, exists := response["community_id"]; exists { - t.Error("Expected community_id to not be present when not authenticated") - } - - if _, exists := response["session_expires"]; exists { - t.Error("Expected session_expires to not be present when not authenticated") - } -} - -func TestWebServer_HandleHealth(t *testing.T) { - cfg := testutils.TestConfig() - authenticator := testutils.NewMockWebAuthnManager() - bridgeClient := testutils.NewMockBridgeClient(cfg) - - server := NewWebServer(cfg, authenticator, bridgeClient) - - req := httptest.NewRequest("GET", "/health", nil) - w := httptest.NewRecorder() - - server.handleHealth(w, req) - - if w.Code != http.StatusOK { - t.Errorf("Expected status 200, got %d", w.Code) - } - - var response map[string]interface{} - err := json.NewDecoder(w.Body).Decode(&response) - if err != nil { - t.Fatalf("Failed to decode response: %v", err) - } - - if status, exists := response["status"]; !exists || status != "healthy" { - t.Error("Expected status to be 'healthy'") - } - - if version, exists := response["version"]; !exists || version != "1.0.0" { - t.Error("Expected version to be '1.0.0'") - } - - if _, exists := response["timestamp"]; !exists { - t.Error("Expected timestamp to be present") - } -} - -func TestWebServer_Start_Shutdown(t *testing.T) { - cfg := testutils.TestConfig() - cfg.WebPort = 0 // Use available port - - authenticator := testutils.NewMockWebAuthnManager() - bridgeClient := testutils.NewMockBridgeClient(cfg) - - server := NewWebServer(cfg, authenticator, bridgeClient) - - // Start server with short-lived context - ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) - defer cancel() - - err := server.Start(ctx) - if err != nil { - t.Fatalf("Server start failed: %v", err) - } - - // Server should have shut down gracefully - if server.server == nil { - t.Error("Expected server to be initialized") - } -} - -func TestWebServer_Routes(t *testing.T) { - cfg := testutils.TestConfig() - authenticator := testutils.NewMockWebAuthnManager() - bridgeClient := testutils.NewMockBridgeClient(cfg) - - server := NewWebServer(cfg, authenticator, bridgeClient) - - // Test route setup by checking various endpoints - testCases := []struct { - method string - path string - body string - expect int - }{ - {"GET", "/", "", http.StatusOK}, - {"GET", "/health", "", http.StatusOK}, - {"GET", "/status", "", http.StatusOK}, - {"POST", "/auth/register/start", `{"user_id":"test","community_id":"test"}`, http.StatusOK}, - {"POST", "/auth/login/start", `{"user_id":"test"}`, http.StatusOK}, - {"POST", "/auth/logout", "", http.StatusOK}, - {"GET", "/nonexistent", "", http.StatusNotFound}, - } - - for _, tc := range testCases { - t.Run(fmt.Sprintf("%s %s", tc.method, tc.path), func(t *testing.T) { - var req *http.Request - if tc.body != "" { - req = httptest.NewRequest(tc.method, tc.path, strings.NewReader(tc.body)) - req.Header.Set("Content-Type", "application/json") - } else { - req = httptest.NewRequest(tc.method, tc.path, nil) - } - - w := httptest.NewRecorder() - - // Create a temporary router to test routing - router := http.NewServeMux() - router.HandleFunc("/", server.handleIndex) - router.HandleFunc("/health", server.handleHealth) - router.HandleFunc("/status", server.handleStatus) - router.HandleFunc("/auth/register/start", server.handleRegisterStart) - router.HandleFunc("/auth/login/start", server.handleLoginStart) - router.HandleFunc("/auth/logout", server.handleLogout) - - router.ServeHTTP(w, req) - - if w.Code != tc.expect { - t.Errorf("Expected status %d, got %d", tc.expect, w.Code) - } - }) - } -} - -func TestWebServer_ContentTypes(t *testing.T) { - cfg := testutils.TestConfig() - authenticator := testutils.NewMockWebAuthnManager() - bridgeClient := testutils.NewMockBridgeClient(cfg) - - server := NewWebServer(cfg, authenticator, bridgeClient) - - testCases := []struct { - handler func(w http.ResponseWriter, r *http.Request) - path string - method string - body string - contentType string - }{ - {server.handleIndex, "/", "GET", "", "text/html"}, - {server.handleHealth, "/health", "GET", "", "application/json"}, - {server.handleStatus, "/status", "GET", "", "application/json"}, - {server.handleRegisterStart, "/auth/register/start", "POST", `{"user_id":"test","community_id":"test"}`, "application/json"}, - {server.handleLoginStart, "/auth/login/start", "POST", `{"user_id":"test"}`, "application/json"}, - {server.handleLogout, "/auth/logout", "POST", "", "application/json"}, - } - - for _, tc := range testCases { - t.Run(fmt.Sprintf("%s %s", tc.method, tc.path), func(t *testing.T) { - var req *http.Request - if tc.body != "" { - req = httptest.NewRequest(tc.method, tc.path, strings.NewReader(tc.body)) - req.Header.Set("Content-Type", "application/json") - } else { - req = httptest.NewRequest(tc.method, tc.path, nil) - } - - w := httptest.NewRecorder() - tc.handler(w, req) - - contentType := w.Header().Get("Content-Type") - if contentType != tc.contentType { - t.Errorf("Expected Content-Type '%s', got '%s'", tc.contentType, contentType) - } - }) - } -} - -func TestWebServer_AuthenticationFlow(t *testing.T) { - cfg := testutils.TestConfig() - authenticator := testutils.NewMockWebAuthnManager() - bridgeClient := testutils.NewMockBridgeClient(cfg) - - server := NewWebServer(cfg, authenticator, bridgeClient) - - // Test complete authentication flow - // 1. Start registration - regStartReq := map[string]string{ - "user_id": "test-user", - "community_id": "test-community", - } - regStartJSON, _ := json.Marshal(regStartReq) - - req := httptest.NewRequest("POST", "/auth/register/start", bytes.NewBuffer(regStartJSON)) - req.Header.Set("Content-Type", "application/json") - w := httptest.NewRecorder() - - server.handleRegisterStart(w, req) - - if w.Code != http.StatusOK { - t.Fatalf("Register start failed: %d", w.Code) - } - - // 2. Complete registration - regCompleteReq := map[string]interface{}{ - "user_id": "test-user", - "credential": map[string]interface{}{ - "id": "test-credential-id", - "type": "public-key", - }, - } - regCompleteJSON, _ := json.Marshal(regCompleteReq) - - req = httptest.NewRequest("POST", "/auth/register/complete", bytes.NewBuffer(regCompleteJSON)) - req.Header.Set("Content-Type", "application/json") - w = httptest.NewRecorder() - - server.handleRegisterComplete(w, req) - - if w.Code != http.StatusOK { - t.Fatalf("Register complete failed: %d", w.Code) - } - - // 3. Check status (should be authenticated) - req = httptest.NewRequest("GET", "/status", nil) - w = httptest.NewRecorder() - - server.handleStatus(w, req) - - if w.Code != http.StatusOK { - t.Fatalf("Status check failed: %d", w.Code) - } - - var statusResponse map[string]interface{} - err := json.NewDecoder(w.Body).Decode(&statusResponse) - if err != nil { - t.Fatalf("Failed to decode status response: %v", err) - } - - if authenticated, exists := statusResponse["authenticated"]; !exists || !authenticated.(bool) { - t.Error("Expected to be authenticated after registration") - } - - // 4. Logout - req = httptest.NewRequest("POST", "/auth/logout", nil) - w = httptest.NewRecorder() - - server.handleLogout(w, req) - - if w.Code != http.StatusOK { - t.Fatalf("Logout failed: %d", w.Code) - } - - // 5. Check status (should not be authenticated) - req = httptest.NewRequest("GET", "/status", nil) - w = httptest.NewRecorder() - - server.handleStatus(w, req) - - if w.Code != http.StatusOK { - t.Fatalf("Status check failed: %d", w.Code) - } - - err = json.NewDecoder(w.Body).Decode(&statusResponse) - if err != nil { - t.Fatalf("Failed to decode status response: %v", err) - } - - if authenticated, exists := statusResponse["authenticated"]; !exists || authenticated.(bool) { - t.Error("Expected to not be authenticated after logout") - } -} \ No newline at end of file diff --git a/Premium/Desktop/internal/storage/bolt.go b/Premium/Desktop/internal/storage/bolt.go deleted file mode 100644 index dd64217e5..000000000 --- a/Premium/Desktop/internal/storage/bolt.go +++ /dev/null @@ -1,302 +0,0 @@ -package storage - -import ( - "fmt" - "path/filepath" - "time" - - "go.etcd.io/bbolt" -) - -const ( - // Default bucket for general storage - defaultBucket = "waddlebot" - - // Additional buckets for specific data types - sessionsBucket = "sessions" - modulesBucket = "modules" - configBucket = "config" -) - -// BoltStorage implements the Storage interface using BoltDB -type BoltStorage struct { - db *bbolt.DB -} - -// NewBoltStorage creates a new BoltDB storage instance -func NewBoltStorage(dataDir string) (*BoltStorage, error) { - // Create the database file path - dbPath := filepath.Join(dataDir, "waddlebot-bridge.db") - - // Open the database - db, err := bbolt.Open(dbPath, 0600, &bbolt.Options{ - Timeout: 1 * time.Second, - }) - if err != nil { - return nil, fmt.Errorf("failed to open bolt database: %w", err) - } - - storage := &BoltStorage{db: db} - - // Initialize buckets - if err := storage.initBuckets(); err != nil { - db.Close() - return nil, fmt.Errorf("failed to initialize buckets: %w", err) - } - - return storage, nil -} - -// initBuckets creates the required buckets if they don't exist -func (s *BoltStorage) initBuckets() error { - return s.db.Update(func(tx *bbolt.Tx) error { - buckets := []string{defaultBucket, sessionsBucket, modulesBucket, configBucket} - - for _, bucket := range buckets { - if _, err := tx.CreateBucketIfNotExists([]byte(bucket)); err != nil { - return fmt.Errorf("failed to create bucket %s: %w", bucket, err) - } - } - - return nil - }) -} - -// Set stores a key-value pair -func (s *BoltStorage) Set(key string, value []byte) error { - return s.db.Update(func(tx *bbolt.Tx) error { - bucket := tx.Bucket([]byte(defaultBucket)) - if bucket == nil { - return fmt.Errorf("bucket %s not found", defaultBucket) - } - - return bucket.Put([]byte(key), value) - }) -} - -// Get retrieves a value by key -func (s *BoltStorage) Get(key string) ([]byte, error) { - var value []byte - - err := s.db.View(func(tx *bbolt.Tx) error { - bucket := tx.Bucket([]byte(defaultBucket)) - if bucket == nil { - return fmt.Errorf("bucket %s not found", defaultBucket) - } - - data := bucket.Get([]byte(key)) - if data == nil { - return fmt.Errorf("key %s not found", key) - } - - // Make a copy of the data since it's only valid during the transaction - value = make([]byte, len(data)) - copy(value, data) - - return nil - }) - - return value, err -} - -// Delete removes a key -func (s *BoltStorage) Delete(key string) error { - return s.db.Update(func(tx *bbolt.Tx) error { - bucket := tx.Bucket([]byte(defaultBucket)) - if bucket == nil { - return fmt.Errorf("bucket %s not found", defaultBucket) - } - - return bucket.Delete([]byte(key)) - }) -} - -// Exists checks if a key exists -func (s *BoltStorage) Exists(key string) bool { - err := s.db.View(func(tx *bbolt.Tx) error { - bucket := tx.Bucket([]byte(defaultBucket)) - if bucket == nil { - return fmt.Errorf("bucket %s not found", defaultBucket) - } - - data := bucket.Get([]byte(key)) - if data == nil { - return fmt.Errorf("key not found") - } - - return nil - }) - - return err == nil -} - -// List returns all keys with a given prefix -func (s *BoltStorage) List(prefix string) ([]string, error) { - var keys []string - - err := s.db.View(func(tx *bbolt.Tx) error { - bucket := tx.Bucket([]byte(defaultBucket)) - if bucket == nil { - return fmt.Errorf("bucket %s not found", defaultBucket) - } - - cursor := bucket.Cursor() - prefixBytes := []byte(prefix) - - for k, _ := cursor.Seek(prefixBytes); k != nil && len(k) >= len(prefixBytes); k, _ = cursor.Next() { - if len(k) >= len(prefixBytes) && string(k[:len(prefixBytes)]) == prefix { - keys = append(keys, string(k)) - } else { - break - } - } - - return nil - }) - - return keys, err -} - -// SetWithBucket stores a key-value pair in a specific bucket -func (s *BoltStorage) SetWithBucket(bucketName, key string, value []byte) error { - return s.db.Update(func(tx *bbolt.Tx) error { - bucket := tx.Bucket([]byte(bucketName)) - if bucket == nil { - return fmt.Errorf("bucket %s not found", bucketName) - } - - return bucket.Put([]byte(key), value) - }) -} - -// GetWithBucket retrieves a value by key from a specific bucket -func (s *BoltStorage) GetWithBucket(bucketName, key string) ([]byte, error) { - var value []byte - - err := s.db.View(func(tx *bbolt.Tx) error { - bucket := tx.Bucket([]byte(bucketName)) - if bucket == nil { - return fmt.Errorf("bucket %s not found", bucketName) - } - - data := bucket.Get([]byte(key)) - if data == nil { - return fmt.Errorf("key %s not found", key) - } - - value = make([]byte, len(data)) - copy(value, data) - - return nil - }) - - return value, err -} - -// DeleteWithBucket removes a key from a specific bucket -func (s *BoltStorage) DeleteWithBucket(bucketName, key string) error { - return s.db.Update(func(tx *bbolt.Tx) error { - bucket := tx.Bucket([]byte(bucketName)) - if bucket == nil { - return fmt.Errorf("bucket %s not found", bucketName) - } - - return bucket.Delete([]byte(key)) - }) -} - -// ListWithBucket returns all keys with a given prefix from a specific bucket -func (s *BoltStorage) ListWithBucket(bucketName, prefix string) ([]string, error) { - var keys []string - - err := s.db.View(func(tx *bbolt.Tx) error { - bucket := tx.Bucket([]byte(bucketName)) - if bucket == nil { - return fmt.Errorf("bucket %s not found", bucketName) - } - - cursor := bucket.Cursor() - prefixBytes := []byte(prefix) - - for k, _ := cursor.Seek(prefixBytes); k != nil && len(k) >= len(prefixBytes); k, _ = cursor.Next() { - if len(k) >= len(prefixBytes) && string(k[:len(prefixBytes)]) == prefix { - keys = append(keys, string(k)) - } else { - break - } - } - - return nil - }) - - return keys, err -} - -// GetAllFromBucket returns all key-value pairs from a specific bucket -func (s *BoltStorage) GetAllFromBucket(bucketName string) (map[string][]byte, error) { - data := make(map[string][]byte) - - err := s.db.View(func(tx *bbolt.Tx) error { - bucket := tx.Bucket([]byte(bucketName)) - if bucket == nil { - return fmt.Errorf("bucket %s not found", bucketName) - } - - return bucket.ForEach(func(k, v []byte) error { - // Make copies of the key and value - key := make([]byte, len(k)) - value := make([]byte, len(v)) - copy(key, k) - copy(value, v) - - data[string(key)] = value - return nil - }) - }) - - return data, err -} - -// ClearBucket removes all data from a specific bucket -func (s *BoltStorage) ClearBucket(bucketName string) error { - return s.db.Update(func(tx *bbolt.Tx) error { - // Delete the bucket - if err := tx.DeleteBucket([]byte(bucketName)); err != nil { - return fmt.Errorf("failed to delete bucket %s: %w", bucketName, err) - } - - // Recreate the bucket - if _, err := tx.CreateBucket([]byte(bucketName)); err != nil { - return fmt.Errorf("failed to recreate bucket %s: %w", bucketName, err) - } - - return nil - }) -} - -// Close closes the database connection -func (s *BoltStorage) Close() error { - return s.db.Close() -} - -// Backup creates a backup of the database -func (s *BoltStorage) Backup(backupPath string) error { - return s.db.View(func(tx *bbolt.Tx) error { - return tx.CopyFile(backupPath, 0600) - }) -} - -// Stats returns database statistics -func (s *BoltStorage) Stats() map[string]interface{} { - stats := s.db.Stats() - - return map[string]interface{}{ - "free_page_n": stats.FreePageN, - "pending_page_n": stats.PendingPageN, - "free_alloc": stats.FreeAlloc, - "free_list_inuse": stats.FreelistInuse, - "tx_n": stats.TxN, - "tx_stats": stats.TxStats, - "open_tx_n": stats.OpenTxN, - } -} \ No newline at end of file diff --git a/Premium/Desktop/internal/storage/bolt_test.go b/Premium/Desktop/internal/storage/bolt_test.go deleted file mode 100644 index 7fab598ad..000000000 --- a/Premium/Desktop/internal/storage/bolt_test.go +++ /dev/null @@ -1,503 +0,0 @@ -package storage - -import ( - "fmt" - "os" - "path/filepath" - "testing" -) - -func TestNewBoltStorage(t *testing.T) { - tmpDir := t.TempDir() - - storage, err := NewBoltStorage(tmpDir) - if err != nil { - t.Fatalf("NewBoltStorage failed: %v", err) - } - defer storage.Close() - - if storage == nil { - t.Fatal("Expected non-nil storage") - } - - if storage.db == nil { - t.Fatal("Expected non-nil database") - } - - // Check that database file was created - dbPath := filepath.Join(tmpDir, "waddlebot-bridge.db") - if _, err := os.Stat(dbPath); os.IsNotExist(err) { - t.Error("Database file was not created") - } -} - -func TestBoltStorage_Set_Get(t *testing.T) { - tmpDir := t.TempDir() - storage, err := NewBoltStorage(tmpDir) - if err != nil { - t.Fatalf("NewBoltStorage failed: %v", err) - } - defer storage.Close() - - key := "test-key" - value := []byte("test-value") - - // Test Set - err = storage.Set(key, value) - if err != nil { - t.Fatalf("Set failed: %v", err) - } - - // Test Get - retrievedValue, err := storage.Get(key) - if err != nil { - t.Fatalf("Get failed: %v", err) - } - - if string(retrievedValue) != string(value) { - t.Errorf("Expected value %s, got %s", string(value), string(retrievedValue)) - } -} - -func TestBoltStorage_Get_NotFound(t *testing.T) { - tmpDir := t.TempDir() - storage, err := NewBoltStorage(tmpDir) - if err != nil { - t.Fatalf("NewBoltStorage failed: %v", err) - } - defer storage.Close() - - _, err = storage.Get("nonexistent-key") - if err == nil { - t.Error("Expected error for nonexistent key") - } -} - -func TestBoltStorage_Delete(t *testing.T) { - tmpDir := t.TempDir() - storage, err := NewBoltStorage(tmpDir) - if err != nil { - t.Fatalf("NewBoltStorage failed: %v", err) - } - defer storage.Close() - - key := "test-key" - value := []byte("test-value") - - // Set a value - err = storage.Set(key, value) - if err != nil { - t.Fatalf("Set failed: %v", err) - } - - // Verify it exists - _, err = storage.Get(key) - if err != nil { - t.Fatalf("Get failed: %v", err) - } - - // Delete the value - err = storage.Delete(key) - if err != nil { - t.Fatalf("Delete failed: %v", err) - } - - // Verify it no longer exists - _, err = storage.Get(key) - if err == nil { - t.Error("Expected error for deleted key") - } -} - -func TestBoltStorage_Exists(t *testing.T) { - tmpDir := t.TempDir() - storage, err := NewBoltStorage(tmpDir) - if err != nil { - t.Fatalf("NewBoltStorage failed: %v", err) - } - defer storage.Close() - - key := "test-key" - value := []byte("test-value") - - // Test non-existent key - if storage.Exists(key) { - t.Error("Expected false for non-existent key") - } - - // Set a value - err = storage.Set(key, value) - if err != nil { - t.Fatalf("Set failed: %v", err) - } - - // Test existing key - if !storage.Exists(key) { - t.Error("Expected true for existing key") - } -} - -func TestBoltStorage_List(t *testing.T) { - tmpDir := t.TempDir() - storage, err := NewBoltStorage(tmpDir) - if err != nil { - t.Fatalf("NewBoltStorage failed: %v", err) - } - defer storage.Close() - - // Set multiple values with same prefix - prefix := "test-" - keys := []string{"test-key1", "test-key2", "test-key3", "other-key"} - - for _, key := range keys { - err = storage.Set(key, []byte("value-"+key)) - if err != nil { - t.Fatalf("Set failed for key %s: %v", key, err) - } - } - - // List keys with prefix - foundKeys, err := storage.List(prefix) - if err != nil { - t.Fatalf("List failed: %v", err) - } - - // Should find 3 keys with the prefix - expectedCount := 3 - if len(foundKeys) != expectedCount { - t.Errorf("Expected %d keys, got %d", expectedCount, len(foundKeys)) - } - - // Verify all found keys have the prefix - for _, key := range foundKeys { - if len(key) < len(prefix) || key[:len(prefix)] != prefix { - t.Errorf("Found key %s does not have prefix %s", key, prefix) - } - } -} - -func TestBoltStorage_BucketOperations(t *testing.T) { - tmpDir := t.TempDir() - storage, err := NewBoltStorage(tmpDir) - if err != nil { - t.Fatalf("NewBoltStorage failed: %v", err) - } - defer storage.Close() - - bucketName := "test-bucket" - key := "test-key" - value := []byte("test-value") - - // Test SetWithBucket - err = storage.SetWithBucket(bucketName, key, value) - if err != nil { - t.Fatalf("SetWithBucket failed: %v", err) - } - - // Test GetWithBucket - retrievedValue, err := storage.GetWithBucket(bucketName, key) - if err != nil { - t.Fatalf("GetWithBucket failed: %v", err) - } - - if string(retrievedValue) != string(value) { - t.Errorf("Expected value %s, got %s", string(value), string(retrievedValue)) - } - - // Test DeleteWithBucket - err = storage.DeleteWithBucket(bucketName, key) - if err != nil { - t.Fatalf("DeleteWithBucket failed: %v", err) - } - - // Verify deletion - _, err = storage.GetWithBucket(bucketName, key) - if err == nil { - t.Error("Expected error for deleted key") - } -} - -func TestBoltStorage_ListWithBucket(t *testing.T) { - tmpDir := t.TempDir() - storage, err := NewBoltStorage(tmpDir) - if err != nil { - t.Fatalf("NewBoltStorage failed: %v", err) - } - defer storage.Close() - - bucketName := "test-bucket" - prefix := "test-" - keys := []string{"test-key1", "test-key2", "test-key3", "other-key"} - - // Set multiple values in bucket - for _, key := range keys { - err = storage.SetWithBucket(bucketName, key, []byte("value-"+key)) - if err != nil { - t.Fatalf("SetWithBucket failed for key %s: %v", key, err) - } - } - - // List keys with prefix in bucket - foundKeys, err := storage.ListWithBucket(bucketName, prefix) - if err != nil { - t.Fatalf("ListWithBucket failed: %v", err) - } - - // Should find 3 keys with the prefix - expectedCount := 3 - if len(foundKeys) != expectedCount { - t.Errorf("Expected %d keys, got %d", expectedCount, len(foundKeys)) - } -} - -func TestBoltStorage_GetAllFromBucket(t *testing.T) { - tmpDir := t.TempDir() - storage, err := NewBoltStorage(tmpDir) - if err != nil { - t.Fatalf("NewBoltStorage failed: %v", err) - } - defer storage.Close() - - bucketName := "test-bucket" - testData := map[string][]byte{ - "key1": []byte("value1"), - "key2": []byte("value2"), - "key3": []byte("value3"), - } - - // Set multiple values in bucket - for key, value := range testData { - err = storage.SetWithBucket(bucketName, key, value) - if err != nil { - t.Fatalf("SetWithBucket failed for key %s: %v", key, err) - } - } - - // Get all from bucket - allData, err := storage.GetAllFromBucket(bucketName) - if err != nil { - t.Fatalf("GetAllFromBucket failed: %v", err) - } - - // Verify all data is returned - if len(allData) != len(testData) { - t.Errorf("Expected %d items, got %d", len(testData), len(allData)) - } - - for key, expectedValue := range testData { - actualValue, exists := allData[key] - if !exists { - t.Errorf("Key %s not found in results", key) - continue - } - - if string(actualValue) != string(expectedValue) { - t.Errorf("Expected value %s for key %s, got %s", string(expectedValue), key, string(actualValue)) - } - } -} - -func TestBoltStorage_ClearBucket(t *testing.T) { - tmpDir := t.TempDir() - storage, err := NewBoltStorage(tmpDir) - if err != nil { - t.Fatalf("NewBoltStorage failed: %v", err) - } - defer storage.Close() - - bucketName := "test-bucket" - keys := []string{"key1", "key2", "key3"} - - // Set multiple values in bucket - for _, key := range keys { - err = storage.SetWithBucket(bucketName, key, []byte("value-"+key)) - if err != nil { - t.Fatalf("SetWithBucket failed for key %s: %v", key, err) - } - } - - // Verify data exists - allData, err := storage.GetAllFromBucket(bucketName) - if err != nil { - t.Fatalf("GetAllFromBucket failed: %v", err) - } - - if len(allData) != len(keys) { - t.Errorf("Expected %d items before clear, got %d", len(keys), len(allData)) - } - - // Clear bucket - err = storage.ClearBucket(bucketName) - if err != nil { - t.Fatalf("ClearBucket failed: %v", err) - } - - // Verify bucket is empty - allData, err = storage.GetAllFromBucket(bucketName) - if err != nil { - t.Fatalf("GetAllFromBucket failed after clear: %v", err) - } - - if len(allData) != 0 { - t.Errorf("Expected 0 items after clear, got %d", len(allData)) - } -} - -func TestBoltStorage_Stats(t *testing.T) { - tmpDir := t.TempDir() - storage, err := NewBoltStorage(tmpDir) - if err != nil { - t.Fatalf("NewBoltStorage failed: %v", err) - } - defer storage.Close() - - stats := storage.Stats() - - if stats == nil { - t.Fatal("Expected non-nil stats") - } - - // Check that stats contains expected fields - expectedFields := []string{ - "free_page_n", - "pending_page_n", - "free_alloc", - "free_list_inuse", - "tx_n", - "tx_stats", - "open_tx_n", - } - - for _, field := range expectedFields { - if _, exists := stats[field]; !exists { - t.Errorf("Expected stats field %s not found", field) - } - } -} - -func TestBoltStorage_Backup(t *testing.T) { - tmpDir := t.TempDir() - storage, err := NewBoltStorage(tmpDir) - if err != nil { - t.Fatalf("NewBoltStorage failed: %v", err) - } - defer storage.Close() - - // Add some data - err = storage.Set("test-key", []byte("test-value")) - if err != nil { - t.Fatalf("Set failed: %v", err) - } - - // Create backup - backupPath := filepath.Join(tmpDir, "backup.db") - err = storage.Backup(backupPath) - if err != nil { - t.Fatalf("Backup failed: %v", err) - } - - // Verify backup file exists - if _, err := os.Stat(backupPath); os.IsNotExist(err) { - t.Error("Backup file was not created") - } - - // Verify backup file has content - info, err := os.Stat(backupPath) - if err != nil { - t.Fatalf("Failed to stat backup file: %v", err) - } - - if info.Size() == 0 { - t.Error("Backup file is empty") - } -} - -func TestBoltStorage_Close(t *testing.T) { - tmpDir := t.TempDir() - storage, err := NewBoltStorage(tmpDir) - if err != nil { - t.Fatalf("NewBoltStorage failed: %v", err) - } - - // Test that storage works before closing - err = storage.Set("test-key", []byte("test-value")) - if err != nil { - t.Fatalf("Set failed before close: %v", err) - } - - // Close storage - err = storage.Close() - if err != nil { - t.Fatalf("Close failed: %v", err) - } - - // Test that operations fail after closing - err = storage.Set("test-key2", []byte("test-value2")) - if err == nil { - t.Error("Expected error after closing storage") - } -} - -func TestBoltStorage_ConcurrentAccess(t *testing.T) { - tmpDir := t.TempDir() - storage, err := NewBoltStorage(tmpDir) - if err != nil { - t.Fatalf("NewBoltStorage failed: %v", err) - } - defer storage.Close() - - // Test concurrent writes - done := make(chan bool) - numGoroutines := 10 - - for i := 0; i < numGoroutines; i++ { - go func(id int) { - defer func() { done <- true }() - - key := fmt.Sprintf("key-%d", id) - value := []byte(fmt.Sprintf("value-%d", id)) - - err := storage.Set(key, value) - if err != nil { - t.Errorf("Concurrent Set failed for key %s: %v", key, err) - return - } - - retrievedValue, err := storage.Get(key) - if err != nil { - t.Errorf("Concurrent Get failed for key %s: %v", key, err) - return - } - - if string(retrievedValue) != string(value) { - t.Errorf("Concurrent access: expected value %s, got %s", string(value), string(retrievedValue)) - } - }(i) - } - - // Wait for all goroutines to complete - for i := 0; i < numGoroutines; i++ { - <-done - } -} - -func TestBoltStorage_InitBuckets(t *testing.T) { - tmpDir := t.TempDir() - storage, err := NewBoltStorage(tmpDir) - if err != nil { - t.Fatalf("NewBoltStorage failed: %v", err) - } - defer storage.Close() - - // Test that default buckets were created - buckets := []string{"waddlebot", "sessions", "modules", "config"} - - for _, bucket := range buckets { - // Try to set a value in each bucket - err = storage.SetWithBucket(bucket, "test-key", []byte("test-value")) - if err != nil { - t.Errorf("Failed to set value in bucket %s: %v", bucket, err) - } - } -} - diff --git a/Premium/Desktop/internal/storage/errors.go b/Premium/Desktop/internal/storage/errors.go deleted file mode 100644 index 7c761d5db..000000000 --- a/Premium/Desktop/internal/storage/errors.go +++ /dev/null @@ -1,12 +0,0 @@ -package storage - -import "fmt" - -// Common storage errors -var ( - ErrKeyNotFound = fmt.Errorf("key not found") - ErrBucketNotFound = fmt.Errorf("bucket not found") - ErrInvalidKey = fmt.Errorf("invalid key") - ErrStorageClosed = fmt.Errorf("storage is closed") - ErrPermission = fmt.Errorf("permission denied") -) \ No newline at end of file diff --git a/Premium/Desktop/internal/storage/storage.go b/Premium/Desktop/internal/storage/storage.go deleted file mode 100644 index aa3f856d4..000000000 --- a/Premium/Desktop/internal/storage/storage.go +++ /dev/null @@ -1,24 +0,0 @@ -package storage - -// Storage defines the interface for data storage operations -type Storage interface { - // Basic operations - Set(key string, value []byte) error - Get(key string) ([]byte, error) - Delete(key string) error - Exists(key string) bool - List(prefix string) ([]string, error) - - // Bucket operations - SetWithBucket(bucketName, key string, value []byte) error - GetWithBucket(bucketName, key string) ([]byte, error) - DeleteWithBucket(bucketName, key string) error - ListWithBucket(bucketName, prefix string) ([]string, error) - GetAllFromBucket(bucketName string) (map[string][]byte, error) - ClearBucket(bucketName string) error - - // Utility operations - Close() error - Backup(backupPath string) error - Stats() map[string]interface{} -} \ No newline at end of file diff --git a/Premium/Desktop/internal/testutils/errors.go b/Premium/Desktop/internal/testutils/errors.go deleted file mode 100644 index c534cc561..000000000 --- a/Premium/Desktop/internal/testutils/errors.go +++ /dev/null @@ -1,13 +0,0 @@ -package testutils - -import "fmt" - -// Common test errors -var ( - ErrTestFailed = fmt.Errorf("test failed") - ErrTestTimeout = fmt.Errorf("test timeout") - ErrTestSetupFailed = fmt.Errorf("test setup failed") - ErrTestExpected = fmt.Errorf("expected test error") - ErrTestUnexpected = fmt.Errorf("unexpected test error") - ErrTestNotImplemented = fmt.Errorf("test not implemented") -) \ No newline at end of file diff --git a/Premium/Desktop/internal/testutils/mocks.go b/Premium/Desktop/internal/testutils/mocks.go deleted file mode 100644 index 15942bb79..000000000 --- a/Premium/Desktop/internal/testutils/mocks.go +++ /dev/null @@ -1,345 +0,0 @@ -package testutils - -import ( - "context" - "fmt" - "time" - - "waddlebot-bridge/internal/config" - "waddlebot-bridge/internal/models" - "waddlebot-bridge/internal/storage" -) - -// MockStorage implements the storage interface for testing -type MockStorage struct { - data map[string][]byte -} - -// NewMockStorage creates a new mock storage instance -func NewMockStorage() *MockStorage { - return &MockStorage{ - data: make(map[string][]byte), - } -} - -// Set stores a value in mock storage -func (m *MockStorage) Set(key string, value []byte) error { - m.data[key] = value - return nil -} - -// Get retrieves a value from mock storage -func (m *MockStorage) Get(key string) ([]byte, error) { - if value, exists := m.data[key]; exists { - return value, nil - } - return nil, storage.ErrKeyNotFound -} - -// Delete removes a key from mock storage -func (m *MockStorage) Delete(key string) error { - delete(m.data, key) - return nil -} - -// Exists checks if a key exists in mock storage -func (m *MockStorage) Exists(key string) bool { - _, exists := m.data[key] - return exists -} - -// List returns all keys with a given prefix in mock storage -func (m *MockStorage) List(prefix string) ([]string, error) { - var keys []string - for key := range m.data { - if len(key) >= len(prefix) && key[:len(prefix)] == prefix { - keys = append(keys, key) - } - } - return keys, nil -} - -// SetWithBucket stores a value in a named bucket -func (m *MockStorage) SetWithBucket(bucketName, key string, value []byte) error { - return m.Set(bucketName+":"+key, value) -} - -// GetWithBucket retrieves a value from a named bucket -func (m *MockStorage) GetWithBucket(bucketName, key string) ([]byte, error) { - return m.Get(bucketName + ":" + key) -} - -// DeleteWithBucket removes a key from a named bucket -func (m *MockStorage) DeleteWithBucket(bucketName, key string) error { - return m.Delete(bucketName + ":" + key) -} - -// ListWithBucket returns all keys in a bucket with a given prefix -func (m *MockStorage) ListWithBucket(bucketName, prefix string) ([]string, error) { - return m.List(bucketName + ":" + prefix) -} - -// GetAllFromBucket retrieves all key-value pairs from a named bucket -func (m *MockStorage) GetAllFromBucket(bucketName string) (map[string][]byte, error) { - result := make(map[string][]byte) - bucketPrefix := bucketName + ":" - for key, value := range m.data { - if len(key) >= len(bucketPrefix) && key[:len(bucketPrefix)] == bucketPrefix { - cleanKey := key[len(bucketPrefix):] - result[cleanKey] = value - } - } - return result, nil -} - -// ClearBucket removes all keys from a named bucket -func (m *MockStorage) ClearBucket(bucketName string) error { - bucketPrefix := bucketName + ":" - for key := range m.data { - if len(key) >= len(bucketPrefix) && key[:len(bucketPrefix)] == bucketPrefix { - delete(m.data, key) - } - } - return nil -} - -// Close closes the mock storage -func (m *MockStorage) Close() error { - return nil -} - -// Backup creates a backup of the mock storage (no-op for mock) -func (m *MockStorage) Backup(backupPath string) error { - return nil -} - -// Stats returns statistics about the mock storage -func (m *MockStorage) Stats() map[string]interface{} { - return map[string]interface{}{ - "keys": len(m.data), - } -} - -// MockModule implements the module interface for testing -type MockModule struct { - name string - actions map[string]func(ctx context.Context, parameters map[string]string) (map[string]interface{}, error) - initFunc func(config map[string]string) error -} - -// NewMockModule creates a new mock module -func NewMockModule(name string) *MockModule { - return &MockModule{ - name: name, - actions: make(map[string]func(ctx context.Context, parameters map[string]string) (map[string]interface{}, error)), - } -} - -// Initialize initializes the mock module -func (m *MockModule) Initialize(config map[string]string) error { - if m.initFunc != nil { - return m.initFunc(config) - } - return nil -} - -// GetInfo returns information about the mock module -func (m *MockModule) GetInfo() *models.ModuleInfo { - var actions []models.ActionInfo - for actionName := range m.actions { - actions = append(actions, models.ActionInfo{ - Name: actionName, - Description: "Test action", - Parameters: map[string]interface{}{}, - ReturnType: "object", - Timeout: 30, - }) - } - - return &models.ModuleInfo{ - Name: m.name, - Version: "1.0.0", - Description: "Mock module for testing", - Author: "Test", - Actions: actions, - Enabled: true, - LoadedAt: time.Now(), - } -} - -// ExecuteAction executes an action in the mock module -func (m *MockModule) ExecuteAction(ctx context.Context, action string, parameters map[string]string) (map[string]interface{}, error) { - if actionFunc, exists := m.actions[action]; exists { - return actionFunc(ctx, parameters) - } - // Return a generic error instead of importing modules for the error type - return nil, fmt.Errorf("action not found: %s", action) -} - -// GetActions returns available actions in the mock module -func (m *MockModule) GetActions() []models.ActionInfo { - return m.GetInfo().Actions -} - -// Cleanup cleans up the mock module resources -func (m *MockModule) Cleanup() error { - return nil -} - -// AddAction adds an action to the mock module -func (m *MockModule) AddAction(name string, handler func(ctx context.Context, parameters map[string]string) (map[string]interface{}, error)) { - m.actions[name] = handler -} - -// SetInitFunc sets the initialization function for the mock module -func (m *MockModule) SetInitFunc(fn func(config map[string]string) error) { - m.initFunc = fn -} - -// MockAuthSession represents a mock auth session -type MockAuthSession struct { - ID string - UserID string - CommunityID string - IssuedAt time.Time - ExpiresAt time.Time - Valid bool -} - -// MockWebAuthnManager implements auth manager for testing -type MockWebAuthnManager struct { - sessions map[string]*MockAuthSession - users map[string]*MockUser -} - -// MockUser represents a mock user -type MockUser struct { - ID string - Name string - DisplayName string - CommunityID string -} - -// NewMockWebAuthnManager creates a new mock auth manager -func NewMockWebAuthnManager() *MockWebAuthnManager { - return &MockWebAuthnManager{ - sessions: make(map[string]*MockAuthSession), - users: make(map[string]*MockUser), - } -} - -// AddUser adds a user to the mock manager -func (m *MockWebAuthnManager) AddUser(userID, communityID string) { - m.users[userID] = &MockUser{ - ID: userID, - Name: userID, - DisplayName: "Test User " + userID, - CommunityID: communityID, - } -} - -// AddSession adds a session to the mock manager -func (m *MockWebAuthnManager) AddSession(sessionID, userID, communityID string) { - m.sessions[sessionID] = &MockAuthSession{ - ID: sessionID, - UserID: userID, - CommunityID: communityID, - IssuedAt: time.Now(), - ExpiresAt: time.Now().Add(time.Hour), - Valid: true, - } -} - -// ValidateSession validates a session in the mock manager -func (m *MockWebAuthnManager) ValidateSession(sessionID string) (*models.AuthSession, error) { - session, exists := m.sessions[sessionID] - if !exists { - return nil, fmt.Errorf("session not found") - } - - if !session.Valid || time.Now().After(session.ExpiresAt) { - return nil, fmt.Errorf("session expired") - } - - return &models.AuthSession{ - ID: session.ID, - UserID: session.UserID, - CommunityID: session.CommunityID, - IssuedAt: session.IssuedAt, - ExpiresAt: session.ExpiresAt, - }, nil -} - -// IsAuthenticated checks if any session is currently authenticated -func (m *MockWebAuthnManager) IsAuthenticated() bool { - return len(m.sessions) > 0 -} - -// GetCurrentSession returns the current active session from the mock manager -func (m *MockWebAuthnManager) GetCurrentSession() *models.AuthSession { - for _, session := range m.sessions { - if session.Valid && time.Now().Before(session.ExpiresAt) { - return &models.AuthSession{ - ID: session.ID, - UserID: session.UserID, - CommunityID: session.CommunityID, - IssuedAt: session.IssuedAt, - ExpiresAt: session.ExpiresAt, - } - } - } - return nil -} - -// TestConfig creates a test configuration -func TestConfig() *config.Config { - return &config.Config{ - APIURL: "http://test.waddlebot.io", - CommunityID: "test-community", - UserID: "test-user", - PollInterval: 30, - WebPort: 8080, - WebHost: "127.0.0.1", - DataDir: "/tmp/waddlebot-test", - LogLevel: "info", - WebAuthnDisplayName: "Test Bridge", - WebAuthnOrigin: "http://127.0.0.1:8080", - WebAuthnTimeout: 60, - JWTSecret: "test-secret", - ModulesDir: "/tmp/waddlebot-test/modules", - ModuleTimeout: 30, - MaxConcurrentTasks: 10, - } -} - -// TestModule creates a test module with common test actions -func TestModule(name string) *MockModule { - module := NewMockModule(name) - - // Add some basic test actions - module.AddAction("ping", func(ctx context.Context, parameters map[string]string) (map[string]interface{}, error) { - return map[string]interface{}{"message": "pong"}, nil - }) - - module.AddAction("echo", func(ctx context.Context, parameters map[string]string) (map[string]interface{}, error) { - message := parameters["message"] - if message == "" { - message = "hello" - } - return map[string]interface{}{"echo": message}, nil - }) - - module.AddAction("fail", func(ctx context.Context, parameters map[string]string) (map[string]interface{}, error) { - return nil, fmt.Errorf("action execution failed") - }) - - return module -} - -// ErrKeyNotFound is returned when a key is not found -var ErrKeyNotFound = storage.ErrKeyNotFound - -// TestContext creates a test context with a 5 second timeout -func TestContext() (context.Context, context.CancelFunc) { - return context.WithTimeout(context.Background(), 5*time.Second) -} diff --git a/Premium/Desktop/scripts/build.bat b/Premium/Desktop/scripts/build.bat deleted file mode 100644 index e43d62c81..000000000 --- a/Premium/Desktop/scripts/build.bat +++ /dev/null @@ -1,170 +0,0 @@ -@echo off -REM WaddleBot Premium Desktop Bridge Build Script for Windows -REM Builds for Windows 11 and creates distribution packages - -echo. -echo 🤖 WaddleBot Premium Desktop Bridge Build Script -echo ============================================== - -REM Configuration -set APP_NAME=waddlebot-bridge -set VERSION=1.0.0 -set BUILD_DIR=build -set DIST_DIR=dist - -REM Check if Go is installed -where go >nul 2>nul -if %errorlevel% neq 0 ( - echo [ERROR] Go is not installed or not in PATH - exit /b 1 -) - -REM Check Go version -echo [INFO] Checking Go version... -go version - -REM Clean previous builds -echo [INFO] Cleaning previous builds... -if exist %BUILD_DIR% rmdir /s /q %BUILD_DIR% -if exist %DIST_DIR% rmdir /s /q %DIST_DIR% -mkdir %BUILD_DIR% -mkdir %DIST_DIR% - -REM Build module examples -echo [INFO] Building module examples... -cd internal\modules\examples\system -go build -buildmode=plugin -o ..\..\..\..\%BUILD_DIR%\system.so system.go -if %errorlevel% neq 0 ( - echo [WARNING] Module plugin build failed, continuing... -) -cd ..\..\..\.. - -REM Build Windows Binary -echo [INFO] Building Windows Binary... -set CGO_ENABLED=0 -set GOOS=windows -set GOARCH=amd64 -go build -ldflags="-s -w -X main.version=%VERSION%" -o %BUILD_DIR%\%APP_NAME%-windows-amd64.exe cmd\main.go - -if %errorlevel% neq 0 ( - echo [ERROR] Windows build failed - exit /b 1 -) - -REM Build for other platforms (if desired) -echo [INFO] Building Linux Binary... -set GOOS=linux -go build -ldflags="-s -w -X main.version=%VERSION%" -o %BUILD_DIR%\%APP_NAME%-linux-amd64 cmd\main.go - -echo [INFO] Building macOS Binary... -set GOOS=darwin -set GOARCH=amd64 -go build -ldflags="-s -w -X main.version=%VERSION%" -o %BUILD_DIR%\%APP_NAME%-darwin-amd64 cmd\main.go - -REM Reset environment -set GOOS= -set GOARCH= -set CGO_ENABLED= - -REM Create distribution packages -echo [INFO] Creating distribution packages... - -REM Windows Package -echo [INFO] Creating Windows package... -set WINDOWS_DIR=%DIST_DIR%\WaddleBot-Bridge-Windows-%VERSION% -mkdir "%WINDOWS_DIR%" -copy %BUILD_DIR%\%APP_NAME%-windows-amd64.exe "%WINDOWS_DIR%\waddlebot-bridge.exe" -if exist README.md copy README.md "%WINDOWS_DIR%\" -if exist LICENSE copy LICENSE "%WINDOWS_DIR%\" - -REM Create Windows batch file -echo @echo off > "%WINDOWS_DIR%\start.bat" -echo cd /d "%%~dp0" >> "%WINDOWS_DIR%\start.bat" -echo waddlebot-bridge.exe --config config.yaml >> "%WINDOWS_DIR%\start.bat" -echo pause >> "%WINDOWS_DIR%\start.bat" - -REM Create sample config for Windows -echo # WaddleBot Bridge Configuration > "%WINDOWS_DIR%\config.yaml" -echo api-url: "https://api.waddlebot.io" >> "%WINDOWS_DIR%\config.yaml" -echo community-id: "" >> "%WINDOWS_DIR%\config.yaml" -echo user-id: "" >> "%WINDOWS_DIR%\config.yaml" -echo poll-interval: 30 >> "%WINDOWS_DIR%\config.yaml" -echo web-port: 8080 >> "%WINDOWS_DIR%\config.yaml" -echo web-host: "127.0.0.1" >> "%WINDOWS_DIR%\config.yaml" -echo log-level: "info" >> "%WINDOWS_DIR%\config.yaml" - -REM Create Windows installer script -echo [INFO] Creating Windows installer script... -echo @echo off > "%WINDOWS_DIR%\install.bat" -echo echo Installing WaddleBot Bridge... >> "%WINDOWS_DIR%\install.bat" -echo echo. >> "%WINDOWS_DIR%\install.bat" -echo echo Before running, please configure your community-id and user-id in config.yaml >> "%WINDOWS_DIR%\install.bat" -echo echo. >> "%WINDOWS_DIR%\install.bat" -echo echo You can run the bridge by executing start.bat >> "%WINDOWS_DIR%\install.bat" -echo echo Or run directly: waddlebot-bridge.exe --config config.yaml >> "%WINDOWS_DIR%\install.bat" -echo echo. >> "%WINDOWS_DIR%\install.bat" -echo echo Installation complete! >> "%WINDOWS_DIR%\install.bat" -echo pause >> "%WINDOWS_DIR%\install.bat" - -REM Create archives -echo [INFO] Creating archives... -cd %DIST_DIR% - -REM Windows Archive -where powershell >nul 2>nul -if %errorlevel% equ 0 ( - powershell -Command "Compress-Archive -Path 'WaddleBot-Bridge-Windows-%VERSION%' -DestinationPath 'WaddleBot-Bridge-Windows-%VERSION%.zip' -Force" - echo [INFO] Windows archive created: WaddleBot-Bridge-Windows-%VERSION%.zip -) else ( - echo [WARNING] PowerShell not found, skipping archive creation -) - -cd .. - -REM Generate checksums -echo [INFO] Generating checksums... -cd %DIST_DIR% -where powershell >nul 2>nul -if %errorlevel% equ 0 ( - powershell -Command "Get-FileHash *.zip -Algorithm SHA256 | Format-Table Hash, Path -AutoSize | Out-File -FilePath checksums.txt -Encoding utf8" - echo [INFO] Checksums generated -) -cd .. - -REM Build summary -echo. -echo [INFO] Build Summary: -echo ============================================== -echo Version: %VERSION% -echo Build Directory: %BUILD_DIR% -echo Distribution Directory: %DIST_DIR% -echo. -echo Built Binaries: -dir %BUILD_DIR% -echo. -echo Distribution Packages: -dir %DIST_DIR% -echo. - -REM Get binary sizes -echo [INFO] Binary Sizes: -for %%f in (%BUILD_DIR%\*.exe) do ( - for /f "tokens=3" %%s in ('dir "%%f" ^| findstr /C:"%%~nxf"') do ( - echo Windows x64: %%s bytes - ) -) - -echo. -echo 🎉 Build completed successfully! -echo 📦 Distribution packages are ready in the %DIST_DIR% directory -echo. -echo Installation Instructions: -echo ========================== -echo Windows: Extract the .zip file and run install.bat -echo Then run start.bat to launch the bridge -echo. -echo Before running, configure your community-id and user-id in config.yaml -echo. -echo Web Interface: http://localhost:8080 -echo. -pause \ No newline at end of file diff --git a/Premium/Desktop/scripts/build.sh b/Premium/Desktop/scripts/build.sh deleted file mode 100755 index 8c221eb33..000000000 --- a/Premium/Desktop/scripts/build.sh +++ /dev/null @@ -1,245 +0,0 @@ -#!/bin/bash - -# WaddleBot Premium Desktop Bridge Build Script -# Builds for macOS Universal and Windows 11 - -set -e - -echo "🤖 WaddleBot Premium Desktop Bridge Build Script" -echo "==============================================" - -# Colors for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -NC='\033[0m' # No Color - -# Configuration -APP_NAME="waddlebot-bridge" -VERSION="1.0.0" -BUILD_DIR="build" -DIST_DIR="dist" - -# Function to print colored output -print_status() { - echo -e "${GREEN}[INFO]${NC} $1" -} - -print_warning() { - echo -e "${YELLOW}[WARNING]${NC} $1" -} - -print_error() { - echo -e "${RED}[ERROR]${NC} $1" -} - -# Check if Go is installed -if ! command -v go &> /dev/null; then - print_error "Go is not installed or not in PATH" - exit 1 -fi - -# Check Go version -GO_VERSION=$(go version | awk '{print $3}') -print_status "Using Go version: $GO_VERSION" - -# Clean previous builds -print_status "Cleaning previous builds..." -rm -rf $BUILD_DIR $DIST_DIR -mkdir -p $BUILD_DIR $DIST_DIR - -# Build module examples -print_status "Building module examples..." -cd internal/modules/examples/system -go build -buildmode=plugin -o ../../../../$BUILD_DIR/system.so system.go -cd ../../../../ - -# Build macOS Universal Binary -print_status "Building macOS Universal Binary..." -print_status "Building for macOS arm64..." -CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -ldflags="-s -w -X main.version=$VERSION" -o $BUILD_DIR/${APP_NAME}-darwin-arm64 cmd/main.go - -print_status "Building for macOS amd64..." -CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags="-s -w -X main.version=$VERSION" -o $BUILD_DIR/${APP_NAME}-darwin-amd64 cmd/main.go - -# Create Universal Binary -print_status "Creating Universal Binary..." -if command -v lipo &> /dev/null; then - lipo -create -output $BUILD_DIR/${APP_NAME}-darwin-universal $BUILD_DIR/${APP_NAME}-darwin-arm64 $BUILD_DIR/${APP_NAME}-darwin-amd64 - print_status "Universal Binary created successfully" -else - print_warning "lipo not found, skipping Universal Binary creation" - cp $BUILD_DIR/${APP_NAME}-darwin-arm64 $BUILD_DIR/${APP_NAME}-darwin-universal -fi - -# Build Windows 11 Binary -print_status "Building Windows 11 Binary..." -CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags="-s -w -X main.version=$VERSION" -o $BUILD_DIR/${APP_NAME}-windows-amd64.exe cmd/main.go - -# Build Linux Binary (for completeness) -print_status "Building Linux Binary..." -CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w -X main.version=$VERSION" -o $BUILD_DIR/${APP_NAME}-linux-amd64 cmd/main.go - -# Create distribution packages -print_status "Creating distribution packages..." - -# macOS Package -print_status "Creating macOS package..." -MACOS_DIR="$DIST_DIR/WaddleBot-Bridge-macOS-$VERSION" -mkdir -p "$MACOS_DIR" -cp $BUILD_DIR/${APP_NAME}-darwin-universal "$MACOS_DIR/waddlebot-bridge" -cp $BUILD_DIR/system.so "$MACOS_DIR/" -cp README.md "$MACOS_DIR/" 2>/dev/null || echo "README.md not found, skipping" -cp LICENSE "$MACOS_DIR/" 2>/dev/null || echo "LICENSE not found, skipping" - -# Create macOS startup script -cat > "$MACOS_DIR/start.sh" << 'EOF' -#!/bin/bash -cd "$(dirname "$0")" -./waddlebot-bridge --config config.yaml -EOF -chmod +x "$MACOS_DIR/start.sh" - -# Create sample config -cat > "$MACOS_DIR/config.yaml" << 'EOF' -# WaddleBot Bridge Configuration -api-url: "https://api.waddlebot.io" -community-id: "" -user-id: "" -poll-interval: 30 -web-port: 8080 -web-host: "127.0.0.1" -log-level: "info" -EOF - -# Create macOS app bundle -print_status "Creating macOS app bundle..." -APP_BUNDLE="$DIST_DIR/WaddleBot Bridge.app" -mkdir -p "$APP_BUNDLE/Contents/MacOS" -mkdir -p "$APP_BUNDLE/Contents/Resources" - -cp $BUILD_DIR/${APP_NAME}-darwin-universal "$APP_BUNDLE/Contents/MacOS/waddlebot-bridge" -cp $BUILD_DIR/system.so "$APP_BUNDLE/Contents/MacOS/" - -# Create Info.plist -cat > "$APP_BUNDLE/Contents/Info.plist" << EOF - - - - - CFBundleExecutable - waddlebot-bridge - CFBundleIdentifier - com.waddlebot.bridge - CFBundleName - WaddleBot Bridge - CFBundleVersion - $VERSION - CFBundleShortVersionString - $VERSION - CFBundlePackageType - APPL - LSMinimumSystemVersion - 10.15 - NSHighResolutionCapable - - - -EOF - -# Windows Package -print_status "Creating Windows package..." -WINDOWS_DIR="$DIST_DIR/WaddleBot-Bridge-Windows-$VERSION" -mkdir -p "$WINDOWS_DIR" -cp $BUILD_DIR/${APP_NAME}-windows-amd64.exe "$WINDOWS_DIR/waddlebot-bridge.exe" -cp $BUILD_DIR/system.so "$WINDOWS_DIR/" 2>/dev/null || true # May not work on Windows -cp README.md "$WINDOWS_DIR/" 2>/dev/null || echo "README.md not found, skipping" -cp LICENSE "$WINDOWS_DIR/" 2>/dev/null || echo "LICENSE not found, skipping" - -# Create Windows batch file -cat > "$WINDOWS_DIR/start.bat" << 'EOF' -@echo off -cd /d "%~dp0" -waddlebot-bridge.exe --config config.yaml -pause -EOF - -# Create sample config for Windows -cat > "$WINDOWS_DIR/config.yaml" << 'EOF' -# WaddleBot Bridge Configuration -api-url: "https://api.waddlebot.io" -community-id: "" -user-id: "" -poll-interval: 30 -web-port: 8080 -web-host: "127.0.0.1" -log-level: "info" -EOF - -# Create archives -print_status "Creating archives..." -cd $DIST_DIR - -# macOS Archive -if command -v tar &> /dev/null; then - tar -czf "WaddleBot-Bridge-macOS-$VERSION.tar.gz" "WaddleBot-Bridge-macOS-$VERSION" - print_status "macOS archive created: WaddleBot-Bridge-macOS-$VERSION.tar.gz" -fi - -# Windows Archive -if command -v zip &> /dev/null; then - zip -r "WaddleBot-Bridge-Windows-$VERSION.zip" "WaddleBot-Bridge-Windows-$VERSION" - print_status "Windows archive created: WaddleBot-Bridge-Windows-$VERSION.zip" -fi - -# App Bundle Archive -if command -v tar &> /dev/null; then - tar -czf "WaddleBot-Bridge-macOS-App-$VERSION.tar.gz" "WaddleBot Bridge.app" - print_status "macOS app bundle created: WaddleBot-Bridge-macOS-App-$VERSION.tar.gz" -fi - -cd .. - -# Generate checksums -print_status "Generating checksums..." -cd $DIST_DIR -if command -v shasum &> /dev/null; then - shasum -a 256 *.tar.gz *.zip > checksums.txt 2>/dev/null || true - print_status "Checksums generated" -elif command -v sha256sum &> /dev/null; then - sha256sum *.tar.gz *.zip > checksums.txt 2>/dev/null || true - print_status "Checksums generated" -fi -cd .. - -# Build summary -print_status "Build Summary:" -echo "==============================================" -echo "Version: $VERSION" -echo "Build Directory: $BUILD_DIR" -echo "Distribution Directory: $DIST_DIR" -echo "" -echo "Built Binaries:" -ls -la $BUILD_DIR/ -echo "" -echo "Distribution Packages:" -ls -la $DIST_DIR/ -echo "" - -# Get binary sizes -print_status "Binary Sizes:" -echo "macOS Universal: $(du -h $BUILD_DIR/${APP_NAME}-darwin-universal | cut -f1)" -echo "Windows x64: $(du -h $BUILD_DIR/${APP_NAME}-windows-amd64.exe | cut -f1)" -echo "Linux x64: $(du -h $BUILD_DIR/${APP_NAME}-linux-amd64 | cut -f1)" - -echo "" -echo -e "${GREEN}🎉 Build completed successfully!${NC}" -echo -e "${GREEN}📦 Distribution packages are ready in the $DIST_DIR directory${NC}" -echo "" -echo "Installation Instructions:" -echo "==========================" -echo "macOS: Extract the .tar.gz file and run ./start.sh" -echo "Windows: Extract the .zip file and run start.bat" -echo "Or use the macOS app bundle by double-clicking the .app file" -echo "" -echo "Before running, configure your community-id and user-id in config.yaml" \ No newline at end of file diff --git a/Premium/Desktop/test.sh b/Premium/Desktop/test.sh deleted file mode 100755 index d0aa1da89..000000000 --- a/Premium/Desktop/test.sh +++ /dev/null @@ -1,164 +0,0 @@ -#!/bin/bash - -# WaddleBot Bridge Test Runner -# This script runs all unit tests and integration tests for the WaddleBot Bridge - -set -e - -echo "🤖 WaddleBot Bridge Test Runner" -echo "================================" - -# Colors for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -NC='\033[0m' # No Color - -# Function to print colored output -print_status() { - local status=$1 - local message=$2 - case $status in - "SUCCESS") - echo -e "${GREEN}✓ $message${NC}" - ;; - "ERROR") - echo -e "${RED}✗ $message${NC}" - ;; - "INFO") - echo -e "${YELLOW}ℹ $message${NC}" - ;; - esac -} - -# Check if we're in the right directory -if [ ! -f "go.mod" ]; then - print_status "ERROR" "go.mod not found. Please run this script from the project root." - exit 1 -fi - -# Check if Go is installed -if ! command -v go &> /dev/null; then - print_status "ERROR" "Go is not installed or not in PATH" - exit 1 -fi - -print_status "INFO" "Go version: $(go version)" - -# Clean up any previous test artifacts -print_status "INFO" "Cleaning up previous test artifacts..." -go clean -testcache - -# Download dependencies -print_status "INFO" "Downloading dependencies..." -go mod download - -# Run unit tests -print_status "INFO" "Running unit tests..." -UNIT_TEST_PACKAGES="./internal/..." - -if go test -v -race -coverprofile=coverage.out $UNIT_TEST_PACKAGES; then - print_status "SUCCESS" "Unit tests passed" -else - print_status "ERROR" "Unit tests failed" - exit 1 -fi - -# Run integration tests -print_status "INFO" "Running integration tests..." -if go test -v -race -tags=integration ./...; then - print_status "SUCCESS" "Integration tests passed" -else - print_status "ERROR" "Integration tests failed" - exit 1 -fi - -# Generate coverage report -if [ -f "coverage.out" ]; then - print_status "INFO" "Generating coverage report..." - go tool cover -html=coverage.out -o coverage.html - - # Calculate coverage percentage - COVERAGE=$(go tool cover -func=coverage.out | grep total | awk '{print $3}') - print_status "SUCCESS" "Coverage report generated: coverage.html" - print_status "INFO" "Total coverage: $COVERAGE" - - # Check if coverage is above threshold - COVERAGE_NUM=$(echo $COVERAGE | sed 's/%//') - if (( $(echo "$COVERAGE_NUM >= 80" | bc -l) )); then - print_status "SUCCESS" "Coverage above 80% threshold" - else - print_status "ERROR" "Coverage below 80% threshold" - fi -fi - -# Run linter if available -if command -v golangci-lint &> /dev/null; then - print_status "INFO" "Running linter..." - if golangci-lint run; then - print_status "SUCCESS" "Linter passed" - else - print_status "ERROR" "Linter found issues" - exit 1 - fi -else - print_status "INFO" "golangci-lint not found, skipping linter" -fi - -# Run go vet -print_status "INFO" "Running go vet..." -if go vet ./...; then - print_status "SUCCESS" "go vet passed" -else - print_status "ERROR" "go vet found issues" - exit 1 -fi - -# Run go fmt check -print_status "INFO" "Checking code formatting..." -UNFORMATTED=$(go fmt ./...) -if [ -z "$UNFORMATTED" ]; then - print_status "SUCCESS" "Code is properly formatted" -else - print_status "ERROR" "Code formatting issues found:" - echo "$UNFORMATTED" - exit 1 -fi - -# Check for security issues if gosec is available -if command -v gosec &> /dev/null; then - print_status "INFO" "Running security scan..." - if gosec ./...; then - print_status "SUCCESS" "Security scan passed" - else - print_status "ERROR" "Security issues found" - exit 1 - fi -else - print_status "INFO" "gosec not found, skipping security scan" -fi - -# Run benchmarks if requested -if [ "$1" = "bench" ]; then - print_status "INFO" "Running benchmarks..." - go test -bench=. -benchmem ./... -fi - -# Final summary -echo "" -echo "🎉 All tests completed successfully!" -echo "================================" -print_status "SUCCESS" "Unit tests: PASSED" -print_status "SUCCESS" "Integration tests: PASSED" -print_status "SUCCESS" "Code quality checks: PASSED" - -if [ -f "coverage.html" ]; then - print_status "INFO" "Coverage report: coverage.html" -fi - -echo "" -echo "To run specific test categories:" -echo " Unit tests only: go test ./internal/..." -echo " Integration tests only: go test -tags=integration ./..." -echo " With benchmarks: ./test.sh bench" -echo " Coverage report: go tool cover -html=coverage.out" \ No newline at end of file diff --git a/Premium/Desktop/waddlebot-bridge b/Premium/Desktop/waddlebot-bridge deleted file mode 100755 index 37472cd3e..000000000 Binary files a/Premium/Desktop/waddlebot-bridge and /dev/null differ