[
  {
    "path": ".dockerignore",
    "content": "web/node_modules\nweb/dist\n.git\n.github\nbuild/\ntmp/\nmemos\n*.md\n.gitignore\n.golangci.yaml\n.dockerignore\ndocs/\n.DS_Store"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "github: usememos\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.yml",
    "content": "name: Bug Report\ndescription: Something isn't working as expected\ntype: Bug\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        Thanks for reporting a bug! Please fill out the form below so we can reproduce and fix the issue.\n\n        **Before submitting**, please search [existing issues](https://github.com/usememos/memos/issues) to avoid duplicates.\n\n  - type: checkboxes\n    id: pre-check\n    attributes:\n      label: Pre-submission Checklist\n      options:\n        - label: I have searched existing issues and confirmed this bug has not been reported\n          required: true\n        - label: I can reproduce this bug on the latest version or the [demo site](https://demo.usememos.com)\n          required: true\n        - label: This is a bug, not a question (use [Discussions](https://github.com/usememos/memos/discussions) for questions)\n          required: true\n\n  - type: input\n    id: version\n    attributes:\n      label: Memos Version\n      description: Find this in **Settings > System > About** or via the `--version` flag\n      placeholder: \"v0.25.2\"\n    validations:\n      required: true\n\n  - type: dropdown\n    id: deployment\n    attributes:\n      label: Deployment Method\n      options:\n        - Docker\n        - Pre-built binary\n        - Built from source\n    validations:\n      required: true\n\n  - type: dropdown\n    id: database\n    attributes:\n      label: Database\n      options:\n        - SQLite\n        - PostgreSQL\n        - MySQL\n    validations:\n      required: true\n\n  - type: input\n    id: browser-os\n    attributes:\n      label: Browser & OS\n      description: e.g. Chrome 120 on macOS 15, Firefox 130 on Ubuntu 24.04\n      placeholder: \"Chrome 120 on macOS 15\"\n    validations:\n      required: false\n\n  - type: textarea\n    id: bug-description\n    attributes:\n      label: Bug Description\n      description: A clear and concise description of what the bug is\n      placeholder: When I try to..., the application...\n    validations:\n      required: true\n\n  - type: textarea\n    id: reproduction-steps\n    attributes:\n      label: Steps to Reproduce\n      description: Minimal steps to reliably reproduce the issue\n      placeholder: |\n        1. Go to '...'\n        2. Click on '...'\n        3. See error\n    validations:\n      required: true\n\n  - type: textarea\n    id: expected-behavior\n    attributes:\n      label: Expected Behavior\n      description: What did you expect to happen instead?\n      placeholder: I expected...\n    validations:\n      required: true\n\n  - type: textarea\n    id: additional-context\n    attributes:\n      label: Screenshots, Logs & Additional Context\n      description: Attach screenshots, browser console errors, or server logs if available\n      placeholder: Drag and drop images here, or paste error logs...\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: false\ncontact_links:\n  - name: Questions & Support\n    url: https://github.com/usememos/memos/discussions\n    about: Ask questions or get help in GitHub Discussions — please don't open issues for questions\n  - name: Documentation\n    url: https://www.usememos.com/docs\n    about: Check the documentation before opening an issue\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.yml",
    "content": "name: Feature Request\ndescription: Suggest a new feature or improvement\ntype: Feature\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        Thanks for suggesting a feature! Please fill out the form below so we can understand your idea.\n\n        **Before submitting**, please search [existing issues](https://github.com/usememos/memos/issues?q=label%3Aenhancement) to avoid duplicates.\n\n  - type: checkboxes\n    id: pre-check\n    attributes:\n      label: Pre-submission Checklist\n      options:\n        - label: I have searched existing issues and confirmed this feature has not been requested\n          required: true\n        - label: This is a feature request, not a bug report or question\n          required: true\n\n  - type: dropdown\n    id: feature-type\n    attributes:\n      label: Feature Area\n      options:\n        - User Interface (UI)\n        - User Experience (UX)\n        - API / Backend\n        - Integrations / Plugins\n        - Security / Privacy\n        - Performance\n        - Other\n    validations:\n      required: true\n\n  - type: textarea\n    id: problem-statement\n    attributes:\n      label: Problem or Use Case\n      description: What problem does this feature solve? Why do you need it?\n      placeholder: |\n        I often need to... but currently there's no way to...\n    validations:\n      required: true\n\n  - type: textarea\n    id: proposed-solution\n    attributes:\n      label: Proposed Solution\n      description: Describe what you'd like to happen\n      placeholder: |\n        It would be great if Memos could...\n    validations:\n      required: true\n\n  - type: textarea\n    id: alternatives\n    attributes:\n      label: Alternatives Considered\n      description: Have you considered any workarounds or alternative approaches?\n      placeholder: |\n        I've tried... but it doesn't work well because...\n\n  - type: textarea\n    id: additional-context\n    attributes:\n      label: Additional Context\n      description: Mockups, screenshots, examples from other apps, or any other context\n      placeholder: Drag and drop images here...\n\n  - type: checkboxes\n    id: contribution\n    attributes:\n      label: Contribution\n      description: Would you be willing to help implement this feature?\n      options:\n        - label: I'm willing to submit a pull request for this feature\n"
  },
  {
    "path": ".github/workflows/backend-tests.yml",
    "content": "name: Backend Tests\n\non:\n  push:\n    branches: [main]\n  pull_request:\n    branches: [main]\n    paths:\n      - \"go.mod\"\n      - \"go.sum\"\n      - \"**.go\"\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.ref }}\n  cancel-in-progress: true\n\nenv:\n  GO_VERSION: \"1.26.1\"\n\njobs:\n  static-checks:\n    name: Static Checks\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v6\n      \n      - name: Setup Go\n        uses: actions/setup-go@v6\n        with:\n          go-version: ${{ env.GO_VERSION }}\n          cache: true\n          cache-dependency-path: go.sum\n\n      - name: Verify go.mod is tidy\n        run: |\n          go mod tidy -go=${{ env.GO_VERSION }}\n          git diff --exit-code\n\n      - name: Run golangci-lint\n        uses: golangci/golangci-lint-action@v9\n        with:\n          version: v2.11.3\n          args: --timeout=3m\n\n  tests:\n    name: Tests (${{ matrix.test-group }})\n    runs-on: ubuntu-latest\n    strategy:\n      fail-fast: false\n      matrix:\n        test-group: [store, server, plugin, other]\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v6\n      \n      - name: Setup Go\n        uses: actions/setup-go@v6\n        with:\n          go-version: ${{ env.GO_VERSION }}\n          cache: true\n          cache-dependency-path: go.sum\n\n      - name: Run tests\n        run: |\n          case \"${{ matrix.test-group }}\" in\n            store)\n              # Run store tests for all drivers (sqlite, mysql, postgres)\n              go test -v -coverprofile=coverage.out -covermode=atomic ./store/...\n              ;;\n            server)\n              go test -v -race -coverprofile=coverage.out -covermode=atomic ./server/...\n              ;;\n            plugin)\n              go test -v -race -coverprofile=coverage.out -covermode=atomic ./plugin/...\n              ;;\n            other)\n              go test -v -race -coverprofile=coverage.out -covermode=atomic \\\n                ./cmd/... ./internal/... ./proto/...\n              ;;\n          esac\n        env:\n          DRIVER: ${{ matrix.test-group == 'store' && '' || 'sqlite' }}\n\n      - name: Upload coverage\n        if: github.event_name == 'push' && github.ref == 'refs/heads/main'\n        uses: codecov/codecov-action@v5\n        with:\n          files: ./coverage.out\n          flags: ${{ matrix.test-group }}\n          fail_ci_if_error: false\n"
  },
  {
    "path": ".github/workflows/build-canary-image.yml",
    "content": "name: Build Canary Image\n\non:\n  push:\n    branches: [main]\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.repository }}\n  cancel-in-progress: true\n\njobs:\n  build-frontend:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v6\n\n      - uses: pnpm/action-setup@v4.2.0\n        with:\n          version: 10\n      - uses: actions/setup-node@v6\n        with:\n          node-version: \"24\"\n          cache: pnpm\n          cache-dependency-path: \"web/pnpm-lock.yaml\"\n      - name: Get pnpm store directory\n        id: pnpm-cache\n        shell: bash\n        run: echo \"STORE_PATH=$(pnpm store path)\" >> $GITHUB_OUTPUT\n      - name: Setup pnpm cache\n        uses: actions/cache@v5\n        with:\n          path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}\n          key: ${{ runner.os }}-pnpm-store-${{ hashFiles('web/pnpm-lock.yaml') }}\n          restore-keys: ${{ runner.os }}-pnpm-store-\n      - run: pnpm install --frozen-lockfile\n        working-directory: web\n      - name: Run frontend build\n        run: pnpm release\n        working-directory: web\n\n      - name: Upload frontend artifacts\n        uses: actions/upload-artifact@v6\n        with:\n          name: frontend-dist\n          path: server/router/frontend/dist\n          retention-days: 1\n\n  build-push:\n    needs: build-frontend\n    runs-on: ubuntu-latest\n    permissions:\n      contents: read\n      packages: write\n    strategy:\n      fail-fast: false\n      matrix:\n        platform:\n          - linux/amd64\n          - linux/arm64\n    steps:\n      - uses: actions/checkout@v6\n\n      - name: Download frontend artifacts\n        uses: actions/download-artifact@v7\n        with:\n          name: frontend-dist\n          path: server/router/frontend/dist\n\n      - name: Set up QEMU\n        uses: docker/setup-qemu-action@v3\n\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v3\n\n      - name: Login to Docker Hub\n        uses: docker/login-action@v3\n        with:\n          username: ${{ secrets.DOCKER_HUB_USERNAME }}\n          password: ${{ secrets.DOCKER_HUB_TOKEN }}\n\n      - name: Login to GitHub Container Registry\n        uses: docker/login-action@v3\n        with:\n          registry: ghcr.io\n          username: ${{ github.actor }}\n          password: ${{ github.token }}\n\n      - name: Build and push by digest\n        id: build\n        uses: docker/build-push-action@v6\n        with:\n          context: .\n          file: ./scripts/Dockerfile\n          platforms: ${{ matrix.platform }}\n          cache-from: type=gha,scope=build-${{ matrix.platform }}\n          cache-to: type=gha,mode=max,scope=build-${{ matrix.platform }}\n          outputs: type=image,name=neosmemo/memos,push-by-digest=true,name-canonical=true,push=true\n\n      - name: Export digest\n        run: |\n          mkdir -p /tmp/digests\n          digest=\"${{ steps.build.outputs.digest }}\"\n          touch \"/tmp/digests/${digest#sha256:}\"\n\n      - name: Upload digest\n        uses: actions/upload-artifact@v6\n        with:\n          name: digests-${{ strategy.job-index }}\n          path: /tmp/digests/*\n          if-no-files-found: error\n          retention-days: 1\n\n  merge:\n    needs: build-push\n    runs-on: ubuntu-latest\n    permissions:\n      contents: read\n      packages: write\n    steps:\n      - name: Download digests\n        uses: actions/download-artifact@v7\n        with:\n          pattern: digests-*\n          merge-multiple: true\n          path: /tmp/digests\n\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v3\n\n      - name: Docker meta\n        id: meta\n        uses: docker/metadata-action@v5\n        with:\n          images: |\n            neosmemo/memos\n            ghcr.io/usememos/memos\n          flavor: |\n            latest=false\n          tags: |\n            type=raw,value=canary\n\n      - name: Login to Docker Hub\n        uses: docker/login-action@v3\n        with:\n          username: ${{ secrets.DOCKER_HUB_USERNAME }}\n          password: ${{ secrets.DOCKER_HUB_TOKEN }}\n\n      - name: Login to GitHub Container Registry\n        uses: docker/login-action@v3\n        with:\n          registry: ghcr.io\n          username: ${{ github.actor }}\n          password: ${{ github.token }}\n\n      - name: Create manifest list and push\n        working-directory: /tmp/digests\n        run: |\n          docker buildx imagetools create $(jq -cr '.tags | map(\"-t \" + .) | join(\" \")' <<< \"$DOCKER_METADATA_OUTPUT_JSON\") \\\n            $(printf 'neosmemo/memos@sha256:%s ' *)\n        env:\n          DOCKER_METADATA_OUTPUT_JSON: ${{ steps.meta.outputs.json }}\n\n      - name: Inspect images\n        run: |\n          docker buildx imagetools inspect neosmemo/memos:canary\n          docker buildx imagetools inspect ghcr.io/usememos/memos:canary\n"
  },
  {
    "path": ".github/workflows/demo-deploy.yml",
    "content": "name: Demo Deploy\n\non:\n  workflow_dispatch:\n\njobs:\n  deploy-demo:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Trigger Render Deploy\n        run: |\n          curl -X POST \"${{ secrets.RENDER_DEPLOY_HOOK }}\" \\\n            -H \"Content-Type: application/json\" \\\n            -d '{\"trigger\": \"github_action\"}'\n\n      - name: Deployment Status\n        run: echo \"Demo deployment triggered successfully on Render\"\n"
  },
  {
    "path": ".github/workflows/frontend-tests.yml",
    "content": "name: Frontend Tests\n\non:\n  push:\n    branches: [main]\n  pull_request:\n    branches: [main]\n    paths:\n      - \"web/**\"\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.ref }}\n  cancel-in-progress: true\n\nenv:\n  NODE_VERSION: \"24\"\n  PNPM_VERSION: \"10\"\n\njobs:\n  lint:\n    name: Lint\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v6\n\n      - name: Setup pnpm\n        uses: pnpm/action-setup@v4.2.0\n        with:\n          version: ${{ env.PNPM_VERSION }}\n\n      - name: Setup Node.js\n        uses: actions/setup-node@v6\n        with:\n          node-version: ${{ env.NODE_VERSION }}\n          cache: pnpm\n          cache-dependency-path: web/pnpm-lock.yaml\n\n      - name: Install dependencies\n        working-directory: web\n        run: pnpm install --frozen-lockfile\n\n      - name: Run lint\n        working-directory: web\n        run: pnpm lint\n\n  build:\n    name: Build\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v6\n\n      - name: Setup pnpm\n        uses: pnpm/action-setup@v4.2.0\n        with:\n          version: ${{ env.PNPM_VERSION }}\n\n      - name: Setup Node.js\n        uses: actions/setup-node@v6\n        with:\n          node-version: ${{ env.NODE_VERSION }}\n          cache: pnpm\n          cache-dependency-path: web/pnpm-lock.yaml\n\n      - name: Install dependencies\n        working-directory: web\n        run: pnpm install --frozen-lockfile\n\n      - name: Build frontend\n        working-directory: web\n        run: pnpm build\n"
  },
  {
    "path": ".github/workflows/proto-linter.yml",
    "content": "name: Proto Linter\n\non:\n  push:\n    branches: [main]\n  pull_request:\n    branches: [main]\n    paths:\n      - \"proto/**\"\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.ref }}\n  cancel-in-progress: true\n\njobs:\n  lint:\n    name: Lint Protos\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v6\n        with:\n          fetch-depth: 0\n\n      - name: Setup buf\n        uses: bufbuild/buf-setup-action@v1\n        with:\n          github_token: ${{ github.token }}\n\n      - name: Run buf lint\n        uses: bufbuild/buf-lint-action@v1\n        with:\n          input: proto\n\n      - name: Check buf format\n        run: |\n          if [[ $(buf format -d) ]]; then\n            echo \"❌ Proto files are not formatted. Run 'buf format -w' to fix.\"\n            exit 1\n          fi\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: Release\n\non:\n  push:\n    tags:\n      - \"v*.*.*\"\n  workflow_dispatch:\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.ref }}\n  cancel-in-progress: true\n\nenv:\n  GO_VERSION: \"1.26.1\"\n  NODE_VERSION: \"24\"\n  PNPM_VERSION: \"10\"\n  ARTIFACT_RETENTION_DAYS: 60\n  ARTIFACT_PREFIX: memos\n\njobs:\n  prepare:\n    name: Extract Version\n    runs-on: ubuntu-latest\n    outputs:\n      version: ${{ steps.version.outputs.version }}\n      tag: ${{ steps.version.outputs.tag }}\n    steps:\n      - name: Extract version\n        id: version\n        env:\n          REF_NAME: ${{ github.ref_name }}\n          EVENT_NAME: ${{ github.event_name }}\n        run: |\n          if [ \"$EVENT_NAME\" = \"workflow_dispatch\" ]; then\n            echo \"tag=\" >> \"$GITHUB_OUTPUT\"\n            echo \"version=manual-${GITHUB_SHA::7}\" >> \"$GITHUB_OUTPUT\"\n            exit 0\n          fi\n\n          echo \"tag=${REF_NAME}\" >> \"$GITHUB_OUTPUT\"\n          echo \"version=${REF_NAME#v}\" >> \"$GITHUB_OUTPUT\"\n\n  build-frontend:\n    name: Build Frontend\n    needs: prepare\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v6\n\n      - name: Setup pnpm\n        uses: pnpm/action-setup@v4.2.0\n        with:\n          version: ${{ env.PNPM_VERSION }}\n\n      - name: Setup Node.js\n        uses: actions/setup-node@v6\n        with:\n          node-version: ${{ env.NODE_VERSION }}\n          cache: pnpm\n          cache-dependency-path: web/pnpm-lock.yaml\n\n      - name: Get pnpm store directory\n        id: pnpm-cache\n        shell: bash\n        run: echo \"STORE_PATH=$(pnpm store path)\" >> \"$GITHUB_OUTPUT\"\n\n      - name: Setup pnpm cache\n        uses: actions/cache@v4\n        with:\n          path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}\n          key: ${{ runner.os }}-pnpm-store-${{ hashFiles('web/pnpm-lock.yaml') }}\n          restore-keys: ${{ runner.os }}-pnpm-store-\n\n      - name: Install dependencies\n        working-directory: web\n        run: pnpm install --frozen-lockfile\n\n      - name: Build frontend release assets\n        working-directory: web\n        run: pnpm release\n\n      - name: Upload frontend artifacts\n        uses: actions/upload-artifact@v4\n        with:\n          name: frontend-dist\n          path: server/router/frontend/dist\n          retention-days: 1\n\n  build-binaries:\n    name: Build ${{ matrix.goos }}-${{ matrix.goarch }}${{ matrix.goarm && format('v{0}', matrix.goarm) || '' }}\n    needs: [prepare, build-frontend]\n    runs-on: ubuntu-latest\n    strategy:\n      fail-fast: false\n      matrix:\n        include:\n          - goos: linux\n            goarch: amd64\n          - goos: linux\n            goarch: arm64\n          - goos: linux\n            goarch: arm\n            goarm: \"7\"\n          - goos: darwin\n            goarch: amd64\n          - goos: darwin\n            goarch: arm64\n          - goos: windows\n            goarch: amd64\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v6\n\n      - name: Setup Go\n        uses: actions/setup-go@v6\n        with:\n          go-version: ${{ env.GO_VERSION }}\n          cache: true\n\n      - name: Download frontend artifacts\n        uses: actions/download-artifact@v4\n        with:\n          name: frontend-dist\n          path: server/router/frontend/dist\n\n      - name: Build binary\n        env:\n          GOOS: ${{ matrix.goos }}\n          GOARCH: ${{ matrix.goarch }}\n          GOARM: ${{ matrix.goarm }}\n          CGO_ENABLED: \"0\"\n        run: |\n          output_name=\"memos\"\n          if [ \"$GOOS\" = \"windows\" ]; then\n            output_name=\"memos.exe\"\n          fi\n\n          mkdir -p build\n\n          go build \\\n            -trimpath \\\n            -ldflags=\"-s -w -X github.com/usememos/memos/internal/version.Version=${{ needs.prepare.outputs.version }} -extldflags '-static'\" \\\n            -tags netgo,osusergo \\\n            -o \"build/${output_name}\" \\\n            ./cmd/memos\n\n      - name: Package binary\n        env:\n          VERSION: ${{ needs.prepare.outputs.version }}\n          GOOS: ${{ matrix.goos }}\n          GOARCH: ${{ matrix.goarch }}\n          GOARM: ${{ matrix.goarm }}\n        run: |\n          cd build\n\n          package_name=\"${ARTIFACT_PREFIX}_${VERSION}_${GOOS}_${GOARCH}\"\n          if [ -n \"$GOARM\" ]; then\n            package_name=\"${package_name}v${GOARM}\"\n          fi\n\n          if [ \"$GOOS\" = \"windows\" ]; then\n            artifact_name=\"${package_name}.zip\"\n            zip -q \"${artifact_name}\" memos.exe\n          else\n            artifact_name=\"${package_name}.tar.gz\"\n            tar czf \"${artifact_name}\" memos\n          fi\n\n          echo \"artifact_name=${artifact_name}\" >> \"$GITHUB_ENV\"\n\n      - name: Upload binary artifact\n        uses: actions/upload-artifact@v4\n        with:\n          name: ${{ env.artifact_name }}\n          path: build/${{ env.artifact_name }}\n          retention-days: ${{ env.ARTIFACT_RETENTION_DAYS }}\n\n  checksums:\n    name: Generate Checksums\n    needs: [prepare, build-binaries]\n    runs-on: ubuntu-latest\n    steps:\n      - name: Download binary artifacts\n        uses: actions/download-artifact@v4\n        with:\n          path: artifacts\n          pattern: ${{ env.ARTIFACT_PREFIX }}_*\n          merge-multiple: true\n\n      - name: Generate checksums\n        working-directory: artifacts\n        run: sha256sum * > checksums.txt\n\n      - name: Upload checksum artifact\n        uses: actions/upload-artifact@v4\n        with:\n          name: checksums\n          path: artifacts/checksums.txt\n          retention-days: ${{ env.ARTIFACT_RETENTION_DAYS }}\n\n  release:\n    name: Publish GitHub Release\n    needs: [prepare, build-binaries, checksums]\n    if: github.event_name != 'workflow_dispatch'\n    runs-on: ubuntu-latest\n    permissions:\n      contents: write\n    steps:\n      - name: Download binary artifacts\n        uses: actions/download-artifact@v4\n        with:\n          path: artifacts\n          pattern: ${{ env.ARTIFACT_PREFIX }}_*\n          merge-multiple: true\n\n      - name: Download checksum artifact\n        uses: actions/download-artifact@v4\n        with:\n          name: checksums\n          path: artifacts\n\n      - name: Publish release assets\n        uses: softprops/action-gh-release@v2\n        with:\n          tag_name: ${{ needs.prepare.outputs.tag }}\n          name: ${{ needs.prepare.outputs.tag }}\n          generate_release_notes: true\n          files: artifacts/*\n\n  build-push:\n    name: Build Image ${{ matrix.platform }}\n    needs: [prepare, build-frontend]\n    if: github.event_name != 'workflow_dispatch'\n    runs-on: ubuntu-latest\n    permissions:\n      contents: read\n      packages: write\n    strategy:\n      fail-fast: false\n      matrix:\n        platform:\n          - linux/amd64\n          - linux/arm/v7\n          - linux/arm64\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v6\n\n      - name: Download frontend artifacts\n        uses: actions/download-artifact@v4\n        with:\n          name: frontend-dist\n          path: server/router/frontend/dist\n\n      - name: Set up QEMU\n        uses: docker/setup-qemu-action@v3\n\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v3\n\n      - name: Login to Docker Hub\n        uses: docker/login-action@v3\n        with:\n          username: ${{ secrets.DOCKER_HUB_USERNAME }}\n          password: ${{ secrets.DOCKER_HUB_TOKEN }}\n\n      - name: Login to GitHub Container Registry\n        uses: docker/login-action@v3\n        with:\n          registry: ghcr.io\n          username: ${{ github.actor }}\n          password: ${{ github.token }}\n\n      - name: Build and push by digest\n        id: build\n        uses: docker/build-push-action@v6\n        with:\n          context: .\n          file: ./scripts/Dockerfile\n          platforms: ${{ matrix.platform }}\n          build-args: |\n            VERSION=${{ needs.prepare.outputs.version }}\n            COMMIT=${{ github.sha }}\n          cache-from: type=gha,scope=release-${{ matrix.platform }}\n          cache-to: type=gha,mode=max,scope=release-${{ matrix.platform }}\n          outputs: type=image,name=neosmemo/memos,push-by-digest=true,name-canonical=true,push=true\n\n      - name: Export digest\n        run: |\n          mkdir -p /tmp/digests\n          digest=\"${{ steps.build.outputs.digest }}\"\n          touch \"/tmp/digests/${digest#sha256:}\"\n\n      - name: Upload digest\n        uses: actions/upload-artifact@v4\n        with:\n          name: digests-${{ strategy.job-index }}\n          path: /tmp/digests/*\n          if-no-files-found: error\n          retention-days: 1\n\n  merge-images:\n    name: Publish Stable Image Tags\n    needs: [prepare, build-push]\n    if: github.event_name != 'workflow_dispatch'\n    runs-on: ubuntu-latest\n    permissions:\n      contents: read\n      packages: write\n    steps:\n      - name: Download digests\n        uses: actions/download-artifact@v4\n        with:\n          pattern: digests-*\n          merge-multiple: true\n          path: /tmp/digests\n\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v3\n\n      - name: Login to Docker Hub\n        uses: docker/login-action@v3\n        with:\n          username: ${{ secrets.DOCKER_HUB_USERNAME }}\n          password: ${{ secrets.DOCKER_HUB_TOKEN }}\n\n      - name: Login to GitHub Container Registry\n        uses: docker/login-action@v3\n        with:\n          registry: ghcr.io\n          username: ${{ github.actor }}\n          password: ${{ github.token }}\n\n      - name: Create manifest list and push\n        working-directory: /tmp/digests\n        run: |\n          version=\"${{ needs.prepare.outputs.version }}\"\n          major_minor=$(echo \"$version\" | cut -d. -f1,2)\n          docker buildx imagetools create \\\n            -t \"neosmemo/memos:${version}\" \\\n            -t \"neosmemo/memos:${major_minor}\" \\\n            -t \"neosmemo/memos:stable\" \\\n            -t \"ghcr.io/usememos/memos:${version}\" \\\n            -t \"ghcr.io/usememos/memos:${major_minor}\" \\\n            -t \"ghcr.io/usememos/memos:stable\" \\\n            $(printf 'neosmemo/memos@sha256:%s ' *)\n\n      - name: Inspect images\n        run: |\n          docker buildx imagetools inspect neosmemo/memos:${{ needs.prepare.outputs.version }}\n          docker buildx imagetools inspect neosmemo/memos:stable\n"
  },
  {
    "path": ".github/workflows/stale.yml",
    "content": "name: Close Stale\n\non:\n  schedule:\n    - cron: \"0 */8 * * *\" # Every 8 hours\n\njobs:\n  close-stale:\n    name: Close Stale Issues and PRs\n    runs-on: ubuntu-latest\n    permissions:\n      issues: write\n      pull-requests: write\n    steps:\n      - name: Mark and close stale issues and PRs\n        uses: actions/stale@v10.1.1\n        with:\n          # Issues: mark stale after 14 days of inactivity, close after 3 more days\n          days-before-issue-stale: 14\n          days-before-issue-close: 3\n\n          # Pull requests: mark stale after 14 days of inactivity, close after 3 more days\n          days-before-pr-stale: 14\n          days-before-pr-close: 3\n"
  },
  {
    "path": ".gitignore",
    "content": "# temp folder\ntmp\n\n# Frontend asset\nweb/dist\n\n# Build artifacts\nbuild/\nbin/\nmemos\n\n# Plan/design documents\ndocs/plans/\n\n.DS_Store\n\n# Jetbrains\n.idea\n\n# Docker Compose Environment File\n.env\n\ndist\n\n# VSCode settings\n.vscode\n\n# Git worktrees\n.worktrees/\n"
  },
  {
    "path": ".golangci.yaml",
    "content": "version: \"2\"\n\nlinters:\n  enable:\n    - revive\n    - govet\n    - staticcheck\n    - misspell\n    - gocritic\n    - sqlclosecheck\n    - rowserrcheck\n    - nilerr\n    - godot\n    - forbidigo\n    - mirror\n    - bodyclose\n  disable:\n    - errcheck\n  settings:\n    exhaustive:\n      explicit-exhaustive-switch: false\n    staticcheck:\n      checks:\n        - all\n        - -ST1000\n        - -ST1003\n        - -ST1021\n        - -QF1003\n    revive:\n      # Default to run all linters so that new rules in the future could automatically be added to the static check.\n      enable-all-rules: true\n      rules:\n        # The following rules are too strict and make coding harder. We do not enable them for now.\n        - name: file-header\n          disabled: true\n        - name: line-length-limit\n          disabled: true\n        - name: function-length\n          disabled: true\n        - name: max-public-structs\n          disabled: true\n        - name: function-result-limit\n          disabled: true\n        - name: banned-characters\n          disabled: true\n        - name: argument-limit\n          disabled: true\n        - name: cognitive-complexity\n          disabled: true\n        - name: cyclomatic\n          disabled: true\n        - name: confusing-results\n          disabled: true\n        - name: add-constant\n          disabled: true\n        - name: flag-parameter\n          disabled: true\n        - name: nested-structs\n          disabled: true\n        - name: import-shadowing\n          disabled: true\n        - name: early-return\n          disabled: true\n        - name: use-any\n          disabled: true\n        - name: exported\n          disabled: true\n        - name: unhandled-error\n          disabled: true\n        - name: if-return\n          disabled: true\n        - name: max-control-nesting\n          disabled: true\n        - name: redefines-builtin-id\n          disabled: true\n        - name: package-comments\n          disabled: true\n    gocritic:\n      disabled-checks:\n        - ifElseChain\n    govet:\n      settings:\n        printf: # The name of the analyzer, run `go tool vet help` to see the list of all analyzers\n          funcs: # Run `go tool vet help printf` to see the full configuration of `printf`.\n            - common.Errorf\n      enable-all: true\n      disable:\n        - fieldalignment\n        - shadow\n    forbidigo:\n      forbid:\n        - pattern: 'fmt\\.Errorf(# Please use errors\\.Wrap\\|Wrapf\\|Errorf instead)?'\n        - pattern: 'ioutil\\.ReadDir(# Please use os\\.ReadDir)?'\n\nformatters:\n  enable:\n    - goimports\n  settings:\n    goimports:\n      local-prefixes:\n        - github.com/usememos/memos\n"
  },
  {
    "path": "AGENTS.md",
    "content": "# AGENTS.md\n\nThis file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.\n\nSelf-hosted note-taking tool. Go 1.26 backend (Echo v5, Connect RPC + gRPC-Gateway), React 18 + TypeScript 5.9 + Vite 7 frontend, Protocol Buffers API, SQLite/MySQL/PostgreSQL.\n\n## Commands\n\n```bash\n# Backend\ngo run ./cmd/memos --port 8081    # Start dev server\ngo test ./...                      # Run all tests\ngo test -v ./store/...             # Run store tests (all 3 DB drivers via TestContainers)\ngo test -v -race ./server/...      # Run server tests with race detection\ngo test -v -run TestFoo ./pkg/...  # Run a single test\ngolangci-lint run                  # Lint (v2, config: .golangci.yaml)\ngolangci-lint run --fix            # Auto-fix lint issues (includes goimports)\n\n# Frontend (cd web)\npnpm install                       # Install deps\npnpm dev                           # Dev server (:3001, proxies API to :8081)\npnpm lint                          # Type check + Biome lint\npnpm lint:fix                      # Auto-fix lint issues\npnpm format                        # Format code\npnpm build                         # Production build\npnpm release                       # Build to server/router/frontend/dist\n\n# Protocol Buffers (cd proto)\nbuf generate                       # Regenerate Go + TypeScript + OpenAPI\nbuf lint                           # Lint proto files\nbuf format -w                      # Format proto files\n```\n\n## Architecture\n\n```\ncmd/memos/main.go           # Cobra CLI + Viper config, server init\n\nserver/\n├── server.go               # Echo v5 HTTP server, background runners\n├── auth/                   # JWT access (15min) + refresh (30d) tokens, PAT\n├── router/\n│   ├── api/v1/             # 8 gRPC services (Connect + Gateway)\n│   │   ├── acl_config.go   # Public endpoints whitelist\n│   │   ├── sse_hub.go      # Server-Sent Events (live updates)\n│   │   └── mcp/            # MCP server for AI assistants\n│   ├── frontend/           # SPA static file serving\n│   ├── fileserver/         # Native HTTP file server (thumbnails, range requests)\n│   └── rss/                # RSS feeds\n└── runner/                 # Background: memo payload processing, S3 presign refresh\n\nstore/\n├── driver.go               # Database driver interface\n├── store.go                # Store wrapper + in-memory cache (TTL 10min, max 1000)\n├── migrator.go             # Migration logic (LATEST.sql for fresh, incremental for upgrades)\n└── db/{sqlite,mysql,postgres}/  # Driver implementations\n\nproto/\n├── api/v1/                 # Service definitions\n├── store/                  # Internal storage messages\n└── gen/                    # Generated Go, TypeScript, OpenAPI\n\nplugin/                     # scheduler, cron, email, filter (CEL), webhook,\n                            # markdown (Goldmark), httpgetter, idp (OAuth2), storage/s3\n\nweb/src/\n├── connect.ts              # Connect RPC client + auth interceptor + token refresh\n├── auth-state.ts           # Token storage (localStorage + BroadcastChannel cross-tab)\n├── contexts/               # AuthContext, InstanceContext, ViewContext, MemoFilterContext\n├── hooks/                  # React Query hooks (useMemoQueries, useUserQueries, etc.)\n├── lib/query-client.ts     # React Query v5 (staleTime: 30s, gcTime: 5min)\n├── router/index.tsx        # Route definitions\n├── components/             # UI components (Radix UI primitives, MemoEditor, Settings, etc.)\n├── themes/                 # CSS themes (default, dark, paper) — OKLch color tokens\n└── pages/                  # Page components\n```\n\n## Conventions\n\n### Go\n- **Errors:** `errors.Wrap(err, \"context\")` from `github.com/pkg/errors`. Never `fmt.Errorf` (lint-enforced via forbidigo).\n- **gRPC errors:** `status.Errorf(codes.X, \"message\")` from service methods.\n- **Imports:** stdlib, then third-party, then local (`github.com/usememos/memos`). Enforced by goimports (runs as golangci-lint formatter).\n- **Comments:** All exported functions must have doc comments (godot enforced).\n\n### Frontend\n- **Imports:** Use `@/` alias for absolute imports.\n- **Formatting:** Biome — 140 char lines, double quotes, always semicolons, 2-space indent.\n- **State:** Server data via React Query hooks (`hooks/`). Client state via React Context (`contexts/`).\n- **Styling:** Tailwind CSS v4 (`@tailwindcss/vite`), `cn()` utility (clsx + tailwind-merge), CVA for variants.\n\n### Database & Proto\n- **DB changes:** Migration files for all 3 drivers + update `LATEST.sql`.\n- **Proto changes:** Run `buf generate`. Generated code: `proto/gen/` and `web/src/types/proto/`.\n- **Public endpoints:** Add to `server/router/api/v1/acl_config.go`.\n\n## CI/CD\n\n- **backend-tests.yml:** Go 1.26.1, golangci-lint v2.4.0, tests parallelized by group (store, server, plugin, other)\n- **frontend-tests.yml:** Node 24, pnpm 10, lint + build\n- **proto-linter.yml:** buf lint + format check\n- **Docker:** Multi-stage (`scripts/Dockerfile`), Alpine 3.21, non-root user, port 5230, multi-arch (amd64/arm64/arm/v7)\n"
  },
  {
    "path": "CLAUDE.md",
    "content": "# CLAUDE.md\n\nThis file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.\n\nSee `AGENTS.md` for full architecture, workflows, conventions, and patterns.\n"
  },
  {
    "path": "CODEOWNERS",
    "content": "# These owners will be the default owners for everything in the repo.\n* @usememos/moderators\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2025 Memos\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "<div align=\"center\">\n  <p>\n    <span>Featured Sponsor:</span>\n    <a href=\"https://go.warp.dev/memos\" target=\"_blank\" rel=\"noopener\">\n      <b>Warp</b>\n      <span>— The AI-powered terminal built for speed and collaboration</span>\n    </a>\n  </p>\n  <a href=\"https://go.warp.dev/memos\" target=\"_blank\" rel=\"noopener\">\n    <img alt=\"Warp sponsorship\" height=\"196\" src=\"https://raw.githubusercontent.com/warpdotdev/brand-assets/refs/heads/main/Github/Sponsor/Warp-Github-LG-03.png\">\n  </a>\n</div>\n\n# Memos\n\n<img align=\"right\" height=\"96px\" src=\"https://raw.githubusercontent.com/usememos/.github/refs/heads/main/assets/logo-rounded.png\" alt=\"Memos\" />\n\nOpen-source, self-hosted note-taking tool built for quick capture. Markdown-native, lightweight, and fully yours.\n\n[![Home](https://img.shields.io/badge/🏠-usememos.com-blue?style=flat-square)](https://usememos.com)\n[![Live Demo](https://img.shields.io/badge/✨-Try%20Demo-orange?style=flat-square)](https://demo.usememos.com/)\n[![Docs](https://img.shields.io/badge/📚-Documentation-green?style=flat-square)](https://usememos.com/docs)\n[![Discord](https://img.shields.io/badge/💬-Discord-5865f2?style=flat-square&logo=discord&logoColor=white)](https://discord.gg/tfPJa4UmAv)\n[![Docker Pulls](https://img.shields.io/docker/pulls/neosmemo/memos?style=flat-square&logo=docker)](https://hub.docker.com/r/neosmemo/memos)\n\n<img src=\"https://raw.githubusercontent.com/usememos/.github/refs/heads/main/assets/demo.png\" alt=\"Memos Demo Screenshot\" height=\"512\" />\n\n### 💎 Featured Sponsors\n\n[**Warp** — The AI-powered terminal built for speed and collaboration](https://go.warp.dev/memos)\n\n<a href=\"https://go.warp.dev/memos\" target=\"_blank\" rel=\"noopener\">\n  <img src=\"https://raw.githubusercontent.com/warpdotdev/brand-assets/refs/heads/main/Logos/Warp-Wordmark-Black.png\" alt=\"Warp - The AI-powered terminal built for speed and collaboration\" height=\"44\" />\n</a>\n\n<p></p>\n\n[**TestMu AI** - The world’s first full-stack Agentic AI Quality Engineering platform](https://www.testmuai.com/?utm_medium=sponsor&utm_source=memos)\n  \n<a href=\"https://www.testmuai.com/?utm_medium=sponsor&utm_source=memos\" target=\"_blank\" rel=\"noopener\">\n  <img src=\"https://usememos.com/sponsors/testmu.svg\" alt=\"TestMu AI\" height=\"36\" />\n</a>\n\n<p></p>\n\n[**SSD Nodes** - Affordable VPS hosting for self-hosters](https://ssdnodes.com/?utm_source=memos&utm_medium=sponsor)\n  \n<a href=\"https://ssdnodes.com/?utm_source=memos&utm_medium=sponsor\" target=\"_blank\" rel=\"noopener\">\n  <img src=\"https://usememos.com/sponsors/ssd-nodes.svg\" alt=\"SSD Nodes\" height=\"72\" />\n</a>\n\n## Features\n\n- **Instant Capture** — Timeline-first UI. Open, write, done — no folders to navigate.\n- **Total Data Ownership** — Self-hosted on your infrastructure. Notes stored in Markdown, always portable. Zero telemetry.\n- **Radical Simplicity** — Single Go binary, ~20MB Docker image. One command to deploy with SQLite, MySQL, or PostgreSQL.\n- **Open & Extensible** — MIT-licensed with full REST and gRPC APIs for integration.\n\n## Quick Start\n\n### Docker (Recommended)\n\n```bash\ndocker run -d \\\n  --name memos \\\n  -p 5230:5230 \\\n  -v ~/.memos:/var/opt/memos \\\n  neosmemo/memos:stable\n```\n\nOpen `http://localhost:5230` and start writing!\n\n### Native Binary\n\n```bash\ncurl -fsSL https://raw.githubusercontent.com/usememos/memos/main/scripts/install.sh | sh\n```\n\n### Try the Live Demo\n\nDon't want to install yet? Try our [live demo](https://demo.usememos.com/) first!\n\n### Other Installation Methods\n\n- **Docker Compose** - Recommended for production deployments\n- **Pre-built Binaries** - Available for Linux, macOS, and Windows\n- **Kubernetes** - Helm charts and manifests available\n- **Build from Source** - For development and customization\n\nSee our [installation guide](https://usememos.com/docs/deploy) for detailed instructions.\n\n## Contributing\n\nContributions are welcome — bug reports, feature suggestions, pull requests, documentation, and translations.\n\n- [Report bugs](https://github.com/usememos/memos/issues/new?template=bug_report.md)\n- [Suggest features](https://github.com/usememos/memos/issues/new?template=feature_request.md)\n- [Submit pull requests](https://github.com/usememos/memos/pulls)\n- [Improve documentation](https://github.com/usememos/dotcom)\n- [Help with translations](https://github.com/usememos/memos/tree/main/web/src/locales)\n\n## Sponsors\n\nLove Memos? [Sponsor us on GitHub](https://github.com/sponsors/usememos) to help keep the project growing!\n\n## Star History\n\n[![Star History Chart](https://api.star-history.com/svg?repos=usememos/memos&type=Date)](https://star-history.com/#usememos/memos&Date)\n\n## License\n\nMemos is open-source software licensed under the [MIT License](LICENSE). See our [Privacy Policy](https://usememos.com/privacy) for details on data handling.\n\n---\n\n**[Website](https://usememos.com)** • **[Documentation](https://usememos.com/docs)** • **[Demo](https://demo.usememos.com/)** • **[Discord](https://discord.gg/tfPJa4UmAv)** • **[X/Twitter](https://x.com/usememos)**\n\n<a href=\"https://vercel.com/oss\">\n  <img alt=\"Vercel OSS Program\" src=\"https://vercel.com/oss/program-badge.svg\" />\n</a>\n"
  },
  {
    "path": "SECURITY.md",
    "content": "# Security Policy\n\n## Project Status\n\nMemos is currently in beta (v0.x). While we take security seriously, we are not yet ready for formal CVE assignments or coordinated disclosure programs.\n\n## Reporting Security Issues\n\n### For All Security Concerns:\nPlease report via **email only**: dev@usememos.com\n\n**DO NOT open public GitHub issues for security vulnerabilities.**\n\nInclude in your report:\n- Description of the issue\n- Steps to reproduce\n- Affected versions\n- Your assessment of severity\n\n### What to Expect:\n- We will acknowledge your report as soon as we can\n- Fixes will be included in regular releases without special security advisories\n- No CVEs will be assigned during the beta phase\n- Credit will be given in release notes if you wish\n\n### For Non-Security Bugs:\nUse GitHub issues for functionality bugs, feature requests, and general questions.\n\n## Philosophy\n\nAs a beta project, we prioritize:\n1. **Rapid iteration** over lengthy disclosure timelines\n2. **Quick patches** over formal security processes\n3. **Transparency** about our beta status\n\nWe plan to implement formal vulnerability disclosure and CVE handling after reaching v1.0 stable.\n\n## Self-Hosting Security\n\nSince Memos is self-hosted software:\n- Keep your instance updated to the latest release\n- Don't expose your instance directly to the internet without authentication\n- Use reverse proxies (nginx, Caddy) with rate limiting\n- Review the deployment documentation for security best practices\n\nThank you for helping improve Memos!\n"
  },
  {
    "path": "go.mod",
    "content": "module github.com/usememos/memos\n\ngo 1.26.1\n\nrequire (\n\tconnectrpc.com/connect v1.19.1\n\tgithub.com/aws/aws-sdk-go-v2 v1.41.4\n\tgithub.com/aws/aws-sdk-go-v2/config v1.32.12\n\tgithub.com/aws/aws-sdk-go-v2/credentials v1.19.12\n\tgithub.com/aws/aws-sdk-go-v2/service/s3 v1.97.1\n\tgithub.com/docker/docker v28.5.2+incompatible\n\tgithub.com/go-sql-driver/mysql v1.9.3\n\tgithub.com/google/cel-go v0.27.0\n\tgithub.com/google/uuid v1.6.0\n\tgithub.com/gorilla/feeds v1.2.0\n\tgithub.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0\n\tgithub.com/joho/godotenv v1.5.1\n\tgithub.com/labstack/echo/v5 v5.0.4\n\tgithub.com/lib/pq v1.11.2\n\tgithub.com/lithammer/shortuuid/v4 v4.2.0\n\tgithub.com/mark3labs/mcp-go v0.45.0\n\tgithub.com/pkg/errors v0.9.1\n\tgithub.com/spf13/cobra v1.10.2\n\tgithub.com/spf13/viper v1.21.0\n\tgithub.com/stretchr/testify v1.11.1\n\tgithub.com/testcontainers/testcontainers-go v0.41.0\n\tgithub.com/testcontainers/testcontainers-go/modules/mysql v0.41.0\n\tgithub.com/testcontainers/testcontainers-go/modules/postgres v0.41.0\n\tgithub.com/yuin/goldmark v1.7.16\n\tgolang.org/x/crypto v0.49.0\n\tgolang.org/x/mod v0.34.0\n\tgolang.org/x/net v0.52.0\n\tgolang.org/x/oauth2 v0.36.0\n\tgolang.org/x/sync v0.20.0\n\tgoogle.golang.org/genproto v0.0.0-20260316180232-0b37fe3546d5\n\tgoogle.golang.org/genproto/googleapis/api v0.0.0-20260316172706-e463d84ca32d\n\tgoogle.golang.org/grpc v1.79.2\n\tmodernc.org/sqlite v1.46.1\n)\n\nrequire (\n\tcel.dev/expr v0.25.1 // indirect\n\tdario.cat/mergo v1.0.2 // indirect\n\tfilippo.io/edwards25519 v1.1.0 // indirect\n\tgithub.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect\n\tgithub.com/Microsoft/go-winio v0.6.2 // indirect\n\tgithub.com/antlr4-go/antlr/v4 v4.13.1 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/signin v1.0.8 // indirect\n\tgithub.com/bahlo/generic-list-go v0.2.0 // indirect\n\tgithub.com/buger/jsonparser v1.1.1 // indirect\n\tgithub.com/cenkalti/backoff/v4 v4.3.0 // indirect\n\tgithub.com/cespare/xxhash/v2 v2.3.0 // indirect\n\tgithub.com/containerd/errdefs v1.0.0 // indirect\n\tgithub.com/containerd/errdefs/pkg v0.3.0 // indirect\n\tgithub.com/containerd/log v0.1.0 // indirect\n\tgithub.com/containerd/platforms v0.2.1 // indirect\n\tgithub.com/cpuguy83/dockercfg v0.3.2 // indirect\n\tgithub.com/distribution/reference v0.6.0 // indirect\n\tgithub.com/docker/go-connections v0.6.0 // indirect\n\tgithub.com/docker/go-units v0.5.0 // indirect\n\tgithub.com/dustin/go-humanize v1.0.1 // indirect\n\tgithub.com/ebitengine/purego v0.10.0 // indirect\n\tgithub.com/felixge/httpsnoop v1.0.4 // indirect\n\tgithub.com/fsnotify/fsnotify v1.9.0 // indirect\n\tgithub.com/go-logr/logr v1.4.3 // indirect\n\tgithub.com/go-logr/stdr v1.2.2 // indirect\n\tgithub.com/go-ole/go-ole v1.2.6 // indirect\n\tgithub.com/go-viper/mapstructure/v2 v2.4.0 // indirect\n\tgithub.com/inconshreveable/mousetrap v1.1.0 // indirect\n\tgithub.com/invopop/jsonschema v0.13.0 // indirect\n\tgithub.com/klauspost/compress v1.18.2 // indirect\n\tgithub.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect\n\tgithub.com/magiconair/properties v1.8.10 // indirect\n\tgithub.com/mailru/easyjson v0.7.7 // indirect\n\tgithub.com/moby/docker-image-spec v1.3.1 // indirect\n\tgithub.com/moby/go-archive v0.2.0 // indirect\n\tgithub.com/moby/patternmatcher v0.6.0 // indirect\n\tgithub.com/moby/sys/sequential v0.6.0 // indirect\n\tgithub.com/moby/sys/user v0.4.0 // indirect\n\tgithub.com/moby/sys/userns v0.1.0 // indirect\n\tgithub.com/moby/term v0.5.2 // indirect\n\tgithub.com/morikuni/aec v1.0.0 // indirect\n\tgithub.com/ncruces/go-strftime v1.0.0 // indirect\n\tgithub.com/opencontainers/go-digest v1.0.0 // indirect\n\tgithub.com/opencontainers/image-spec v1.1.1 // indirect\n\tgithub.com/pelletier/go-toml/v2 v2.2.4 // indirect\n\tgithub.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect\n\tgithub.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect\n\tgithub.com/sagikazarmark/locafero v0.11.0 // indirect\n\tgithub.com/shirou/gopsutil/v4 v4.26.2 // indirect\n\tgithub.com/sirupsen/logrus v1.9.3 // indirect\n\tgithub.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect\n\tgithub.com/spf13/afero v1.15.0 // indirect\n\tgithub.com/spf13/cast v1.10.0 // indirect\n\tgithub.com/spf13/pflag v1.0.10 // indirect\n\tgithub.com/subosito/gotenv v1.6.0 // indirect\n\tgithub.com/tklauser/go-sysconf v0.3.16 // indirect\n\tgithub.com/tklauser/numcpus v0.11.0 // indirect\n\tgithub.com/wk8/go-ordered-map/v2 v2.1.8 // indirect\n\tgithub.com/yosida95/uritemplate/v3 v3.0.2 // indirect\n\tgithub.com/yusufpapurcu/wmi v1.2.4 // indirect\n\tgo.opentelemetry.io/auto/sdk v1.2.1 // indirect\n\tgo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect\n\tgo.opentelemetry.io/otel v1.41.0 // indirect\n\tgo.opentelemetry.io/otel/metric v1.41.0 // indirect\n\tgo.opentelemetry.io/otel/trace v1.41.0 // indirect\n\tgo.yaml.in/yaml/v3 v3.0.4 // indirect\n\tgolang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect\n\tgolang.org/x/image v0.30.0 // indirect\n\tgoogle.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c // indirect\n\tmodernc.org/libc v1.67.6 // indirect\n\tmodernc.org/mathutil v1.7.1 // indirect\n\tmodernc.org/memory v1.11.0 // indirect\n)\n\nrequire (\n\tgithub.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.7 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/internal/v4a v1.4.21 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.12 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.20 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/sso v1.30.13 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/sts v1.41.9 // indirect\n\tgithub.com/aws/smithy-go v1.24.2 // indirect\n\tgithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect\n\tgithub.com/disintegration/imaging v1.6.2\n\tgithub.com/golang-jwt/jwt/v5 v5.3.1\n\tgithub.com/mattn/go-isatty v0.0.20 // indirect\n\tgithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect\n\tgolang.org/x/sys v0.42.0 // indirect\n\tgolang.org/x/text v0.35.0\n\tgolang.org/x/time v0.14.0 // indirect\n\tgoogle.golang.org/protobuf v1.36.11\n\tgopkg.in/yaml.v3 v3.0.1 // indirect\n)\n"
  },
  {
    "path": "go.sum",
    "content": "cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4=\ncel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=\nconnectrpc.com/connect v1.19.1 h1:R5M57z05+90EfEvCY1b7hBxDVOUl45PrtXtAV2fOC14=\nconnectrpc.com/connect v1.19.1/go.mod h1:tN20fjdGlewnSFeZxLKb0xwIZ6ozc3OQs2hTXy4du9w=\ndario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=\ndario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=\nfilippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=\nfilippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=\ngithub.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk=\ngithub.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=\ngithub.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=\ngithub.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=\ngithub.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=\ngithub.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=\ngithub.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ=\ngithub.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw=\ngithub.com/aws/aws-sdk-go-v2 v1.41.4 h1:10f50G7WyU02T56ox1wWXq+zTX9I1zxG46HYuG1hH/k=\ngithub.com/aws/aws-sdk-go-v2 v1.41.4/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o=\ngithub.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.7 h1:3kGOqnh1pPeddVa/E37XNTaWJ8W6vrbYV9lJEkCnhuY=\ngithub.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.7/go.mod h1:lyw7GFp3qENLh7kwzf7iMzAxDn+NzjXEAGjKS2UOKqI=\ngithub.com/aws/aws-sdk-go-v2/config v1.32.12 h1:O3csC7HUGn2895eNrLytOJQdoL2xyJy0iYXhoZ1OmP0=\ngithub.com/aws/aws-sdk-go-v2/config v1.32.12/go.mod h1:96zTvoOFR4FURjI+/5wY1vc1ABceROO4lWgWJuxgy0g=\ngithub.com/aws/aws-sdk-go-v2/credentials v1.19.12 h1:oqtA6v+y5fZg//tcTWahyN9PEn5eDU/Wpvc2+kJ4aY8=\ngithub.com/aws/aws-sdk-go-v2/credentials v1.19.12/go.mod h1:U3R1RtSHx6NB0DvEQFGyf/0sbrpJrluENHdPy1j/3TE=\ngithub.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20 h1:zOgq3uezl5nznfoK3ODuqbhVg1JzAGDUhXOsU0IDCAo=\ngithub.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20/go.mod h1:z/MVwUARehy6GAg/yQ1GO2IMl0k++cu1ohP9zo887wE=\ngithub.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20 h1:CNXO7mvgThFGqOFgbNAP2nol2qAWBOGfqR/7tQlvLmc=\ngithub.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20/go.mod h1:oydPDJKcfMhgfcgBUZaG+toBbwy8yPWubJXBVERtI4o=\ngithub.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20 h1:tN6W/hg+pkM+tf9XDkWUbDEjGLb+raoBMFsTodcoYKw=\ngithub.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20/go.mod h1:YJ898MhD067hSHA6xYCx5ts/jEd8BSOLtQDL3iZsvbc=\ngithub.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 h1:qYQ4pzQ2Oz6WpQ8T3HvGHnZydA72MnLuFK9tJwmrbHw=\ngithub.com/aws/aws-sdk-go-v2/internal/ini v1.8.6/go.mod h1:O3h0IK87yXci+kg6flUKzJnWeziQUKciKrLjcatSNcY=\ngithub.com/aws/aws-sdk-go-v2/internal/v4a v1.4.21 h1:SwGMTMLIlvDNyhMteQ6r8IJSBPlRdXX5d4idhIGbkXA=\ngithub.com/aws/aws-sdk-go-v2/internal/v4a v1.4.21/go.mod h1:UUxgWxofmOdAMuqEsSppbDtGKLfR04HGsD0HXzvhI1k=\ngithub.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 h1:5EniKhLZe4xzL7a+fU3C2tfUN4nWIqlLesfrjkuPFTY=\ngithub.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI=\ngithub.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.12 h1:qtJZ70afD3ISKWnoX3xB0J2otEqu3LqicRcDBqsj0hQ=\ngithub.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.12/go.mod h1:v2pNpJbRNl4vEUWEh5ytQok0zACAKfdmKS51Hotc3pQ=\ngithub.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20 h1:2HvVAIq+YqgGotK6EkMf+KIEqTISmTYh5zLpYyeTo1Y=\ngithub.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20/go.mod h1:V4X406Y666khGa8ghKmphma/7C0DAtEQYhkq9z4vpbk=\ngithub.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.20 h1:siU1A6xjUZ2N8zjTHSXFhB9L/2OY8Dqs0xXiLjF30jA=\ngithub.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.20/go.mod h1:4TLZCmVJDM3FOu5P5TJP0zOlu9zWgDWU7aUxWbr+rcw=\ngithub.com/aws/aws-sdk-go-v2/service/s3 v1.97.1 h1:csi9NLpFZXb9fxY7rS1xVzgPRGMt7MSNWeQ6eo247kE=\ngithub.com/aws/aws-sdk-go-v2/service/s3 v1.97.1/go.mod h1:qXVal5H0ChqXP63t6jze5LmFalc7+ZE7wOdLtZ0LCP0=\ngithub.com/aws/aws-sdk-go-v2/service/signin v1.0.8 h1:0GFOLzEbOyZABS3PhYfBIx2rNBACYcKty+XGkTgw1ow=\ngithub.com/aws/aws-sdk-go-v2/service/signin v1.0.8/go.mod h1:LXypKvk85AROkKhOG6/YEcHFPoX+prKTowKnVdcaIxE=\ngithub.com/aws/aws-sdk-go-v2/service/sso v1.30.13 h1:kiIDLZ005EcKomYYITtfsjn7dtOwHDOFy7IbPXKek2o=\ngithub.com/aws/aws-sdk-go-v2/service/sso v1.30.13/go.mod h1:2h/xGEowcW/g38g06g3KpRWDlT+OTfxxI0o1KqayAB8=\ngithub.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17 h1:jzKAXIlhZhJbnYwHbvUQZEB8KfgAEuG0dc08Bkda7NU=\ngithub.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17/go.mod h1:Al9fFsXjv4KfbzQHGe6V4NZSZQXecFcvaIF4e70FoRA=\ngithub.com/aws/aws-sdk-go-v2/service/sts v1.41.9 h1:Cng+OOwCHmFljXIxpEVXAGMnBia8MSU6Ch5i9PgBkcU=\ngithub.com/aws/aws-sdk-go-v2/service/sts v1.41.9/go.mod h1:LrlIndBDdjA/EeXeyNBle+gyCwTlizzW5ycgWnvIxkk=\ngithub.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng=\ngithub.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=\ngithub.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=\ngithub.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=\ngithub.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=\ngithub.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=\ngithub.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=\ngithub.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=\ngithub.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=\ngithub.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=\ngithub.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=\ngithub.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=\ngithub.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=\ngithub.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=\ngithub.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=\ngithub.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=\ngithub.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=\ngithub.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A=\ngithub.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw=\ngithub.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA=\ngithub.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=\ngithub.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=\ngithub.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=\ngithub.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=\ngithub.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=\ngithub.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=\ngithub.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM=\ngithub.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=\ngithub.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=\ngithub.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=\ngithub.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=\ngithub.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=\ngithub.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=\ngithub.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=\ngithub.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU=\ngithub.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=\ngithub.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=\ngithub.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=\ngithub.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=\ngithub.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=\ngithub.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=\ngithub.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=\ngithub.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=\ngithub.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=\ngithub.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=\ngithub.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=\ngithub.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=\ngithub.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=\ngithub.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=\ngithub.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=\ngithub.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=\ngithub.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=\ngithub.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=\ngithub.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=\ngithub.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=\ngithub.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=\ngithub.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=\ngithub.com/google/cel-go v0.27.0 h1:e7ih85+4qVrBuqQWTW4FKSqZYokVuc3HnhH5keboFTo=\ngithub.com/google/cel-go v0.27.0/go.mod h1:tTJ11FWqnhw5KKpnWpvW9CJC3Y9GK4EIS0WXnBbebzw=\ngithub.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=\ngithub.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=\ngithub.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=\ngithub.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/gorilla/feeds v1.2.0 h1:O6pBiXJ5JHhPvqy53NsjKOThq+dNFm8+DFrxBEdzSCc=\ngithub.com/gorilla/feeds v1.2.0/go.mod h1:WMib8uJP3BbY+X8Szd1rA5Pzhdfh+HCCAYT2z7Fza6Y=\ngithub.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs=\ngithub.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c=\ngithub.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=\ngithub.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=\ngithub.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=\ngithub.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=\ngithub.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E=\ngithub.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0=\ngithub.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=\ngithub.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=\ngithub.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=\ngithub.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=\ngithub.com/jackc/pgx/v5 v5.5.4 h1:Xp2aQS8uXButQdnCMWNmvx6UysWQQC+u1EoizjguY+8=\ngithub.com/jackc/pgx/v5 v5.5.4/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A=\ngithub.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=\ngithub.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=\ngithub.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=\ngithub.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=\ngithub.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=\ngithub.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=\ngithub.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=\ngithub.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=\ngithub.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\ngithub.com/labstack/echo/v5 v5.0.4 h1:ll3I/O8BifjMztj9dD1vx/peZQv8cR2CTUdQK6QxGGc=\ngithub.com/labstack/echo/v5 v5.0.4/go.mod h1:SyvlSdObGjRXeQfCCXW/sybkZdOOQZBmpKF0bvALaeo=\ngithub.com/lib/pq v1.11.2 h1:x6gxUeu39V0BHZiugWe8LXZYZ+Utk7hSJGThs8sdzfs=\ngithub.com/lib/pq v1.11.2/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=\ngithub.com/lithammer/shortuuid/v4 v4.2.0 h1:LMFOzVB3996a7b8aBuEXxqOBflbfPQAiVzkIcHO0h8c=\ngithub.com/lithammer/shortuuid/v4 v4.2.0/go.mod h1:D5noHZ2oFw/YaKCfGy0YxyE7M0wMbezmMjPdhyEFe6Y=\ngithub.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=\ngithub.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=\ngithub.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=\ngithub.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=\ngithub.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=\ngithub.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=\ngithub.com/mark3labs/mcp-go v0.45.0 h1:s0S8qR/9fWaQ3pHxz7pm1uQ0DrswoSnRIxKIjbiQtkc=\ngithub.com/mark3labs/mcp-go v0.45.0/go.mod h1:YnJfOL382MIWDx1kMY+2zsRHU/q78dBg9aFb8W6Thdw=\ngithub.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=\ngithub.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=\ngithub.com/mdelapenya/tlscert v0.2.0 h1:7H81W6Z/4weDvZBNOfQte5GpIMo0lGYEeWbkGp5LJHI=\ngithub.com/mdelapenya/tlscert v0.2.0/go.mod h1:O4njj3ELLnJjGdkN7M/vIVCpZ+Cf0L6muqOG4tLSl8o=\ngithub.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=\ngithub.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=\ngithub.com/moby/go-archive v0.2.0 h1:zg5QDUM2mi0JIM9fdQZWC7U8+2ZfixfTYoHL7rWUcP8=\ngithub.com/moby/go-archive v0.2.0/go.mod h1:mNeivT14o8xU+5q1YnNrkQVpK+dnNe/K6fHqnTg4qPU=\ngithub.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=\ngithub.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=\ngithub.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=\ngithub.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=\ngithub.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=\ngithub.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=\ngithub.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs=\ngithub.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=\ngithub.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=\ngithub.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=\ngithub.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=\ngithub.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=\ngithub.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=\ngithub.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=\ngithub.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=\ngithub.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=\ngithub.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=\ngithub.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=\ngithub.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=\ngithub.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=\ngithub.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=\ngithub.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=\ngithub.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=\ngithub.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=\ngithub.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=\ngithub.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=\ngithub.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=\ngithub.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=\ngithub.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=\ngithub.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=\ngithub.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=\ngithub.com/shirou/gopsutil/v4 v4.26.2 h1:X8i6sicvUFih4BmYIGT1m2wwgw2VG9YgrDTi7cIRGUI=\ngithub.com/shirou/gopsutil/v4 v4.26.2/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ=\ngithub.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=\ngithub.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=\ngithub.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=\ngithub.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=\ngithub.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=\ngithub.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=\ngithub.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=\ngithub.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=\ngithub.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=\ngithub.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=\ngithub.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=\ngithub.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=\ngithub.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=\ngithub.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=\ngithub.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=\ngithub.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=\ngithub.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=\ngithub.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngithub.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=\ngithub.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=\ngithub.com/testcontainers/testcontainers-go v0.41.0 h1:mfpsD0D36YgkxGj2LrIyxuwQ9i2wCKAD+ESsYM1wais=\ngithub.com/testcontainers/testcontainers-go v0.41.0/go.mod h1:pdFrEIfaPl24zmBjerWTTYaY0M6UHsqA1YSvsoU40MI=\ngithub.com/testcontainers/testcontainers-go/modules/mysql v0.41.0 h1:5rwejaJr5nIfw8NK99eKPX7O6k27lnSMklTj5DbYybM=\ngithub.com/testcontainers/testcontainers-go/modules/mysql v0.41.0/go.mod h1:iMO/aFWnbjYkqHw8VPsJB3rVTOD9hKDsUtV0PvzD0DA=\ngithub.com/testcontainers/testcontainers-go/modules/postgres v0.41.0 h1:AOtFXssrDlLm84A2sTTR/AhvJiYbrIuCO59d+Ro9Tb0=\ngithub.com/testcontainers/testcontainers-go/modules/postgres v0.41.0/go.mod h1:k2a09UKhgSp6vNpliIY0QSgm4Hi7GXVTzWvWgUemu/8=\ngithub.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA=\ngithub.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI=\ngithub.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw=\ngithub.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ=\ngithub.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=\ngithub.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=\ngithub.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=\ngithub.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=\ngithub.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE=\ngithub.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=\ngithub.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=\ngithub.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=\ngo.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=\ngo.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=\ngo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk=\ngo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8=\ngo.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c=\ngo.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE=\ngo.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U=\ngo.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE=\ngo.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.41.0 h1:inYW9ZhgqiDqh6BioM7DVHHzEGVq76Db5897WLGZ5Go=\ngo.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.41.0/go.mod h1:Izur+Wt8gClgMJqO/cZ8wdeeMryJ/xxiOVgFSSfpDTY=\ngo.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ=\ngo.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps=\ngo.opentelemetry.io/otel/sdk v1.41.0 h1:YPIEXKmiAwkGl3Gu1huk1aYWwtpRLeskpV+wPisxBp8=\ngo.opentelemetry.io/otel/sdk v1.41.0/go.mod h1:ahFdU0G5y8IxglBf0QBJXgSe7agzjE4GiTJ6HT9ud90=\ngo.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=\ngo.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=\ngo.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0=\ngo.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis=\ngo.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I=\ngo.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM=\ngo.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=\ngo.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=\ngolang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=\ngolang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=\ngolang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY=\ngolang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=\ngolang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=\ngolang.org/x/image v0.30.0 h1:jD5RhkmVAnjqaCUXfbGBrn3lpxbknfN9w2UhHHU+5B4=\ngolang.org/x/image v0.30.0/go.mod h1:SAEUTxCCMWSrJcCy/4HwavEsfZZJlYxeHLc6tTiAe/c=\ngolang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=\ngolang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=\ngolang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=\ngolang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=\ngolang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=\ngolang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=\ngolang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=\ngolang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=\ngolang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=\ngolang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=\ngolang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=\ngolang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=\ngolang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=\ngolang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=\ngolang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=\ngolang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=\ngolang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=\ngolang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=\ngonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=\ngoogle.golang.org/genproto v0.0.0-20260316180232-0b37fe3546d5 h1:JNfk58HZ8lfmXbYK2vx/UvsqIL59TzByCxPIX4TDmsE=\ngoogle.golang.org/genproto v0.0.0-20260316180232-0b37fe3546d5/go.mod h1:x5julN69+ED4PcFk/XWayw35O0lf/nGa4aNgODCmNmw=\ngoogle.golang.org/genproto/googleapis/api v0.0.0-20260316172706-e463d84ca32d h1:RdWlPmVySdTF0IBIZzvZJvSD0ZocPBNUsnE+uGBxj+4=\ngoogle.golang.org/genproto/googleapis/api v0.0.0-20260316172706-e463d84ca32d/go.mod h1:X2gu9Qwng7Nn009s/r3RUxqkzQNqOrAy79bluY7ojIg=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c h1:xgCzyF2LFIO/0X2UAoVRiXKU5Xg6VjToG4i2/ecSswk=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=\ngoogle.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU=\ngoogle.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=\ngoogle.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=\ngoogle.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=\ngotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=\nmodernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=\nmodernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=\nmodernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc=\nmodernc.org/ccgo/v4 v4.30.1/go.mod h1:bIOeI1JL54Utlxn+LwrFyjCx2n2RDiYEaJVSrgdrRfM=\nmodernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA=\nmodernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=\nmodernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=\nmodernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=\nmodernc.org/gc/v3 v3.1.1 h1:k8T3gkXWY9sEiytKhcgyiZ2L0DTyCQ/nvX+LoCljoRE=\nmodernc.org/gc/v3 v3.1.1/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=\nmodernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=\nmodernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=\nmodernc.org/libc v1.67.6 h1:eVOQvpModVLKOdT+LvBPjdQqfrZq+pC39BygcT+E7OI=\nmodernc.org/libc v1.67.6/go.mod h1:JAhxUVlolfYDErnwiqaLvUqc8nfb2r6S6slAgZOnaiE=\nmodernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=\nmodernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=\nmodernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=\nmodernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=\nmodernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=\nmodernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=\nmodernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=\nmodernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=\nmodernc.org/sqlite v1.46.1 h1:eFJ2ShBLIEnUWlLy12raN0Z1plqmFX9Qe3rjQTKt6sU=\nmodernc.org/sqlite v1.46.1/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA=\nmodernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=\nmodernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=\nmodernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=\nmodernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=\n"
  },
  {
    "path": "internal/base/resource_name.go",
    "content": "package base\n\nimport \"regexp\"\n\nvar (\n\tUIDMatcher = regexp.MustCompile(\"^[a-zA-Z0-9]([a-zA-Z0-9-]{0,30}[a-zA-Z0-9])?$\")\n)\n"
  },
  {
    "path": "internal/base/resource_name_test.go",
    "content": "package base\n\nimport (\n\t\"testing\"\n)\n\nfunc TestUIDMatcher(t *testing.T) {\n\ttests := []struct {\n\t\tinput    string\n\t\texpected bool\n\t}{\n\t\t{\"\", false},\n\t\t{\"-abc123\", false},\n\t\t{\"012345678901234567890123456789\", true},\n\t\t{\"1abc-123\", true},\n\t\t{\"A123B456C789\", true},\n\t\t{\"a\", true},\n\t\t{\"ab\", true},\n\t\t{\"a*b&c\", false},\n\t\t{\"a--b\", true},\n\t\t{\"a-1b-2c\", true},\n\t\t{\"a1234567890123456789012345678901\", true},\n\t\t{\"abc123\", true},\n\t\t{\"abc123-\", false},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.input, func(*testing.T) {\n\t\t\tresult := UIDMatcher.MatchString(test.input)\n\t\t\tif result != test.expected {\n\t\t\t\tt.Errorf(\"For input '%s', expected %v but got %v\", test.input, test.expected, result)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/profile/profile.go",
    "content": "package profile\n\nimport (\n\t\"fmt\"\n\t\"log/slog\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strings\"\n\n\t\"github.com/pkg/errors\"\n)\n\n// Profile is the configuration to start main server.\ntype Profile struct {\n\t// Demo indicates if the server is in demo mode\n\tDemo bool\n\t// Addr is the binding address for server\n\tAddr string\n\t// Port is the binding port for server\n\tPort int\n\t// UNIXSock is the IPC binding path. Overrides Addr and Port\n\tUNIXSock string\n\t// Data is the data directory\n\tData string\n\t// DSN points to where memos stores its own data\n\tDSN string\n\t// Driver is the database driver\n\t// sqlite, mysql\n\tDriver string\n\t// Version is the current version of server\n\tVersion string\n\t// InstanceURL is the url of your memos instance.\n\tInstanceURL string\n}\n\nfunc checkDataDir(dataDir string) (string, error) {\n\t// Convert to absolute path if relative path is supplied.\n\tif !filepath.IsAbs(dataDir) {\n\t\t// Use current working directory, not the binary's directory\n\t\t// This ensures we use the actual working directory where the process runs\n\t\tabsDir, err := filepath.Abs(dataDir)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\tdataDir = absDir\n\t}\n\n\t// Trim trailing \\ or / in case user supplies\n\tdataDir = strings.TrimRight(dataDir, \"\\\\/\")\n\tif _, err := os.Stat(dataDir); err != nil {\n\t\treturn \"\", errors.Wrapf(err, \"unable to access data folder %s\", dataDir)\n\t}\n\treturn dataDir, nil\n}\n\nfunc (p *Profile) Validate() error {\n\t// Set default data directory if not specified\n\tif p.Data == \"\" {\n\t\tif runtime.GOOS == \"windows\" {\n\t\t\tp.Data = filepath.Join(os.Getenv(\"ProgramData\"), \"memos\")\n\t\t} else {\n\t\t\t// On Linux/macOS, check if /var/opt/memos exists and is writable (Docker scenario)\n\t\t\tif info, err := os.Stat(\"/var/opt/memos\"); err == nil && info.IsDir() {\n\t\t\t\t// Check if we can write to this directory\n\t\t\t\ttestFile := filepath.Join(\"/var/opt/memos\", \".write-test\")\n\t\t\t\tif err := os.WriteFile(testFile, []byte(\"test\"), 0600); err == nil {\n\t\t\t\t\tos.Remove(testFile)\n\t\t\t\t\tp.Data = \"/var/opt/memos\"\n\t\t\t\t} else {\n\t\t\t\t\t// /var/opt/memos exists but is not writable, use current directory\n\t\t\t\t\tslog.Warn(\"/var/opt/memos is not writable, using current directory\")\n\t\t\t\t\tp.Data = \".\"\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// /var/opt/memos doesn't exist, use current directory (local development)\n\t\t\t\tp.Data = \".\"\n\t\t\t}\n\t\t}\n\t}\n\n\t// Create data directory if it doesn't exist\n\tif _, err := os.Stat(p.Data); os.IsNotExist(err) {\n\t\tif err := os.MkdirAll(p.Data, 0770); err != nil {\n\t\t\tslog.Error(\"failed to create data directory\", slog.String(\"data\", p.Data), slog.String(\"error\", err.Error()))\n\t\t\treturn err\n\t\t}\n\t}\n\n\tdataDir, err := checkDataDir(p.Data)\n\tif err != nil {\n\t\tslog.Error(\"failed to check dsn\", slog.String(\"data\", dataDir), slog.String(\"error\", err.Error()))\n\t\treturn err\n\t}\n\n\tp.Data = dataDir\n\tif p.Driver == \"sqlite\" && p.DSN == \"\" {\n\t\tmode := \"prod\"\n\t\tif p.Demo {\n\t\t\tmode = \"demo\"\n\t\t}\n\t\tdbFile := fmt.Sprintf(\"memos_%s.db\", mode)\n\t\tp.DSN = filepath.Join(dataDir, dbFile)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/util/util.go",
    "content": "package util //nolint:revive // util namespace is intentional for shared helpers\n\nimport (\n\t\"crypto/rand\"\n\t\"math/big\"\n\t\"net/mail\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/google/uuid\"\n)\n\n// ConvertStringToInt32 converts a string to int32.\nfunc ConvertStringToInt32(src string) (int32, error) {\n\tparsed, err := strconv.ParseInt(src, 10, 32)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\treturn int32(parsed), nil\n}\n\n// HasPrefixes returns true if the string s has any of the given prefixes.\nfunc HasPrefixes(src string, prefixes ...string) bool {\n\tfor _, prefix := range prefixes {\n\t\tif strings.HasPrefix(src, prefix) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// ValidateEmail validates the email.\nfunc ValidateEmail(email string) bool {\n\tif _, err := mail.ParseAddress(email); err != nil {\n\t\treturn false\n\t}\n\treturn true\n}\n\nfunc GenUUID() string {\n\treturn uuid.New().String()\n}\n\nvar letters = []rune(\"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ\")\n\n// RandomString returns a random string with length n.\nfunc RandomString(n int) (string, error) {\n\tvar sb strings.Builder\n\tsb.Grow(n)\n\tfor i := 0; i < n; i++ {\n\t\t// The reason for using crypto/rand instead of math/rand is that\n\t\t// the former relies on hardware to generate random numbers and\n\t\t// thus has a stronger source of random numbers.\n\t\trandNum, err := rand.Int(rand.Reader, big.NewInt(int64(len(letters))))\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\tif _, err := sb.WriteRune(letters[randNum.Uint64()]); err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t}\n\treturn sb.String(), nil\n}\n\n// ReplaceString replaces all occurrences of old in slice with new.\nfunc ReplaceString(slice []string, old, new string) []string {\n\tfor i, s := range slice {\n\t\tif s == old {\n\t\t\tslice[i] = new\n\t\t}\n\t}\n\treturn slice\n}\n"
  },
  {
    "path": "internal/util/util_test.go",
    "content": "package util //nolint:revive // util is an appropriate package name for utility functions\n\nimport (\n\t\"testing\"\n)\n\nfunc TestValidateEmail(t *testing.T) {\n\ttests := []struct {\n\t\temail string\n\t\twant  bool\n\t}{\n\t\t{\n\t\t\temail: \"t@gmail.com\",\n\t\t\twant:  true,\n\t\t},\n\t\t{\n\t\t\temail: \"@usememos.com\",\n\t\t\twant:  false,\n\t\t},\n\t\t{\n\t\t\temail: \"1@gmail\",\n\t\t\twant:  true,\n\t\t},\n\t}\n\tfor _, test := range tests {\n\t\tresult := ValidateEmail(test.email)\n\t\tif result != test.want {\n\t\t\tt.Errorf(\"Validate Email %s: got result %v, want %v.\", test.email, result, test.want)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "internal/version/version.go",
    "content": "package version\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"golang.org/x/mod/semver\"\n)\n\n// Version is the service current released version.\n// Semantic versioning: https://semver.org/\nvar Version = \"0.27.0\"\n\nfunc GetCurrentVersion() string {\n\treturn Version\n}\n\n// GetMinorVersion extracts the minor version (e.g., \"0.25\") from a full version string (e.g., \"0.25.1\").\n// Returns the minor version string or empty string if the version format is invalid.\n// Version format should be \"major.minor.patch\" (e.g., \"0.25.1\").\nfunc GetMinorVersion(version string) string {\n\tversionList := strings.Split(version, \".\")\n\tif len(versionList) < 2 {\n\t\treturn \"\"\n\t}\n\t// Return major.minor only (first two components)\n\treturn versionList[0] + \".\" + versionList[1]\n}\n\n// IsVersionGreaterOrEqualThan returns true if version is greater than or equal to target.\nfunc IsVersionGreaterOrEqualThan(version, target string) bool {\n\treturn semver.Compare(fmt.Sprintf(\"v%s\", version), fmt.Sprintf(\"v%s\", target)) > -1\n}\n\n// IsVersionGreaterThan returns true if version is greater than target.\nfunc IsVersionGreaterThan(version, target string) bool {\n\treturn semver.Compare(fmt.Sprintf(\"v%s\", version), fmt.Sprintf(\"v%s\", target)) > 0\n}\n\ntype SortVersion []string\n\nfunc (s SortVersion) Len() int {\n\treturn len(s)\n}\n\nfunc (s SortVersion) Swap(i, j int) {\n\ts[i], s[j] = s[j], s[i]\n}\n\nfunc (s SortVersion) Less(i, j int) bool {\n\tv1 := fmt.Sprintf(\"v%s\", s[i])\n\tv2 := fmt.Sprintf(\"v%s\", s[j])\n\treturn semver.Compare(v1, v2) == -1\n}\n"
  },
  {
    "path": "internal/version/version_test.go",
    "content": "package version\n\nimport (\n\t\"slices\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"golang.org/x/mod/semver\"\n)\n\nfunc TestIsVersionGreaterOrEqualThan(t *testing.T) {\n\ttests := []struct {\n\t\tversion string\n\t\ttarget  string\n\t\twant    bool\n\t}{\n\t\t{\n\t\t\tversion: \"0.9.1\",\n\t\t\ttarget:  \"0.9.1\",\n\t\t\twant:    true,\n\t\t},\n\t\t{\n\t\t\tversion: \"0.10.0\",\n\t\t\ttarget:  \"0.9.1\",\n\t\t\twant:    true,\n\t\t},\n\t\t{\n\t\t\tversion: \"0.9.0\",\n\t\t\ttarget:  \"0.9.1\",\n\t\t\twant:    false,\n\t\t},\n\t}\n\tfor _, test := range tests {\n\t\tresult := IsVersionGreaterOrEqualThan(test.version, test.target)\n\t\tif result != test.want {\n\t\t\tt.Errorf(\"got result %v, want %v.\", result, test.want)\n\t\t}\n\t}\n}\n\nfunc TestIsVersionGreaterThan(t *testing.T) {\n\ttests := []struct {\n\t\tversion string\n\t\ttarget  string\n\t\twant    bool\n\t}{\n\t\t{\n\t\t\tversion: \"0.9.1\",\n\t\t\ttarget:  \"0.9.1\",\n\t\t\twant:    false,\n\t\t},\n\t\t{\n\t\t\tversion: \"0.10.0\",\n\t\t\ttarget:  \"0.8.0\",\n\t\t\twant:    true,\n\t\t},\n\t\t{\n\t\t\tversion: \"0.23\",\n\t\t\ttarget:  \"0.22\",\n\t\t\twant:    true,\n\t\t},\n\t\t{\n\t\t\tversion: \"0.8.0\",\n\t\t\ttarget:  \"0.10.0\",\n\t\t\twant:    false,\n\t\t},\n\t\t{\n\t\t\tversion: \"0.9.0\",\n\t\t\ttarget:  \"0.9.1\",\n\t\t\twant:    false,\n\t\t},\n\t\t{\n\t\t\tversion: \"0.22\",\n\t\t\ttarget:  \"0.22\",\n\t\t\twant:    false,\n\t\t},\n\t}\n\tfor _, test := range tests {\n\t\tresult := IsVersionGreaterThan(test.version, test.target)\n\t\tif result != test.want {\n\t\t\tt.Errorf(\"got result %v, want %v.\", result, test.want)\n\t\t}\n\t}\n}\n\nfunc TestSortVersion(t *testing.T) {\n\ttests := []struct {\n\t\tversionList []string\n\t\twant        []string\n\t}{\n\t\t{\n\t\t\tversionList: []string{\"0.9.1\", \"0.10.0\", \"0.8.0\"},\n\t\t\twant:        []string{\"0.8.0\", \"0.9.1\", \"0.10.0\"},\n\t\t},\n\t\t{\n\t\t\tversionList: []string{\"1.9.1\", \"0.9.1\", \"0.10.0\", \"0.8.0\"},\n\t\t\twant:        []string{\"0.8.0\", \"0.9.1\", \"0.10.0\", \"1.9.1\"},\n\t\t},\n\t}\n\tfor _, test := range tests {\n\t\tslices.SortFunc(test.versionList, func(a, b string) int {\n\t\t\treturn semver.Compare(\"v\"+a, \"v\"+b)\n\t\t})\n\t\tassert.Equal(t, test.versionList, test.want)\n\t}\n}\n"
  },
  {
    "path": "plugin/cron/README.md",
    "content": "Fork from https://github.com/robfig/cron\n"
  },
  {
    "path": "plugin/cron/chain.go",
    "content": "package cron\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"runtime\"\n\t\"sync\"\n\t\"time\"\n)\n\n// JobWrapper decorates the given Job with some behavior.\ntype JobWrapper func(Job) Job\n\n// Chain is a sequence of JobWrappers that decorates submitted jobs with\n// cross-cutting behaviors like logging or synchronization.\ntype Chain struct {\n\twrappers []JobWrapper\n}\n\n// NewChain returns a Chain consisting of the given JobWrappers.\nfunc NewChain(c ...JobWrapper) Chain {\n\treturn Chain{c}\n}\n\n// Then decorates the given job with all JobWrappers in the chain.\n//\n// This:\n//\n//\tNewChain(m1, m2, m3).Then(job)\n//\n// is equivalent to:\n//\n//\tm1(m2(m3(job)))\nfunc (c Chain) Then(j Job) Job {\n\tfor i := range c.wrappers {\n\t\tj = c.wrappers[len(c.wrappers)-i-1](j)\n\t}\n\treturn j\n}\n\n// Recover panics in wrapped jobs and log them with the provided logger.\nfunc Recover(logger Logger) JobWrapper {\n\treturn func(j Job) Job {\n\t\treturn FuncJob(func() {\n\t\t\tdefer func() {\n\t\t\t\tif r := recover(); r != nil {\n\t\t\t\t\tconst size = 64 << 10\n\t\t\t\t\tbuf := make([]byte, size)\n\t\t\t\t\tbuf = buf[:runtime.Stack(buf, false)]\n\t\t\t\t\terr, ok := r.(error)\n\t\t\t\t\tif !ok {\n\t\t\t\t\t\terr = errors.New(\"panic: \" + fmt.Sprint(r))\n\t\t\t\t\t}\n\t\t\t\t\tlogger.Error(err, \"panic\", \"stack\", \"...\\n\"+string(buf))\n\t\t\t\t}\n\t\t\t}()\n\t\t\tj.Run()\n\t\t})\n\t}\n}\n\n// DelayIfStillRunning serializes jobs, delaying subsequent runs until the\n// previous one is complete. Jobs running after a delay of more than a minute\n// have the delay logged at Info.\nfunc DelayIfStillRunning(logger Logger) JobWrapper {\n\treturn func(j Job) Job {\n\t\tvar mu sync.Mutex\n\t\treturn FuncJob(func() {\n\t\t\tstart := time.Now()\n\t\t\tmu.Lock()\n\t\t\tdefer mu.Unlock()\n\t\t\tif dur := time.Since(start); dur > time.Minute {\n\t\t\t\tlogger.Info(\"delay\", \"duration\", dur)\n\t\t\t}\n\t\t\tj.Run()\n\t\t})\n\t}\n}\n\n// SkipIfStillRunning skips an invocation of the Job if a previous invocation is\n// still running. It logs skips to the given logger at Info level.\nfunc SkipIfStillRunning(logger Logger) JobWrapper {\n\treturn func(j Job) Job {\n\t\tvar ch = make(chan struct{}, 1)\n\t\tch <- struct{}{}\n\t\treturn FuncJob(func() {\n\t\t\tselect {\n\t\t\tcase v := <-ch:\n\t\t\t\tdefer func() { ch <- v }()\n\t\t\t\tj.Run()\n\t\t\tdefault:\n\t\t\t\tlogger.Info(\"skip\")\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "plugin/cron/chain_test.go",
    "content": "//nolint:all\npackage cron\n\nimport (\n\t\"io\"\n\t\"log\"\n\t\"reflect\"\n\t\"sync\"\n\t\"testing\"\n\t\"time\"\n)\n\nfunc waitFor(t *testing.T, timeout time.Duration, fn func() bool) {\n\tt.Helper()\n\n\tdeadline := time.Now().Add(timeout)\n\tfor time.Now().Before(deadline) {\n\t\tif fn() {\n\t\t\treturn\n\t\t}\n\t\ttime.Sleep(time.Millisecond)\n\t}\n\n\tt.Fatal(\"condition not met before timeout\")\n}\n\nfunc appendingJob(slice *[]int, value int) Job {\n\tvar m sync.Mutex\n\treturn FuncJob(func() {\n\t\tm.Lock()\n\t\t*slice = append(*slice, value)\n\t\tm.Unlock()\n\t})\n}\n\nfunc appendingWrapper(slice *[]int, value int) JobWrapper {\n\treturn func(j Job) Job {\n\t\treturn FuncJob(func() {\n\t\t\tappendingJob(slice, value).Run()\n\t\t\tj.Run()\n\t\t})\n\t}\n}\n\nfunc TestChain(t *testing.T) {\n\tvar nums []int\n\tvar (\n\t\tappend1 = appendingWrapper(&nums, 1)\n\t\tappend2 = appendingWrapper(&nums, 2)\n\t\tappend3 = appendingWrapper(&nums, 3)\n\t\tappend4 = appendingJob(&nums, 4)\n\t)\n\tNewChain(append1, append2, append3).Then(append4).Run()\n\tif !reflect.DeepEqual(nums, []int{1, 2, 3, 4}) {\n\t\tt.Error(\"unexpected order of calls:\", nums)\n\t}\n}\n\nfunc TestChainRecover(t *testing.T) {\n\tpanickingJob := FuncJob(func() {\n\t\tpanic(\"panickingJob panics\")\n\t})\n\n\tt.Run(\"panic exits job by default\", func(*testing.T) {\n\t\tdefer func() {\n\t\t\tif err := recover(); err == nil {\n\t\t\t\tt.Errorf(\"panic expected, but none received\")\n\t\t\t}\n\t\t}()\n\t\tNewChain().Then(panickingJob).\n\t\t\tRun()\n\t})\n\n\tt.Run(\"Recovering JobWrapper recovers\", func(*testing.T) {\n\t\tNewChain(Recover(PrintfLogger(log.New(io.Discard, \"\", 0)))).\n\t\t\tThen(panickingJob).\n\t\t\tRun()\n\t})\n\n\tt.Run(\"composed with the *IfStillRunning wrappers\", func(*testing.T) {\n\t\tNewChain(Recover(PrintfLogger(log.New(io.Discard, \"\", 0)))).\n\t\t\tThen(panickingJob).\n\t\t\tRun()\n\t})\n}\n\ntype countJob struct {\n\tm       sync.Mutex\n\tstarted int\n\tdone    int\n\tdelay   time.Duration\n}\n\nfunc (j *countJob) Run() {\n\tj.m.Lock()\n\tj.started++\n\tj.m.Unlock()\n\ttime.Sleep(j.delay)\n\tj.m.Lock()\n\tj.done++\n\tj.m.Unlock()\n}\n\nfunc (j *countJob) Started() int {\n\tdefer j.m.Unlock()\n\tj.m.Lock()\n\treturn j.started\n}\n\nfunc (j *countJob) Done() int {\n\tdefer j.m.Unlock()\n\tj.m.Lock()\n\treturn j.done\n}\n\nfunc TestChainDelayIfStillRunning(t *testing.T) {\n\tt.Run(\"runs immediately\", func(*testing.T) {\n\t\tvar j countJob\n\t\twrappedJob := NewChain(DelayIfStillRunning(DiscardLogger)).Then(&j)\n\t\tgo wrappedJob.Run()\n\n\t\twaitFor(t, 100*time.Millisecond, func() bool { return j.Done() == 1 })\n\t\tif c := j.Done(); c != 1 {\n\t\t\tt.Errorf(\"expected job run once, immediately, got %d\", c)\n\t\t}\n\t})\n\n\tt.Run(\"second run immediate if first done\", func(*testing.T) {\n\t\tvar j countJob\n\t\twrappedJob := NewChain(DelayIfStillRunning(DiscardLogger)).Then(&j)\n\t\tgo func() {\n\t\t\tgo wrappedJob.Run()\n\t\t\ttime.Sleep(time.Millisecond)\n\t\t\tgo wrappedJob.Run()\n\t\t}()\n\n\t\twaitFor(t, 100*time.Millisecond, func() bool { return j.Done() == 2 })\n\t\tif c := j.Done(); c != 2 {\n\t\t\tt.Errorf(\"expected job run twice, immediately, got %d\", c)\n\t\t}\n\t})\n\n\tt.Run(\"second run delayed if first not done\", func(*testing.T) {\n\t\tvar j countJob\n\t\tj.delay = 10 * time.Millisecond\n\t\twrappedJob := NewChain(DelayIfStillRunning(DiscardLogger)).Then(&j)\n\t\tgo func() {\n\t\t\tgo wrappedJob.Run()\n\t\t\ttime.Sleep(time.Millisecond)\n\t\t\tgo wrappedJob.Run()\n\t\t}()\n\n\t\twaitFor(t, 100*time.Millisecond, func() bool { return j.Started() == 1 })\n\t\tstarted, done := j.Started(), j.Done()\n\t\tif done != 0 {\n\t\t\tt.Error(\"expected first job started, but not finished, got\", started, done)\n\t\t}\n\n\t\twaitFor(t, 200*time.Millisecond, func() bool { return j.Done() == 2 })\n\t\tstarted, done = j.Started(), j.Done()\n\t\tif started != 2 || done != 2 {\n\t\t\tt.Error(\"expected both jobs done, got\", started, done)\n\t\t}\n\t})\n}\n\nfunc TestChainSkipIfStillRunning(t *testing.T) {\n\tt.Run(\"runs immediately\", func(*testing.T) {\n\t\tvar j countJob\n\t\twrappedJob := NewChain(SkipIfStillRunning(DiscardLogger)).Then(&j)\n\t\tgo wrappedJob.Run()\n\t\ttime.Sleep(2 * time.Millisecond) // Give the job 2ms to complete.\n\t\tif c := j.Done(); c != 1 {\n\t\t\tt.Errorf(\"expected job run once, immediately, got %d\", c)\n\t\t}\n\t})\n\n\tt.Run(\"second run immediate if first done\", func(*testing.T) {\n\t\tvar j countJob\n\t\twrappedJob := NewChain(SkipIfStillRunning(DiscardLogger)).Then(&j)\n\t\tgo func() {\n\t\t\tgo wrappedJob.Run()\n\t\t\ttime.Sleep(time.Millisecond)\n\t\t\tgo wrappedJob.Run()\n\t\t}()\n\t\ttime.Sleep(3 * time.Millisecond) // Give both jobs 3ms to complete.\n\t\tif c := j.Done(); c != 2 {\n\t\t\tt.Errorf(\"expected job run twice, immediately, got %d\", c)\n\t\t}\n\t})\n\n\tt.Run(\"second run skipped if first not done\", func(*testing.T) {\n\t\tvar j countJob\n\t\tj.delay = 10 * time.Millisecond\n\t\twrappedJob := NewChain(SkipIfStillRunning(DiscardLogger)).Then(&j)\n\t\tgo func() {\n\t\t\tgo wrappedJob.Run()\n\t\t\ttime.Sleep(time.Millisecond)\n\t\t\tgo wrappedJob.Run()\n\t\t}()\n\n\t\t// After 5ms, the first job is still in progress, and the second job was\n\t\t// already skipped.\n\t\ttime.Sleep(5 * time.Millisecond)\n\t\tstarted, done := j.Started(), j.Done()\n\t\tif started != 1 || done != 0 {\n\t\t\tt.Error(\"expected first job started, but not finished, got\", started, done)\n\t\t}\n\n\t\t// Verify that the first job completes and second does not run.\n\t\ttime.Sleep(25 * time.Millisecond)\n\t\tstarted, done = j.Started(), j.Done()\n\t\tif started != 1 || done != 1 {\n\t\t\tt.Error(\"expected second job skipped, got\", started, done)\n\t\t}\n\t})\n\n\tt.Run(\"skip 10 jobs on rapid fire\", func(*testing.T) {\n\t\tvar j countJob\n\t\tj.delay = 10 * time.Millisecond\n\t\twrappedJob := NewChain(SkipIfStillRunning(DiscardLogger)).Then(&j)\n\t\tfor i := 0; i < 11; i++ {\n\t\t\tgo wrappedJob.Run()\n\t\t}\n\t\ttime.Sleep(200 * time.Millisecond)\n\t\tdone := j.Done()\n\t\tif done != 1 {\n\t\t\tt.Error(\"expected 1 jobs executed, 10 jobs dropped, got\", done)\n\t\t}\n\t})\n\n\tt.Run(\"different jobs independent\", func(*testing.T) {\n\t\tvar j1, j2 countJob\n\t\tj1.delay = 10 * time.Millisecond\n\t\tj2.delay = 10 * time.Millisecond\n\t\tchain := NewChain(SkipIfStillRunning(DiscardLogger))\n\t\twrappedJob1 := chain.Then(&j1)\n\t\twrappedJob2 := chain.Then(&j2)\n\t\tfor i := 0; i < 11; i++ {\n\t\t\tgo wrappedJob1.Run()\n\t\t\tgo wrappedJob2.Run()\n\t\t}\n\t\ttime.Sleep(100 * time.Millisecond)\n\t\tvar (\n\t\t\tdone1 = j1.Done()\n\t\t\tdone2 = j2.Done()\n\t\t)\n\t\tif done1 != 1 || done2 != 1 {\n\t\t\tt.Error(\"expected both jobs executed once, got\", done1, \"and\", done2)\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "plugin/cron/constantdelay.go",
    "content": "package cron\n\nimport \"time\"\n\n// ConstantDelaySchedule represents a simple recurring duty cycle, e.g. \"Every 5 minutes\".\n// It does not support jobs more frequent than once a second.\ntype ConstantDelaySchedule struct {\n\tDelay time.Duration\n}\n\n// Every returns a crontab Schedule that activates once every duration.\n// Delays of less than a second are not supported (will round up to 1 second).\n// Any fields less than a Second are truncated.\nfunc Every(duration time.Duration) ConstantDelaySchedule {\n\tif duration < time.Second {\n\t\tduration = time.Second\n\t}\n\treturn ConstantDelaySchedule{\n\t\tDelay: duration - time.Duration(duration.Nanoseconds())%time.Second,\n\t}\n}\n\n// Next returns the next time this should be run.\n// This rounds so that the next activation time will be on the second.\nfunc (schedule ConstantDelaySchedule) Next(t time.Time) time.Time {\n\treturn t.Add(schedule.Delay - time.Duration(t.Nanosecond())*time.Nanosecond)\n}\n"
  },
  {
    "path": "plugin/cron/constantdelay_test.go",
    "content": "//nolint:all\npackage cron\n\nimport (\n\t\"testing\"\n\t\"time\"\n)\n\nfunc TestConstantDelayNext(t *testing.T) {\n\ttests := []struct {\n\t\ttime     string\n\t\tdelay    time.Duration\n\t\texpected string\n\t}{\n\t\t// Simple cases\n\t\t{\"Mon Jul 9 14:45 2012\", 15*time.Minute + 50*time.Nanosecond, \"Mon Jul 9 15:00 2012\"},\n\t\t{\"Mon Jul 9 14:59 2012\", 15 * time.Minute, \"Mon Jul 9 15:14 2012\"},\n\t\t{\"Mon Jul 9 14:59:59 2012\", 15 * time.Minute, \"Mon Jul 9 15:14:59 2012\"},\n\n\t\t// Wrap around hours\n\t\t{\"Mon Jul 9 15:45 2012\", 35 * time.Minute, \"Mon Jul 9 16:20 2012\"},\n\n\t\t// Wrap around days\n\t\t{\"Mon Jul 9 23:46 2012\", 14 * time.Minute, \"Tue Jul 10 00:00 2012\"},\n\t\t{\"Mon Jul 9 23:45 2012\", 35 * time.Minute, \"Tue Jul 10 00:20 2012\"},\n\t\t{\"Mon Jul 9 23:35:51 2012\", 44*time.Minute + 24*time.Second, \"Tue Jul 10 00:20:15 2012\"},\n\t\t{\"Mon Jul 9 23:35:51 2012\", 25*time.Hour + 44*time.Minute + 24*time.Second, \"Thu Jul 11 01:20:15 2012\"},\n\n\t\t// Wrap around months\n\t\t{\"Mon Jul 9 23:35 2012\", 91*24*time.Hour + 25*time.Minute, \"Thu Oct 9 00:00 2012\"},\n\n\t\t// Wrap around minute, hour, day, month, and year\n\t\t{\"Mon Dec 31 23:59:45 2012\", 15 * time.Second, \"Tue Jan 1 00:00:00 2013\"},\n\n\t\t// Round to nearest second on the delay\n\t\t{\"Mon Jul 9 14:45 2012\", 15*time.Minute + 50*time.Nanosecond, \"Mon Jul 9 15:00 2012\"},\n\n\t\t// Round up to 1 second if the duration is less.\n\t\t{\"Mon Jul 9 14:45:00 2012\", 15 * time.Millisecond, \"Mon Jul 9 14:45:01 2012\"},\n\n\t\t// Round to nearest second when calculating the next time.\n\t\t{\"Mon Jul 9 14:45:00.005 2012\", 15 * time.Minute, \"Mon Jul 9 15:00 2012\"},\n\n\t\t// Round to nearest second for both.\n\t\t{\"Mon Jul 9 14:45:00.005 2012\", 15*time.Minute + 50*time.Nanosecond, \"Mon Jul 9 15:00 2012\"},\n\t}\n\n\tfor _, c := range tests {\n\t\tactual := Every(c.delay).Next(getTime(c.time))\n\t\texpected := getTime(c.expected)\n\t\tif actual != expected {\n\t\t\tt.Errorf(\"%s, \\\"%s\\\": (expected) %v != %v (actual)\", c.time, c.delay, expected, actual)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "plugin/cron/cron.go",
    "content": "package cron\n\nimport (\n\t\"context\"\n\t\"slices\"\n\t\"sync\"\n\t\"time\"\n)\n\n// Cron keeps track of any number of entries, invoking the associated func as\n// specified by the schedule. It may be started, stopped, and the entries may\n// be inspected while running.\ntype Cron struct {\n\tentries   []*Entry\n\tchain     Chain\n\tstop      chan struct{}\n\tadd       chan *Entry\n\tremove    chan EntryID\n\tsnapshot  chan chan []Entry\n\trunning   bool\n\tlogger    Logger\n\trunningMu sync.Mutex\n\tlocation  *time.Location\n\tparser    ScheduleParser\n\tnextID    EntryID\n\tjobWaiter sync.WaitGroup\n}\n\n// ScheduleParser is an interface for schedule spec parsers that return a Schedule.\ntype ScheduleParser interface {\n\tParse(spec string) (Schedule, error)\n}\n\n// Job is an interface for submitted cron jobs.\ntype Job interface {\n\tRun()\n}\n\n// Schedule describes a job's duty cycle.\ntype Schedule interface {\n\t// Next returns the next activation time, later than the given time.\n\t// Next is invoked initially, and then each time the job is run.\n\tNext(time.Time) time.Time\n}\n\n// EntryID identifies an entry within a Cron instance.\ntype EntryID int\n\n// Entry consists of a schedule and the func to execute on that schedule.\ntype Entry struct {\n\t// ID is the cron-assigned ID of this entry, which may be used to look up a\n\t// snapshot or remove it.\n\tID EntryID\n\n\t// Schedule on which this job should be run.\n\tSchedule Schedule\n\n\t// Next time the job will run, or the zero time if Cron has not been\n\t// started or this entry's schedule is unsatisfiable\n\tNext time.Time\n\n\t// Prev is the last time this job was run, or the zero time if never.\n\tPrev time.Time\n\n\t// WrappedJob is the thing to run when the Schedule is activated.\n\tWrappedJob Job\n\n\t// Job is the thing that was submitted to cron.\n\t// It is kept around so that user code that needs to get at the job later,\n\t// e.g. via Entries() can do so.\n\tJob Job\n}\n\n// Valid returns true if this is not the zero entry.\nfunc (e Entry) Valid() bool { return e.ID != 0 }\n\n// New returns a new Cron job runner, modified by the given options.\n//\n// Available Settings\n//\n//\tTime Zone\n//\t  Description: The time zone in which schedules are interpreted\n//\t  Default:     time.Local\n//\n//\tParser\n//\t  Description: Parser converts cron spec strings into cron.Schedules.\n//\t  Default:     Accepts this spec: https://en.wikipedia.org/wiki/Cron\n//\n//\tChain\n//\t  Description: Wrap submitted jobs to customize behavior.\n//\t  Default:     A chain that recovers panics and logs them to stderr.\n//\n// See \"cron.With*\" to modify the default behavior.\nfunc New(opts ...Option) *Cron {\n\tc := &Cron{\n\t\tentries:   nil,\n\t\tchain:     NewChain(),\n\t\tadd:       make(chan *Entry),\n\t\tstop:      make(chan struct{}),\n\t\tsnapshot:  make(chan chan []Entry),\n\t\tremove:    make(chan EntryID),\n\t\trunning:   false,\n\t\trunningMu: sync.Mutex{},\n\t\tlogger:    DefaultLogger,\n\t\tlocation:  time.Local,\n\t\tparser:    standardParser,\n\t}\n\tfor _, opt := range opts {\n\t\topt(c)\n\t}\n\treturn c\n}\n\n// FuncJob is a wrapper that turns a func() into a cron.Job.\ntype FuncJob func()\n\nfunc (f FuncJob) Run() { f() }\n\n// AddFunc adds a func to the Cron to be run on the given schedule.\n// The spec is parsed using the time zone of this Cron instance as the default.\n// An opaque ID is returned that can be used to later remove it.\nfunc (c *Cron) AddFunc(spec string, cmd func()) (EntryID, error) {\n\treturn c.AddJob(spec, FuncJob(cmd))\n}\n\n// AddJob adds a Job to the Cron to be run on the given schedule.\n// The spec is parsed using the time zone of this Cron instance as the default.\n// An opaque ID is returned that can be used to later remove it.\nfunc (c *Cron) AddJob(spec string, cmd Job) (EntryID, error) {\n\tschedule, err := c.parser.Parse(spec)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\treturn c.Schedule(schedule, cmd), nil\n}\n\n// Schedule adds a Job to the Cron to be run on the given schedule.\n// The job is wrapped with the configured Chain.\nfunc (c *Cron) Schedule(schedule Schedule, cmd Job) EntryID {\n\tc.runningMu.Lock()\n\tdefer c.runningMu.Unlock()\n\tc.nextID++\n\tentry := &Entry{\n\t\tID:         c.nextID,\n\t\tSchedule:   schedule,\n\t\tWrappedJob: c.chain.Then(cmd),\n\t\tJob:        cmd,\n\t}\n\tif !c.running {\n\t\tc.entries = append(c.entries, entry)\n\t} else {\n\t\tc.add <- entry\n\t}\n\treturn entry.ID\n}\n\n// Entries returns a snapshot of the cron entries.\nfunc (c *Cron) Entries() []Entry {\n\tc.runningMu.Lock()\n\tdefer c.runningMu.Unlock()\n\tif c.running {\n\t\treplyChan := make(chan []Entry, 1)\n\t\tc.snapshot <- replyChan\n\t\treturn <-replyChan\n\t}\n\treturn c.entrySnapshot()\n}\n\n// Location gets the time zone location.\nfunc (c *Cron) Location() *time.Location {\n\treturn c.location\n}\n\n// Entry returns a snapshot of the given entry, or nil if it couldn't be found.\nfunc (c *Cron) Entry(id EntryID) Entry {\n\tfor _, entry := range c.Entries() {\n\t\tif id == entry.ID {\n\t\t\treturn entry\n\t\t}\n\t}\n\treturn Entry{}\n}\n\n// Remove an entry from being run in the future.\nfunc (c *Cron) Remove(id EntryID) {\n\tc.runningMu.Lock()\n\tdefer c.runningMu.Unlock()\n\tif c.running {\n\t\tc.remove <- id\n\t} else {\n\t\tc.removeEntry(id)\n\t}\n}\n\n// Start the cron scheduler in its own goroutine, or no-op if already started.\nfunc (c *Cron) Start() {\n\tc.runningMu.Lock()\n\tdefer c.runningMu.Unlock()\n\tif c.running {\n\t\treturn\n\t}\n\tc.running = true\n\tgo c.runScheduler()\n}\n\n// Run the cron scheduler, or no-op if already running.\nfunc (c *Cron) Run() {\n\tc.runningMu.Lock()\n\tif c.running {\n\t\tc.runningMu.Unlock()\n\t\treturn\n\t}\n\tc.running = true\n\tc.runningMu.Unlock()\n\tc.runScheduler()\n}\n\n// runScheduler runs the scheduler.. this is private just due to the need to synchronize\n// access to the 'running' state variable.\nfunc (c *Cron) runScheduler() {\n\tc.logger.Info(\"start\")\n\n\t// Figure out the next activation times for each entry.\n\tnow := c.now()\n\tfor _, entry := range c.entries {\n\t\tentry.Next = entry.Schedule.Next(now)\n\t\tc.logger.Info(\"schedule\", \"now\", now, \"entry\", entry.ID, \"next\", entry.Next)\n\t}\n\n\tfor {\n\t\t// Determine the next entry to run.\n\t\tslices.SortFunc(c.entries, func(a, b *Entry) int {\n\t\t\tswitch {\n\t\t\tcase a.Next.IsZero() && b.Next.IsZero():\n\t\t\t\treturn 0\n\t\t\tcase a.Next.IsZero():\n\t\t\t\treturn 1\n\t\t\tcase b.Next.IsZero():\n\t\t\t\treturn -1\n\t\t\tcase a.Next.Before(b.Next):\n\t\t\t\treturn -1\n\t\t\tcase b.Next.Before(a.Next):\n\t\t\t\treturn 1\n\t\t\tdefault:\n\t\t\t\treturn 0\n\t\t\t}\n\t\t})\n\n\t\tvar timer *time.Timer\n\t\tif len(c.entries) == 0 || c.entries[0].Next.IsZero() {\n\t\t\t// If there are no entries yet, just sleep - it still handles new entries\n\t\t\t// and stop requests.\n\t\t\ttimer = time.NewTimer(100000 * time.Hour)\n\t\t} else {\n\t\t\ttimer = time.NewTimer(c.entries[0].Next.Sub(now))\n\t\t}\n\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase now = <-timer.C:\n\t\t\t\tnow = now.In(c.location)\n\t\t\t\tc.logger.Info(\"wake\", \"now\", now)\n\n\t\t\t\t// Run every entry whose next time was less than now\n\t\t\t\tfor _, e := range c.entries {\n\t\t\t\t\tif e.Next.After(now) || e.Next.IsZero() {\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t\tc.startJob(e.WrappedJob)\n\t\t\t\t\te.Prev = e.Next\n\t\t\t\t\te.Next = e.Schedule.Next(now)\n\t\t\t\t\tc.logger.Info(\"run\", \"now\", now, \"entry\", e.ID, \"next\", e.Next)\n\t\t\t\t}\n\n\t\t\tcase newEntry := <-c.add:\n\t\t\t\ttimer.Stop()\n\t\t\t\tnow = c.now()\n\t\t\t\tnewEntry.Next = newEntry.Schedule.Next(now)\n\t\t\t\tc.entries = append(c.entries, newEntry)\n\t\t\t\tc.logger.Info(\"added\", \"now\", now, \"entry\", newEntry.ID, \"next\", newEntry.Next)\n\n\t\t\tcase replyChan := <-c.snapshot:\n\t\t\t\treplyChan <- c.entrySnapshot()\n\t\t\t\tcontinue\n\n\t\t\tcase <-c.stop:\n\t\t\t\ttimer.Stop()\n\t\t\t\tc.logger.Info(\"stop\")\n\t\t\t\treturn\n\n\t\t\tcase id := <-c.remove:\n\t\t\t\ttimer.Stop()\n\t\t\t\tnow = c.now()\n\t\t\t\tc.removeEntry(id)\n\t\t\t\tc.logger.Info(\"removed\", \"entry\", id)\n\t\t\t}\n\n\t\t\tbreak\n\t\t}\n\t}\n}\n\n// startJob runs the given job in a new goroutine.\nfunc (c *Cron) startJob(j Job) {\n\tc.jobWaiter.Go(func() {\n\t\tj.Run()\n\t})\n}\n\n// now returns current time in c location.\nfunc (c *Cron) now() time.Time {\n\treturn time.Now().In(c.location)\n}\n\n// Stop stops the cron scheduler if it is running; otherwise it does nothing.\n// A context is returned so the caller can wait for running jobs to complete.\nfunc (c *Cron) Stop() context.Context {\n\tc.runningMu.Lock()\n\tdefer c.runningMu.Unlock()\n\tif c.running {\n\t\tc.stop <- struct{}{}\n\t\tc.running = false\n\t}\n\tctx, cancel := context.WithCancel(context.Background())\n\tgo func() {\n\t\tc.jobWaiter.Wait()\n\t\tcancel()\n\t}()\n\treturn ctx\n}\n\n// entrySnapshot returns a copy of the current cron entry list.\nfunc (c *Cron) entrySnapshot() []Entry {\n\tvar entries = make([]Entry, len(c.entries))\n\tfor i, e := range c.entries {\n\t\tentries[i] = *e\n\t}\n\treturn entries\n}\n\nfunc (c *Cron) removeEntry(id EntryID) {\n\tvar entries []*Entry\n\tfor _, e := range c.entries {\n\t\tif e.ID != id {\n\t\t\tentries = append(entries, e)\n\t\t}\n\t}\n\tc.entries = entries\n}\n"
  },
  {
    "path": "plugin/cron/cron_test.go",
    "content": "//nolint:all\npackage cron\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"log\"\n\t\"strings\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"time\"\n)\n\n// Many tests schedule a job for every second, and then wait at most a second\n// for it to run.  This amount is just slightly larger than 1 second to\n// compensate for a few milliseconds of runtime.\nconst OneSecond = 1*time.Second + 50*time.Millisecond\n\ntype syncWriter struct {\n\twr bytes.Buffer\n\tm  sync.Mutex\n}\n\nfunc (sw *syncWriter) Write(data []byte) (n int, err error) {\n\tsw.m.Lock()\n\tn, err = sw.wr.Write(data)\n\tsw.m.Unlock()\n\treturn\n}\n\nfunc (sw *syncWriter) String() string {\n\tsw.m.Lock()\n\tdefer sw.m.Unlock()\n\treturn sw.wr.String()\n}\n\nfunc newBufLogger(sw *syncWriter) Logger {\n\treturn PrintfLogger(log.New(sw, \"\", log.LstdFlags))\n}\n\nfunc TestFuncPanicRecovery(t *testing.T) {\n\tvar buf syncWriter\n\tcron := New(WithParser(secondParser),\n\t\tWithChain(Recover(newBufLogger(&buf))))\n\tcron.Start()\n\tdefer cron.Stop()\n\tcron.AddFunc(\"* * * * * ?\", func() {\n\t\tpanic(\"YOLO\")\n\t})\n\n\tselect {\n\tcase <-time.After(OneSecond):\n\t\tif !strings.Contains(buf.String(), \"YOLO\") {\n\t\t\tt.Error(\"expected a panic to be logged, got none\")\n\t\t}\n\t\treturn\n\t}\n}\n\ntype DummyJob struct{}\n\nfunc (DummyJob) Run() {\n\tpanic(\"YOLO\")\n}\n\nfunc TestJobPanicRecovery(t *testing.T) {\n\tvar job DummyJob\n\n\tvar buf syncWriter\n\tcron := New(WithParser(secondParser),\n\t\tWithChain(Recover(newBufLogger(&buf))))\n\tcron.Start()\n\tdefer cron.Stop()\n\tcron.AddJob(\"* * * * * ?\", job)\n\n\tselect {\n\tcase <-time.After(OneSecond):\n\t\tif !strings.Contains(buf.String(), \"YOLO\") {\n\t\t\tt.Error(\"expected a panic to be logged, got none\")\n\t\t}\n\t\treturn\n\t}\n}\n\n// Start and stop cron with no entries.\nfunc TestNoEntries(t *testing.T) {\n\tcron := newWithSeconds()\n\tcron.Start()\n\n\tselect {\n\tcase <-time.After(OneSecond):\n\t\tt.Fatal(\"expected cron will be stopped immediately\")\n\tcase <-stop(cron):\n\t}\n}\n\n// Start, stop, then add an entry. Verify entry doesn't run.\nfunc TestStopCausesJobsToNotRun(t *testing.T) {\n\twg := &sync.WaitGroup{}\n\twg.Add(1)\n\n\tcron := newWithSeconds()\n\tcron.Start()\n\tcron.Stop()\n\tcron.AddFunc(\"* * * * * ?\", func() { wg.Done() })\n\n\tselect {\n\tcase <-time.After(OneSecond):\n\t\t// No job ran!\n\tcase <-wait(wg):\n\t\tt.Fatal(\"expected stopped cron does not run any job\")\n\t}\n}\n\n// Add a job, start cron, expect it runs.\nfunc TestAddBeforeRunning(t *testing.T) {\n\twg := &sync.WaitGroup{}\n\twg.Add(1)\n\n\tcron := newWithSeconds()\n\tcron.AddFunc(\"* * * * * ?\", func() { wg.Done() })\n\tcron.Start()\n\tdefer cron.Stop()\n\n\t// Give cron 2 seconds to run our job (which is always activated).\n\tselect {\n\tcase <-time.After(OneSecond):\n\t\tt.Fatal(\"expected job runs\")\n\tcase <-wait(wg):\n\t}\n}\n\n// Start cron, add a job, expect it runs.\nfunc TestAddWhileRunning(t *testing.T) {\n\twg := &sync.WaitGroup{}\n\twg.Add(1)\n\n\tcron := newWithSeconds()\n\tcron.Start()\n\tdefer cron.Stop()\n\tcron.AddFunc(\"* * * * * ?\", func() { wg.Done() })\n\n\tselect {\n\tcase <-time.After(OneSecond):\n\t\tt.Fatal(\"expected job runs\")\n\tcase <-wait(wg):\n\t}\n}\n\n// Test for #34. Adding a job after calling start results in multiple job invocations\nfunc TestAddWhileRunningWithDelay(t *testing.T) {\n\tcron := newWithSeconds()\n\tcron.Start()\n\tdefer cron.Stop()\n\ttime.Sleep(5 * time.Second)\n\tvar calls int64\n\tcron.AddFunc(\"* * * * * *\", func() { atomic.AddInt64(&calls, 1) })\n\n\t<-time.After(OneSecond)\n\tif atomic.LoadInt64(&calls) != 1 {\n\t\tt.Errorf(\"called %d times, expected 1\\n\", calls)\n\t}\n}\n\n// Add a job, remove a job, start cron, expect nothing runs.\nfunc TestRemoveBeforeRunning(t *testing.T) {\n\twg := &sync.WaitGroup{}\n\twg.Add(1)\n\n\tcron := newWithSeconds()\n\tid, _ := cron.AddFunc(\"* * * * * ?\", func() { wg.Done() })\n\tcron.Remove(id)\n\tcron.Start()\n\tdefer cron.Stop()\n\n\tselect {\n\tcase <-time.After(OneSecond):\n\t\t// Success, shouldn't run\n\tcase <-wait(wg):\n\t\tt.FailNow()\n\t}\n}\n\n// Start cron, add a job, remove it, expect it doesn't run.\nfunc TestRemoveWhileRunning(t *testing.T) {\n\twg := &sync.WaitGroup{}\n\twg.Add(1)\n\n\tcron := newWithSeconds()\n\tcron.Start()\n\tdefer cron.Stop()\n\tid, _ := cron.AddFunc(\"* * * * * ?\", func() { wg.Done() })\n\tcron.Remove(id)\n\n\tselect {\n\tcase <-time.After(OneSecond):\n\tcase <-wait(wg):\n\t\tt.FailNow()\n\t}\n}\n\n// Test timing with Entries.\nfunc TestSnapshotEntries(t *testing.T) {\n\twg := &sync.WaitGroup{}\n\twg.Add(1)\n\n\tcron := New()\n\tcron.AddFunc(\"@every 2s\", func() { wg.Done() })\n\tcron.Start()\n\tdefer cron.Stop()\n\n\t// Cron should fire in 2 seconds. After 1 second, call Entries.\n\tselect {\n\tcase <-time.After(OneSecond):\n\t\tcron.Entries()\n\t}\n\n\t// Even though Entries was called, the cron should fire at the 2 second mark.\n\tselect {\n\tcase <-time.After(OneSecond):\n\t\tt.Error(\"expected job runs at 2 second mark\")\n\tcase <-wait(wg):\n\t}\n}\n\n// Test that the entries are correctly sorted.\n// Add a bunch of long-in-the-future entries, and an immediate entry, and ensure\n// that the immediate entry runs immediately.\n// Also: Test that multiple jobs run in the same instant.\nfunc TestMultipleEntries(t *testing.T) {\n\twg := &sync.WaitGroup{}\n\twg.Add(2)\n\n\tcron := newWithSeconds()\n\tcron.AddFunc(\"0 0 0 1 1 ?\", func() {})\n\tcron.AddFunc(\"* * * * * ?\", func() { wg.Done() })\n\tid1, _ := cron.AddFunc(\"* * * * * ?\", func() { t.Fatal() })\n\tid2, _ := cron.AddFunc(\"* * * * * ?\", func() { t.Fatal() })\n\tcron.AddFunc(\"0 0 0 31 12 ?\", func() {})\n\tcron.AddFunc(\"* * * * * ?\", func() { wg.Done() })\n\n\tcron.Remove(id1)\n\tcron.Start()\n\tcron.Remove(id2)\n\tdefer cron.Stop()\n\n\tselect {\n\tcase <-time.After(OneSecond):\n\t\tt.Error(\"expected job run in proper order\")\n\tcase <-wait(wg):\n\t}\n}\n\n// Test running the same job twice.\nfunc TestRunningJobTwice(t *testing.T) {\n\twg := &sync.WaitGroup{}\n\twg.Add(2)\n\n\tcron := newWithSeconds()\n\tcron.AddFunc(\"0 0 0 1 1 ?\", func() {})\n\tcron.AddFunc(\"0 0 0 31 12 ?\", func() {})\n\tcron.AddFunc(\"* * * * * ?\", func() { wg.Done() })\n\n\tcron.Start()\n\tdefer cron.Stop()\n\n\tselect {\n\tcase <-time.After(2 * OneSecond):\n\t\tt.Error(\"expected job fires 2 times\")\n\tcase <-wait(wg):\n\t}\n}\n\nfunc TestRunningMultipleSchedules(t *testing.T) {\n\twg := &sync.WaitGroup{}\n\twg.Add(2)\n\n\tcron := newWithSeconds()\n\tcron.AddFunc(\"0 0 0 1 1 ?\", func() {})\n\tcron.AddFunc(\"0 0 0 31 12 ?\", func() {})\n\tcron.AddFunc(\"* * * * * ?\", func() { wg.Done() })\n\tcron.Schedule(Every(time.Minute), FuncJob(func() {}))\n\tcron.Schedule(Every(time.Second), FuncJob(func() { wg.Done() }))\n\tcron.Schedule(Every(time.Hour), FuncJob(func() {}))\n\n\tcron.Start()\n\tdefer cron.Stop()\n\n\tselect {\n\tcase <-time.After(2 * OneSecond):\n\t\tt.Error(\"expected job fires 2 times\")\n\tcase <-wait(wg):\n\t}\n}\n\n// Test that the cron is run in the local time zone (as opposed to UTC).\nfunc TestLocalTimezone(t *testing.T) {\n\twg := &sync.WaitGroup{}\n\twg.Add(2)\n\n\tnow := time.Now()\n\t// FIX: Issue #205\n\t// This calculation doesn't work in seconds 58 or 59.\n\t// Take the easy way out and sleep.\n\tif now.Second() >= 58 {\n\t\ttime.Sleep(2 * time.Second)\n\t\tnow = time.Now()\n\t}\n\tspec := fmt.Sprintf(\"%d,%d %d %d %d %d ?\",\n\t\tnow.Second()+1, now.Second()+2, now.Minute(), now.Hour(), now.Day(), now.Month())\n\n\tcron := newWithSeconds()\n\tcron.AddFunc(spec, func() { wg.Done() })\n\tcron.Start()\n\tdefer cron.Stop()\n\n\tselect {\n\tcase <-time.After(OneSecond * 2):\n\t\tt.Error(\"expected job fires 2 times\")\n\tcase <-wait(wg):\n\t}\n}\n\n// Test that the cron is run in the given time zone (as opposed to local).\nfunc TestNonLocalTimezone(t *testing.T) {\n\twg := &sync.WaitGroup{}\n\twg.Add(2)\n\n\tloc, err := time.LoadLocation(\"Atlantic/Cape_Verde\")\n\tif err != nil {\n\t\tfmt.Printf(\"Failed to load time zone Atlantic/Cape_Verde: %+v\", err)\n\t\tt.Fail()\n\t}\n\n\tnow := time.Now().In(loc)\n\t// FIX: Issue #205\n\t// This calculation doesn't work in seconds 58 or 59.\n\t// Take the easy way out and sleep.\n\tif now.Second() >= 58 {\n\t\ttime.Sleep(2 * time.Second)\n\t\tnow = time.Now().In(loc)\n\t}\n\tspec := fmt.Sprintf(\"%d,%d %d %d %d %d ?\",\n\t\tnow.Second()+1, now.Second()+2, now.Minute(), now.Hour(), now.Day(), now.Month())\n\n\tcron := New(WithLocation(loc), WithParser(secondParser))\n\tcron.AddFunc(spec, func() { wg.Done() })\n\tcron.Start()\n\tdefer cron.Stop()\n\n\tselect {\n\tcase <-time.After(OneSecond * 2):\n\t\tt.Error(\"expected job fires 2 times\")\n\tcase <-wait(wg):\n\t}\n}\n\n// Test that calling stop before start silently returns without\n// blocking the stop channel.\nfunc TestStopWithoutStart(t *testing.T) {\n\tcron := New()\n\tcron.Stop()\n}\n\ntype testJob struct {\n\twg   *sync.WaitGroup\n\tname string\n}\n\nfunc (t testJob) Run() {\n\tt.wg.Done()\n}\n\n// Test that adding an invalid job spec returns an error\nfunc TestInvalidJobSpec(t *testing.T) {\n\tcron := New()\n\t_, err := cron.AddJob(\"this will not parse\", nil)\n\tif err == nil {\n\t\tt.Errorf(\"expected an error with invalid spec, got nil\")\n\t}\n}\n\n// Test blocking run method behaves as Start()\nfunc TestBlockingRun(t *testing.T) {\n\twg := &sync.WaitGroup{}\n\twg.Add(1)\n\n\tcron := newWithSeconds()\n\tcron.AddFunc(\"* * * * * ?\", func() { wg.Done() })\n\n\tvar unblockChan = make(chan struct{})\n\n\tgo func() {\n\t\tcron.Run()\n\t\tclose(unblockChan)\n\t}()\n\tdefer cron.Stop()\n\n\tselect {\n\tcase <-time.After(OneSecond):\n\t\tt.Error(\"expected job fires\")\n\tcase <-unblockChan:\n\t\tt.Error(\"expected that Run() blocks\")\n\tcase <-wait(wg):\n\t}\n}\n\n// Test that double-running is a no-op\nfunc TestStartNoop(t *testing.T) {\n\tvar tickChan = make(chan struct{}, 2)\n\n\tcron := newWithSeconds()\n\tcron.AddFunc(\"* * * * * ?\", func() {\n\t\ttickChan <- struct{}{}\n\t})\n\n\tcron.Start()\n\tdefer cron.Stop()\n\n\t// Wait for the first firing to ensure the runner is going\n\t<-tickChan\n\n\tcron.Start()\n\n\t<-tickChan\n\n\t// Fail if this job fires again in a short period, indicating a double-run\n\tselect {\n\tcase <-time.After(time.Millisecond):\n\tcase <-tickChan:\n\t\tt.Error(\"expected job fires exactly twice\")\n\t}\n}\n\n// Simple test using Runnables.\nfunc TestJob(t *testing.T) {\n\twg := &sync.WaitGroup{}\n\twg.Add(1)\n\n\tcron := newWithSeconds()\n\tcron.AddJob(\"0 0 0 30 Feb ?\", testJob{wg, \"job0\"})\n\tcron.AddJob(\"0 0 0 1 1 ?\", testJob{wg, \"job1\"})\n\tjob2, _ := cron.AddJob(\"* * * * * ?\", testJob{wg, \"job2\"})\n\tcron.AddJob(\"1 0 0 1 1 ?\", testJob{wg, \"job3\"})\n\tcron.Schedule(Every(5*time.Second+5*time.Nanosecond), testJob{wg, \"job4\"})\n\tjob5 := cron.Schedule(Every(5*time.Minute), testJob{wg, \"job5\"})\n\n\t// Test getting an Entry pre-Start.\n\tif actualName := cron.Entry(job2).Job.(testJob).name; actualName != \"job2\" {\n\t\tt.Error(\"wrong job retrieved:\", actualName)\n\t}\n\tif actualName := cron.Entry(job5).Job.(testJob).name; actualName != \"job5\" {\n\t\tt.Error(\"wrong job retrieved:\", actualName)\n\t}\n\n\tcron.Start()\n\tdefer cron.Stop()\n\n\tselect {\n\tcase <-time.After(OneSecond):\n\t\tt.FailNow()\n\tcase <-wait(wg):\n\t}\n\n\t// Ensure the entries are in the right order.\n\texpecteds := []string{\"job2\", \"job4\", \"job5\", \"job1\", \"job3\", \"job0\"}\n\n\tvar actuals []string\n\tfor _, entry := range cron.Entries() {\n\t\tactuals = append(actuals, entry.Job.(testJob).name)\n\t}\n\n\tfor i, expected := range expecteds {\n\t\tif actuals[i] != expected {\n\t\t\tt.Fatalf(\"Jobs not in the right order.  (expected) %s != %s (actual)\", expecteds, actuals)\n\t\t}\n\t}\n\n\t// Test getting Entries.\n\tif actualName := cron.Entry(job2).Job.(testJob).name; actualName != \"job2\" {\n\t\tt.Error(\"wrong job retrieved:\", actualName)\n\t}\n\tif actualName := cron.Entry(job5).Job.(testJob).name; actualName != \"job5\" {\n\t\tt.Error(\"wrong job retrieved:\", actualName)\n\t}\n}\n\n// Issue #206\n// Ensure that the next run of a job after removing an entry is accurate.\nfunc TestScheduleAfterRemoval(t *testing.T) {\n\tvar wg1 sync.WaitGroup\n\tvar wg2 sync.WaitGroup\n\twg1.Add(1)\n\twg2.Add(1)\n\n\t// The first time this job is run, set a timer and remove the other job\n\t// 750ms later. Correct behavior would be to still run the job again in\n\t// 250ms, but the bug would cause it to run instead 1s later.\n\n\tvar calls int\n\tvar mu sync.Mutex\n\n\tcron := newWithSeconds()\n\thourJob := cron.Schedule(Every(time.Hour), FuncJob(func() {}))\n\tcron.Schedule(Every(time.Second), FuncJob(func() {\n\t\tmu.Lock()\n\t\tdefer mu.Unlock()\n\t\tswitch calls {\n\t\tcase 0:\n\t\t\twg1.Done()\n\t\t\tcalls++\n\t\tcase 1:\n\t\t\ttime.Sleep(750 * time.Millisecond)\n\t\t\tcron.Remove(hourJob)\n\t\t\tcalls++\n\t\tcase 2:\n\t\t\tcalls++\n\t\t\twg2.Done()\n\t\tcase 3:\n\t\t\tpanic(\"unexpected 3rd call\")\n\t\t}\n\t}))\n\n\tcron.Start()\n\tdefer cron.Stop()\n\n\t// the first run might be any length of time 0 - 1s, since the schedule\n\t// rounds to the second. wait for the first run to true up.\n\twg1.Wait()\n\n\tselect {\n\tcase <-time.After(2 * OneSecond):\n\t\tt.Error(\"expected job fires 2 times\")\n\tcase <-wait(&wg2):\n\t}\n}\n\ntype ZeroSchedule struct{}\n\nfunc (*ZeroSchedule) Next(time.Time) time.Time {\n\treturn time.Time{}\n}\n\n// Tests that job without time does not run\nfunc TestJobWithZeroTimeDoesNotRun(t *testing.T) {\n\tcron := newWithSeconds()\n\tvar calls int64\n\tcron.AddFunc(\"* * * * * *\", func() { atomic.AddInt64(&calls, 1) })\n\tcron.Schedule(new(ZeroSchedule), FuncJob(func() { t.Error(\"expected zero task will not run\") }))\n\tcron.Start()\n\tdefer cron.Stop()\n\t<-time.After(OneSecond)\n\tif atomic.LoadInt64(&calls) != 1 {\n\t\tt.Errorf(\"called %d times, expected 1\\n\", calls)\n\t}\n}\n\nfunc TestStopAndWait(t *testing.T) {\n\tt.Run(\"nothing running, returns immediately\", func(*testing.T) {\n\t\tcron := newWithSeconds()\n\t\tcron.Start()\n\t\tctx := cron.Stop()\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\tcase <-time.After(time.Millisecond):\n\t\t\tt.Error(\"context was not done immediately\")\n\t\t}\n\t})\n\n\tt.Run(\"repeated calls to Stop\", func(*testing.T) {\n\t\tcron := newWithSeconds()\n\t\tcron.Start()\n\t\t_ = cron.Stop()\n\t\ttime.Sleep(time.Millisecond)\n\t\tctx := cron.Stop()\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\tcase <-time.After(time.Millisecond):\n\t\t\tt.Error(\"context was not done immediately\")\n\t\t}\n\t})\n\n\tt.Run(\"a couple fast jobs added, still returns immediately\", func(*testing.T) {\n\t\tcron := newWithSeconds()\n\t\tcron.AddFunc(\"* * * * * *\", func() {})\n\t\tcron.Start()\n\t\tcron.AddFunc(\"* * * * * *\", func() {})\n\t\tcron.AddFunc(\"* * * * * *\", func() {})\n\t\tcron.AddFunc(\"* * * * * *\", func() {})\n\t\ttime.Sleep(time.Second)\n\t\tctx := cron.Stop()\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\tcase <-time.After(time.Millisecond):\n\t\t\tt.Error(\"context was not done immediately\")\n\t\t}\n\t})\n\n\tt.Run(\"a couple fast jobs and a slow job added, waits for slow job\", func(*testing.T) {\n\t\tcron := newWithSeconds()\n\t\tcron.AddFunc(\"* * * * * *\", func() {})\n\t\tcron.Start()\n\t\tcron.AddFunc(\"* * * * * *\", func() { time.Sleep(2 * time.Second) })\n\t\tcron.AddFunc(\"* * * * * *\", func() {})\n\t\ttime.Sleep(time.Second)\n\n\t\tctx := cron.Stop()\n\n\t\t// Verify that it is not done for at least 750ms\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\tt.Error(\"context was done too quickly immediately\")\n\t\tcase <-time.After(750 * time.Millisecond):\n\t\t\t// expected, because the job sleeping for 1 second is still running\n\t\t}\n\n\t\t// Verify that it IS done in the next 500ms (giving 250ms buffer)\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\t// expected\n\t\tcase <-time.After(1500 * time.Millisecond):\n\t\t\tt.Error(\"context not done after job should have completed\")\n\t\t}\n\t})\n\n\tt.Run(\"repeated calls to stop, waiting for completion and after\", func(*testing.T) {\n\t\tcron := newWithSeconds()\n\t\tcron.AddFunc(\"* * * * * *\", func() {})\n\t\tcron.AddFunc(\"* * * * * *\", func() { time.Sleep(2 * time.Second) })\n\t\tcron.Start()\n\t\tcron.AddFunc(\"* * * * * *\", func() {})\n\t\ttime.Sleep(time.Second)\n\t\tctx := cron.Stop()\n\t\tctx2 := cron.Stop()\n\n\t\t// Verify that it is not done for at least 1500ms\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\tt.Error(\"context was done too quickly immediately\")\n\t\tcase <-ctx2.Done():\n\t\t\tt.Error(\"context2 was done too quickly immediately\")\n\t\tcase <-time.After(1500 * time.Millisecond):\n\t\t\t// expected, because the job sleeping for 2 seconds is still running\n\t\t}\n\n\t\t// Verify that it IS done in the next 1s (giving 500ms buffer)\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\t// expected\n\t\tcase <-time.After(time.Second):\n\t\t\tt.Error(\"context not done after job should have completed\")\n\t\t}\n\n\t\t// Verify that ctx2 is also done.\n\t\tselect {\n\t\tcase <-ctx2.Done():\n\t\t\t// expected\n\t\tcase <-time.After(time.Millisecond):\n\t\t\tt.Error(\"context2 not done even though context1 is\")\n\t\t}\n\n\t\t// Verify that a new context retrieved from stop is immediately done.\n\t\tctx3 := cron.Stop()\n\t\tselect {\n\t\tcase <-ctx3.Done():\n\t\t\t// expected\n\t\tcase <-time.After(time.Millisecond):\n\t\t\tt.Error(\"context not done even when cron Stop is completed\")\n\t\t}\n\t})\n}\n\nfunc TestMultiThreadedStartAndStop(t *testing.T) {\n\tcron := New()\n\tgo cron.Run()\n\ttime.Sleep(2 * time.Millisecond)\n\tcron.Stop()\n}\n\nfunc wait(wg *sync.WaitGroup) chan bool {\n\tch := make(chan bool)\n\tgo func() {\n\t\twg.Wait()\n\t\tch <- true\n\t}()\n\treturn ch\n}\n\nfunc stop(cron *Cron) chan bool {\n\tch := make(chan bool)\n\tgo func() {\n\t\tcron.Stop()\n\t\tch <- true\n\t}()\n\treturn ch\n}\n\n// newWithSeconds returns a Cron with the seconds field enabled.\nfunc newWithSeconds() *Cron {\n\treturn New(WithParser(secondParser), WithChain())\n}\n"
  },
  {
    "path": "plugin/cron/logger.go",
    "content": "package cron\n\nimport (\n\t\"io\"\n\t\"log\"\n\t\"os\"\n\t\"strings\"\n\t\"time\"\n)\n\n// DefaultLogger is used by Cron if none is specified.\nvar DefaultLogger = PrintfLogger(log.New(os.Stdout, \"cron: \", log.LstdFlags))\n\n// DiscardLogger can be used by callers to discard all log messages.\nvar DiscardLogger = PrintfLogger(log.New(io.Discard, \"\", 0))\n\n// Logger is the interface used in this package for logging, so that any backend\n// can be plugged in. It is a subset of the github.com/go-logr/logr interface.\ntype Logger interface {\n\t// Info logs routine messages about cron's operation.\n\tInfo(msg string, keysAndValues ...interface{})\n\t// Error logs an error condition.\n\tError(err error, msg string, keysAndValues ...interface{})\n}\n\n// PrintfLogger wraps a Printf-based logger (such as the standard library \"log\")\n// into an implementation of the Logger interface which logs errors only.\nfunc PrintfLogger(l interface{ Printf(string, ...interface{}) }) Logger {\n\treturn printfLogger{l, false}\n}\n\n// VerbosePrintfLogger wraps a Printf-based logger (such as the standard library\n// \"log\") into an implementation of the Logger interface which logs everything.\nfunc VerbosePrintfLogger(l interface{ Printf(string, ...interface{}) }) Logger {\n\treturn printfLogger{l, true}\n}\n\ntype printfLogger struct {\n\tlogger  interface{ Printf(string, ...interface{}) }\n\tlogInfo bool\n}\n\nfunc (pl printfLogger) Info(msg string, keysAndValues ...interface{}) {\n\tif pl.logInfo {\n\t\tkeysAndValues = formatTimes(keysAndValues)\n\t\tpl.logger.Printf(\n\t\t\tformatString(len(keysAndValues)),\n\t\t\tappend([]interface{}{msg}, keysAndValues...)...)\n\t}\n}\n\nfunc (pl printfLogger) Error(err error, msg string, keysAndValues ...interface{}) {\n\tkeysAndValues = formatTimes(keysAndValues)\n\tpl.logger.Printf(\n\t\tformatString(len(keysAndValues)+2),\n\t\tappend([]interface{}{msg, \"error\", err}, keysAndValues...)...)\n}\n\n// formatString returns a logfmt-like format string for the number of\n// key/values.\nfunc formatString(numKeysAndValues int) string {\n\tvar sb strings.Builder\n\tsb.WriteString(\"%s\")\n\tif numKeysAndValues > 0 {\n\t\tsb.WriteString(\", \")\n\t}\n\tfor i := 0; i < numKeysAndValues/2; i++ {\n\t\tif i > 0 {\n\t\t\tsb.WriteString(\", \")\n\t\t}\n\t\tsb.WriteString(\"%v=%v\")\n\t}\n\treturn sb.String()\n}\n\n// formatTimes formats any time.Time values as RFC3339.\nfunc formatTimes(keysAndValues []interface{}) []interface{} {\n\tvar formattedArgs []interface{}\n\tfor _, arg := range keysAndValues {\n\t\tif t, ok := arg.(time.Time); ok {\n\t\t\targ = t.Format(time.RFC3339)\n\t\t}\n\t\tformattedArgs = append(formattedArgs, arg)\n\t}\n\treturn formattedArgs\n}\n"
  },
  {
    "path": "plugin/cron/option.go",
    "content": "package cron\n\nimport (\n\t\"time\"\n)\n\n// Option represents a modification to the default behavior of a Cron.\ntype Option func(*Cron)\n\n// WithLocation overrides the timezone of the cron instance.\nfunc WithLocation(loc *time.Location) Option {\n\treturn func(c *Cron) {\n\t\tc.location = loc\n\t}\n}\n\n// WithSeconds overrides the parser used for interpreting job schedules to\n// include a seconds field as the first one.\nfunc WithSeconds() Option {\n\treturn WithParser(NewParser(\n\t\tSecond | Minute | Hour | Dom | Month | Dow | Descriptor,\n\t))\n}\n\n// WithParser overrides the parser used for interpreting job schedules.\nfunc WithParser(p ScheduleParser) Option {\n\treturn func(c *Cron) {\n\t\tc.parser = p\n\t}\n}\n\n// WithChain specifies Job wrappers to apply to all jobs added to this cron.\n// Refer to the Chain* functions in this package for provided wrappers.\nfunc WithChain(wrappers ...JobWrapper) Option {\n\treturn func(c *Cron) {\n\t\tc.chain = NewChain(wrappers...)\n\t}\n}\n\n// WithLogger uses the provided logger.\nfunc WithLogger(logger Logger) Option {\n\treturn func(c *Cron) {\n\t\tc.logger = logger\n\t}\n}\n"
  },
  {
    "path": "plugin/cron/option_test.go",
    "content": "//nolint:all\npackage cron\n\nimport (\n\t\"log\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n)\n\nfunc TestWithLocation(t *testing.T) {\n\tc := New(WithLocation(time.UTC))\n\tif c.location != time.UTC {\n\t\tt.Errorf(\"expected UTC, got %v\", c.location)\n\t}\n}\n\nfunc TestWithParser(t *testing.T) {\n\tvar parser = NewParser(Dow)\n\tc := New(WithParser(parser))\n\tif c.parser != parser {\n\t\tt.Error(\"expected provided parser\")\n\t}\n}\n\nfunc TestWithVerboseLogger(t *testing.T) {\n\tvar buf syncWriter\n\tvar logger = log.New(&buf, \"\", log.LstdFlags)\n\tc := New(WithLogger(VerbosePrintfLogger(logger)))\n\tif c.logger.(printfLogger).logger != logger {\n\t\tt.Error(\"expected provided logger\")\n\t}\n\n\tc.AddFunc(\"@every 1s\", func() {})\n\tc.Start()\n\ttime.Sleep(OneSecond)\n\tc.Stop()\n\tout := buf.String()\n\tif !strings.Contains(out, \"schedule,\") ||\n\t\t!strings.Contains(out, \"run,\") {\n\t\tt.Error(\"expected to see some actions, got:\", out)\n\t}\n}\n"
  },
  {
    "path": "plugin/cron/parser.go",
    "content": "package cron\n\nimport (\n\t\"math\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/pkg/errors\"\n)\n\n// Configuration options for creating a parser. Most options specify which\n// fields should be included, while others enable features. If a field is not\n// included the parser will assume a default value. These options do not change\n// the order fields are parsed in.\ntype ParseOption int\n\nconst (\n\tSecond         ParseOption = 1 << iota // Seconds field, default 0\n\tSecondOptional                         // Optional seconds field, default 0\n\tMinute                                 // Minutes field, default 0\n\tHour                                   // Hours field, default 0\n\tDom                                    // Day of month field, default *\n\tMonth                                  // Month field, default *\n\tDow                                    // Day of week field, default *\n\tDowOptional                            // Optional day of week field, default *\n\tDescriptor                             // Allow descriptors such as @monthly, @weekly, etc.\n)\n\nvar places = []ParseOption{\n\tSecond,\n\tMinute,\n\tHour,\n\tDom,\n\tMonth,\n\tDow,\n}\n\nvar defaults = []string{\n\t\"0\",\n\t\"0\",\n\t\"0\",\n\t\"*\",\n\t\"*\",\n\t\"*\",\n}\n\n// A custom Parser that can be configured.\ntype Parser struct {\n\toptions ParseOption\n}\n\n// NewParser creates a Parser with custom options.\n//\n// It panics if more than one Optional is given, since it would be impossible to\n// correctly infer which optional is provided or missing in general.\n//\n// Examples\n//\n//\t// Standard parser without descriptors\n//\tspecParser := NewParser(Minute | Hour | Dom | Month | Dow)\n//\tsched, err := specParser.Parse(\"0 0 15 */3 *\")\n//\n//\t// Same as above, just excludes time fields\n//\tspecParser := NewParser(Dom | Month | Dow)\n//\tsched, err := specParser.Parse(\"15 */3 *\")\n//\n//\t// Same as above, just makes Dow optional\n//\tspecParser := NewParser(Dom | Month | DowOptional)\n//\tsched, err := specParser.Parse(\"15 */3\")\nfunc NewParser(options ParseOption) Parser {\n\toptionals := 0\n\tif options&DowOptional > 0 {\n\t\toptionals++\n\t}\n\tif options&SecondOptional > 0 {\n\t\toptionals++\n\t}\n\tif optionals > 1 {\n\t\tpanic(\"multiple optionals may not be configured\")\n\t}\n\treturn Parser{options}\n}\n\n// Parse returns a new crontab schedule representing the given spec.\n// It returns a descriptive error if the spec is not valid.\n// It accepts crontab specs and features configured by NewParser.\nfunc (p Parser) Parse(spec string) (Schedule, error) {\n\tif len(spec) == 0 {\n\t\treturn nil, errors.New(\"empty spec string\")\n\t}\n\n\t// Extract timezone if present\n\tvar loc = time.Local\n\tif strings.HasPrefix(spec, \"TZ=\") || strings.HasPrefix(spec, \"CRON_TZ=\") {\n\t\tvar err error\n\t\ti := strings.Index(spec, \" \")\n\t\teq := strings.Index(spec, \"=\")\n\t\tif loc, err = time.LoadLocation(spec[eq+1 : i]); err != nil {\n\t\t\treturn nil, errors.Wrap(err, \"provided bad location\")\n\t\t}\n\t\tspec = strings.TrimSpace(spec[i:])\n\t}\n\n\t// Handle named schedules (descriptors), if configured\n\tif strings.HasPrefix(spec, \"@\") {\n\t\tif p.options&Descriptor == 0 {\n\t\t\treturn nil, errors.New(\"descriptors not enabled\")\n\t\t}\n\t\treturn parseDescriptor(spec, loc)\n\t}\n\n\t// Split on whitespace.\n\tfields := strings.Fields(spec)\n\n\t// Validate & fill in any omitted or optional fields\n\tvar err error\n\tfields, err = normalizeFields(fields, p.options)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfield := func(field string, r bounds) uint64 {\n\t\tif err != nil {\n\t\t\treturn 0\n\t\t}\n\t\tvar bits uint64\n\t\tbits, err = getField(field, r)\n\t\treturn bits\n\t}\n\n\tvar (\n\t\tsecond     = field(fields[0], seconds)\n\t\tminute     = field(fields[1], minutes)\n\t\thour       = field(fields[2], hours)\n\t\tdayofmonth = field(fields[3], dom)\n\t\tmonth      = field(fields[4], months)\n\t\tdayofweek  = field(fields[5], dow)\n\t)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &SpecSchedule{\n\t\tSecond:   second,\n\t\tMinute:   minute,\n\t\tHour:     hour,\n\t\tDom:      dayofmonth,\n\t\tMonth:    month,\n\t\tDow:      dayofweek,\n\t\tLocation: loc,\n\t}, nil\n}\n\n// normalizeFields takes a subset set of the time fields and returns the full set\n// with defaults (zeroes) populated for unset fields.\n//\n// As part of performing this function, it also validates that the provided\n// fields are compatible with the configured options.\nfunc normalizeFields(fields []string, options ParseOption) ([]string, error) {\n\t// Validate optionals & add their field to options\n\toptionals := 0\n\tif options&SecondOptional > 0 {\n\t\toptions |= Second\n\t\toptionals++\n\t}\n\tif options&DowOptional > 0 {\n\t\toptions |= Dow\n\t\toptionals++\n\t}\n\tif optionals > 1 {\n\t\treturn nil, errors.New(\"multiple optionals may not be configured\")\n\t}\n\n\t// Figure out how many fields we need\n\tmax := 0\n\tfor _, place := range places {\n\t\tif options&place > 0 {\n\t\t\tmax++\n\t\t}\n\t}\n\tmin := max - optionals\n\n\t// Validate number of fields\n\tif count := len(fields); count < min || count > max {\n\t\tif min == max {\n\t\t\treturn nil, errors.New(\"incorrect number of fields\")\n\t\t}\n\t\treturn nil, errors.New(\"incorrect number of fields, expected \" + strconv.Itoa(min) + \"-\" + strconv.Itoa(max))\n\t}\n\n\t// Populate the optional field if not provided\n\tif min < max && len(fields) == min {\n\t\tswitch {\n\t\tcase options&DowOptional > 0:\n\t\t\tfields = append(fields, defaults[5]) // TODO: improve access to default\n\t\tcase options&SecondOptional > 0:\n\t\t\tfields = append([]string{defaults[0]}, fields...)\n\t\tdefault:\n\t\t\treturn nil, errors.New(\"unexpected optional field\")\n\t\t}\n\t}\n\n\t// Populate all fields not part of options with their defaults\n\tn := 0\n\texpandedFields := make([]string, len(places))\n\tcopy(expandedFields, defaults)\n\tfor i, place := range places {\n\t\tif options&place > 0 {\n\t\t\texpandedFields[i] = fields[n]\n\t\t\tn++\n\t\t}\n\t}\n\treturn expandedFields, nil\n}\n\nvar standardParser = NewParser(\n\tMinute | Hour | Dom | Month | Dow | Descriptor,\n)\n\n// ParseStandard returns a new crontab schedule representing the given\n// standardSpec (https://en.wikipedia.org/wiki/Cron). It requires 5 entries\n// representing: minute, hour, day of month, month and day of week, in that\n// order. It returns a descriptive error if the spec is not valid.\n//\n// It accepts\n//   - Standard crontab specs, e.g. \"* * * * ?\"\n//   - Descriptors, e.g. \"@midnight\", \"@every 1h30m\"\nfunc ParseStandard(standardSpec string) (Schedule, error) {\n\treturn standardParser.Parse(standardSpec)\n}\n\n// getField returns an Int with the bits set representing all of the times that\n// the field represents or error parsing field value.  A \"field\" is a comma-separated\n// list of \"ranges\".\nfunc getField(field string, r bounds) (uint64, error) {\n\tvar bits uint64\n\tranges := strings.FieldsFunc(field, func(r rune) bool { return r == ',' })\n\tfor _, expr := range ranges {\n\t\tbit, err := getRange(expr, r)\n\t\tif err != nil {\n\t\t\treturn bits, err\n\t\t}\n\t\tbits |= bit\n\t}\n\treturn bits, nil\n}\n\n// getRange returns the bits indicated by the given expression:\n//\n//\tnumber | number \"-\" number [ \"/\" number ]\n//\n// or error parsing range.\nfunc getRange(expr string, r bounds) (uint64, error) {\n\tvar (\n\t\tstart, end, step uint\n\t\trangeAndStep     = strings.Split(expr, \"/\")\n\t\tlowAndHigh       = strings.Split(rangeAndStep[0], \"-\")\n\t\tsingleDigit      = len(lowAndHigh) == 1\n\t\terr              error\n\t)\n\n\tvar extra uint64\n\tif lowAndHigh[0] == \"*\" || lowAndHigh[0] == \"?\" {\n\t\tstart = r.min\n\t\tend = r.max\n\t\textra = starBit\n\t} else {\n\t\tstart, err = parseIntOrName(lowAndHigh[0], r.names)\n\t\tif err != nil {\n\t\t\treturn 0, err\n\t\t}\n\t\tswitch len(lowAndHigh) {\n\t\tcase 1:\n\t\t\tend = start\n\t\tcase 2:\n\t\t\tend, err = parseIntOrName(lowAndHigh[1], r.names)\n\t\t\tif err != nil {\n\t\t\t\treturn 0, err\n\t\t\t}\n\t\tdefault:\n\t\t\treturn 0, errors.New(\"too many hyphens: \" + expr)\n\t\t}\n\t}\n\n\tswitch len(rangeAndStep) {\n\tcase 1:\n\t\tstep = 1\n\tcase 2:\n\t\tstep, err = mustParseInt(rangeAndStep[1])\n\t\tif err != nil {\n\t\t\treturn 0, err\n\t\t}\n\n\t\t// Special handling: \"N/step\" means \"N-max/step\".\n\t\tif singleDigit {\n\t\t\tend = r.max\n\t\t}\n\t\tif step > 1 {\n\t\t\textra = 0\n\t\t}\n\tdefault:\n\t\treturn 0, errors.New(\"too many slashes: \" + expr)\n\t}\n\n\tif start < r.min {\n\t\treturn 0, errors.New(\"beginning of range below minimum: \" + expr)\n\t}\n\tif end > r.max {\n\t\treturn 0, errors.New(\"end of range above maximum: \" + expr)\n\t}\n\tif start > end {\n\t\treturn 0, errors.New(\"beginning of range after end: \" + expr)\n\t}\n\tif step == 0 {\n\t\treturn 0, errors.New(\"step cannot be zero: \" + expr)\n\t}\n\n\treturn getBits(start, end, step) | extra, nil\n}\n\n// parseIntOrName returns the (possibly-named) integer contained in expr.\nfunc parseIntOrName(expr string, names map[string]uint) (uint, error) {\n\tif names != nil {\n\t\tif namedInt, ok := names[strings.ToLower(expr)]; ok {\n\t\t\treturn namedInt, nil\n\t\t}\n\t}\n\treturn mustParseInt(expr)\n}\n\n// mustParseInt parses the given expression as an int or returns an error.\nfunc mustParseInt(expr string) (uint, error) {\n\tnum, err := strconv.Atoi(expr)\n\tif err != nil {\n\t\treturn 0, errors.Wrap(err, \"failed to parse number\")\n\t}\n\tif num < 0 {\n\t\treturn 0, errors.New(\"number must be positive\")\n\t}\n\n\treturn uint(num), nil\n}\n\n// getBits sets all bits in the range [min, max], modulo the given step size.\nfunc getBits(min, max, step uint) uint64 {\n\tvar bits uint64\n\n\t// If step is 1, use shifts.\n\tif step == 1 {\n\t\treturn ^(math.MaxUint64 << (max + 1)) & (math.MaxUint64 << min)\n\t}\n\n\t// Else, use a simple loop.\n\tfor i := min; i <= max; i += step {\n\t\tbits |= 1 << i\n\t}\n\treturn bits\n}\n\n// all returns all bits within the given bounds.\nfunc all(r bounds) uint64 {\n\treturn getBits(r.min, r.max, 1) | starBit\n}\n\n// parseDescriptor returns a predefined schedule for the expression, or error if none matches.\nfunc parseDescriptor(descriptor string, loc *time.Location) (Schedule, error) {\n\tswitch descriptor {\n\tcase \"@yearly\", \"@annually\":\n\t\treturn &SpecSchedule{\n\t\t\tSecond:   1 << seconds.min,\n\t\t\tMinute:   1 << minutes.min,\n\t\t\tHour:     1 << hours.min,\n\t\t\tDom:      1 << dom.min,\n\t\t\tMonth:    1 << months.min,\n\t\t\tDow:      all(dow),\n\t\t\tLocation: loc,\n\t\t}, nil\n\n\tcase \"@monthly\":\n\t\treturn &SpecSchedule{\n\t\t\tSecond:   1 << seconds.min,\n\t\t\tMinute:   1 << minutes.min,\n\t\t\tHour:     1 << hours.min,\n\t\t\tDom:      1 << dom.min,\n\t\t\tMonth:    all(months),\n\t\t\tDow:      all(dow),\n\t\t\tLocation: loc,\n\t\t}, nil\n\n\tcase \"@weekly\":\n\t\treturn &SpecSchedule{\n\t\t\tSecond:   1 << seconds.min,\n\t\t\tMinute:   1 << minutes.min,\n\t\t\tHour:     1 << hours.min,\n\t\t\tDom:      all(dom),\n\t\t\tMonth:    all(months),\n\t\t\tDow:      1 << dow.min,\n\t\t\tLocation: loc,\n\t\t}, nil\n\n\tcase \"@daily\", \"@midnight\":\n\t\treturn &SpecSchedule{\n\t\t\tSecond:   1 << seconds.min,\n\t\t\tMinute:   1 << minutes.min,\n\t\t\tHour:     1 << hours.min,\n\t\t\tDom:      all(dom),\n\t\t\tMonth:    all(months),\n\t\t\tDow:      all(dow),\n\t\t\tLocation: loc,\n\t\t}, nil\n\n\tcase \"@hourly\":\n\t\treturn &SpecSchedule{\n\t\t\tSecond:   1 << seconds.min,\n\t\t\tMinute:   1 << minutes.min,\n\t\t\tHour:     all(hours),\n\t\t\tDom:      all(dom),\n\t\t\tMonth:    all(months),\n\t\t\tDow:      all(dow),\n\t\t\tLocation: loc,\n\t\t}, nil\n\tdefault:\n\t\t// Continue to check @every prefix below\n\t}\n\n\tconst every = \"@every \"\n\tif strings.HasPrefix(descriptor, every) {\n\t\tduration, err := time.ParseDuration(descriptor[len(every):])\n\t\tif err != nil {\n\t\t\treturn nil, errors.Wrap(err, \"failed to parse duration\")\n\t\t}\n\t\treturn Every(duration), nil\n\t}\n\n\treturn nil, errors.New(\"unrecognized descriptor: \" + descriptor)\n}\n"
  },
  {
    "path": "plugin/cron/parser_test.go",
    "content": "//nolint:all\npackage cron\n\nimport (\n\t\"reflect\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n)\n\nvar secondParser = NewParser(Second | Minute | Hour | Dom | Month | DowOptional | Descriptor)\n\nfunc TestRange(t *testing.T) {\n\tzero := uint64(0)\n\tranges := []struct {\n\t\texpr     string\n\t\tmin, max uint\n\t\texpected uint64\n\t\terr      string\n\t}{\n\t\t{\"5\", 0, 7, 1 << 5, \"\"},\n\t\t{\"0\", 0, 7, 1 << 0, \"\"},\n\t\t{\"7\", 0, 7, 1 << 7, \"\"},\n\n\t\t{\"5-5\", 0, 7, 1 << 5, \"\"},\n\t\t{\"5-6\", 0, 7, 1<<5 | 1<<6, \"\"},\n\t\t{\"5-7\", 0, 7, 1<<5 | 1<<6 | 1<<7, \"\"},\n\n\t\t{\"5-6/2\", 0, 7, 1 << 5, \"\"},\n\t\t{\"5-7/2\", 0, 7, 1<<5 | 1<<7, \"\"},\n\t\t{\"5-7/1\", 0, 7, 1<<5 | 1<<6 | 1<<7, \"\"},\n\n\t\t{\"*\", 1, 3, 1<<1 | 1<<2 | 1<<3 | starBit, \"\"},\n\t\t{\"*/2\", 1, 3, 1<<1 | 1<<3, \"\"},\n\n\t\t{\"5--5\", 0, 0, zero, \"too many hyphens\"},\n\t\t{\"jan-x\", 0, 0, zero, `failed to parse number: strconv.Atoi: parsing \"jan\": invalid syntax`},\n\t\t{\"2-x\", 1, 5, zero, `failed to parse number: strconv.Atoi: parsing \"x\": invalid syntax`},\n\t\t{\"*/-12\", 0, 0, zero, \"number must be positive\"},\n\t\t{\"*//2\", 0, 0, zero, \"too many slashes\"},\n\t\t{\"1\", 3, 5, zero, \"below minimum\"},\n\t\t{\"6\", 3, 5, zero, \"above maximum\"},\n\t\t{\"5-3\", 3, 5, zero, \"beginning of range after end: 5-3\"},\n\t\t{\"*/0\", 0, 0, zero, \"step cannot be zero: */0\"},\n\t}\n\n\tfor _, c := range ranges {\n\t\tactual, err := getRange(c.expr, bounds{c.min, c.max, nil})\n\t\tif len(c.err) != 0 && (err == nil || !strings.Contains(err.Error(), c.err)) {\n\t\t\tt.Errorf(\"%s => expected %v, got %v\", c.expr, c.err, err)\n\t\t}\n\t\tif len(c.err) == 0 && err != nil {\n\t\t\tt.Errorf(\"%s => unexpected error %v\", c.expr, err)\n\t\t}\n\t\tif actual != c.expected {\n\t\t\tt.Errorf(\"%s => expected %d, got %d\", c.expr, c.expected, actual)\n\t\t}\n\t}\n}\n\nfunc TestField(t *testing.T) {\n\tfields := []struct {\n\t\texpr     string\n\t\tmin, max uint\n\t\texpected uint64\n\t}{\n\t\t{\"5\", 1, 7, 1 << 5},\n\t\t{\"5,6\", 1, 7, 1<<5 | 1<<6},\n\t\t{\"5,6,7\", 1, 7, 1<<5 | 1<<6 | 1<<7},\n\t\t{\"1,5-7/2,3\", 1, 7, 1<<1 | 1<<5 | 1<<7 | 1<<3},\n\t}\n\n\tfor _, c := range fields {\n\t\tactual, _ := getField(c.expr, bounds{c.min, c.max, nil})\n\t\tif actual != c.expected {\n\t\t\tt.Errorf(\"%s => expected %d, got %d\", c.expr, c.expected, actual)\n\t\t}\n\t}\n}\n\nfunc TestAll(t *testing.T) {\n\tallBits := []struct {\n\t\tr        bounds\n\t\texpected uint64\n\t}{\n\t\t{minutes, 0xfffffffffffffff}, // 0-59: 60 ones\n\t\t{hours, 0xffffff},            // 0-23: 24 ones\n\t\t{dom, 0xfffffffe},            // 1-31: 31 ones, 1 zero\n\t\t{months, 0x1ffe},             // 1-12: 12 ones, 1 zero\n\t\t{dow, 0x7f},                  // 0-6: 7 ones\n\t}\n\n\tfor _, c := range allBits {\n\t\tactual := all(c.r) // all() adds the starBit, so compensate for that..\n\t\tif c.expected|starBit != actual {\n\t\t\tt.Errorf(\"%d-%d/%d => expected %b, got %b\",\n\t\t\t\tc.r.min, c.r.max, 1, c.expected|starBit, actual)\n\t\t}\n\t}\n}\n\nfunc TestBits(t *testing.T) {\n\tbits := []struct {\n\t\tmin, max, step uint\n\t\texpected       uint64\n\t}{\n\t\t{0, 0, 1, 0x1},\n\t\t{1, 1, 1, 0x2},\n\t\t{1, 5, 2, 0x2a}, // 101010\n\t\t{1, 4, 2, 0xa},  // 1010\n\t}\n\n\tfor _, c := range bits {\n\t\tactual := getBits(c.min, c.max, c.step)\n\t\tif c.expected != actual {\n\t\t\tt.Errorf(\"%d-%d/%d => expected %b, got %b\",\n\t\t\t\tc.min, c.max, c.step, c.expected, actual)\n\t\t}\n\t}\n}\n\nfunc TestParseScheduleErrors(t *testing.T) {\n\tvar tests = []struct{ expr, err string }{\n\t\t{\"* 5 j * * *\", `failed to parse number: strconv.Atoi: parsing \"j\": invalid syntax`},\n\t\t{\"@every Xm\", \"failed to parse duration\"},\n\t\t{\"@unrecognized\", \"unrecognized descriptor\"},\n\t\t{\"* * * *\", \"incorrect number of fields, expected 5-6\"},\n\t\t{\"\", \"empty spec string\"},\n\t}\n\tfor _, c := range tests {\n\t\tactual, err := secondParser.Parse(c.expr)\n\t\tif err == nil || !strings.Contains(err.Error(), c.err) {\n\t\t\tt.Errorf(\"%s => expected %v, got %v\", c.expr, c.err, err)\n\t\t}\n\t\tif actual != nil {\n\t\t\tt.Errorf(\"expected nil schedule on error, got %v\", actual)\n\t\t}\n\t}\n}\n\nfunc TestParseSchedule(t *testing.T) {\n\ttokyo, _ := time.LoadLocation(\"Asia/Tokyo\")\n\tentries := []struct {\n\t\tparser   Parser\n\t\texpr     string\n\t\texpected Schedule\n\t}{\n\t\t{secondParser, \"0 5 * * * *\", every5min(time.Local)},\n\t\t{standardParser, \"5 * * * *\", every5min(time.Local)},\n\t\t{secondParser, \"CRON_TZ=UTC  0 5 * * * *\", every5min(time.UTC)},\n\t\t{standardParser, \"CRON_TZ=UTC  5 * * * *\", every5min(time.UTC)},\n\t\t{secondParser, \"CRON_TZ=Asia/Tokyo 0 5 * * * *\", every5min(tokyo)},\n\t\t{secondParser, \"@every 5m\", ConstantDelaySchedule{5 * time.Minute}},\n\t\t{secondParser, \"@midnight\", midnight(time.Local)},\n\t\t{secondParser, \"TZ=UTC  @midnight\", midnight(time.UTC)},\n\t\t{secondParser, \"TZ=Asia/Tokyo @midnight\", midnight(tokyo)},\n\t\t{secondParser, \"@yearly\", annual(time.Local)},\n\t\t{secondParser, \"@annually\", annual(time.Local)},\n\t\t{\n\t\t\tparser: secondParser,\n\t\t\texpr:   \"* 5 * * * *\",\n\t\t\texpected: &SpecSchedule{\n\t\t\t\tSecond:   all(seconds),\n\t\t\t\tMinute:   1 << 5,\n\t\t\t\tHour:     all(hours),\n\t\t\t\tDom:      all(dom),\n\t\t\t\tMonth:    all(months),\n\t\t\t\tDow:      all(dow),\n\t\t\t\tLocation: time.Local,\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, c := range entries {\n\t\tactual, err := c.parser.Parse(c.expr)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"%s => unexpected error %v\", c.expr, err)\n\t\t}\n\t\tif !reflect.DeepEqual(actual, c.expected) {\n\t\t\tt.Errorf(\"%s => expected %b, got %b\", c.expr, c.expected, actual)\n\t\t}\n\t}\n}\n\nfunc TestOptionalSecondSchedule(t *testing.T) {\n\tparser := NewParser(SecondOptional | Minute | Hour | Dom | Month | Dow | Descriptor)\n\tentries := []struct {\n\t\texpr     string\n\t\texpected Schedule\n\t}{\n\t\t{\"0 5 * * * *\", every5min(time.Local)},\n\t\t{\"5 5 * * * *\", every5min5s(time.Local)},\n\t\t{\"5 * * * *\", every5min(time.Local)},\n\t}\n\n\tfor _, c := range entries {\n\t\tactual, err := parser.Parse(c.expr)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"%s => unexpected error %v\", c.expr, err)\n\t\t}\n\t\tif !reflect.DeepEqual(actual, c.expected) {\n\t\t\tt.Errorf(\"%s => expected %b, got %b\", c.expr, c.expected, actual)\n\t\t}\n\t}\n}\n\nfunc TestNormalizeFields(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    []string\n\t\toptions  ParseOption\n\t\texpected []string\n\t}{\n\t\t{\n\t\t\t\"AllFields_NoOptional\",\n\t\t\t[]string{\"0\", \"5\", \"*\", \"*\", \"*\", \"*\"},\n\t\t\tSecond | Minute | Hour | Dom | Month | Dow | Descriptor,\n\t\t\t[]string{\"0\", \"5\", \"*\", \"*\", \"*\", \"*\"},\n\t\t},\n\t\t{\n\t\t\t\"AllFields_SecondOptional_Provided\",\n\t\t\t[]string{\"0\", \"5\", \"*\", \"*\", \"*\", \"*\"},\n\t\t\tSecondOptional | Minute | Hour | Dom | Month | Dow | Descriptor,\n\t\t\t[]string{\"0\", \"5\", \"*\", \"*\", \"*\", \"*\"},\n\t\t},\n\t\t{\n\t\t\t\"AllFields_SecondOptional_NotProvided\",\n\t\t\t[]string{\"5\", \"*\", \"*\", \"*\", \"*\"},\n\t\t\tSecondOptional | Minute | Hour | Dom | Month | Dow | Descriptor,\n\t\t\t[]string{\"0\", \"5\", \"*\", \"*\", \"*\", \"*\"},\n\t\t},\n\t\t{\n\t\t\t\"SubsetFields_NoOptional\",\n\t\t\t[]string{\"5\", \"15\", \"*\"},\n\t\t\tHour | Dom | Month,\n\t\t\t[]string{\"0\", \"0\", \"5\", \"15\", \"*\", \"*\"},\n\t\t},\n\t\t{\n\t\t\t\"SubsetFields_DowOptional_Provided\",\n\t\t\t[]string{\"5\", \"15\", \"*\", \"4\"},\n\t\t\tHour | Dom | Month | DowOptional,\n\t\t\t[]string{\"0\", \"0\", \"5\", \"15\", \"*\", \"4\"},\n\t\t},\n\t\t{\n\t\t\t\"SubsetFields_DowOptional_NotProvided\",\n\t\t\t[]string{\"5\", \"15\", \"*\"},\n\t\t\tHour | Dom | Month | DowOptional,\n\t\t\t[]string{\"0\", \"0\", \"5\", \"15\", \"*\", \"*\"},\n\t\t},\n\t\t{\n\t\t\t\"SubsetFields_SecondOptional_NotProvided\",\n\t\t\t[]string{\"5\", \"15\", \"*\"},\n\t\t\tSecondOptional | Hour | Dom | Month,\n\t\t\t[]string{\"0\", \"0\", \"5\", \"15\", \"*\", \"*\"},\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(*testing.T) {\n\t\t\tactual, err := normalizeFields(test.input, test.options)\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"unexpected error: %v\", err)\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(actual, test.expected) {\n\t\t\t\tt.Errorf(\"expected %v, got %v\", test.expected, actual)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNormalizeFields_Errors(t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\tinput   []string\n\t\toptions ParseOption\n\t\terr     string\n\t}{\n\t\t{\n\t\t\t\"TwoOptionals\",\n\t\t\t[]string{\"0\", \"5\", \"*\", \"*\", \"*\", \"*\"},\n\t\t\tSecondOptional | Minute | Hour | Dom | Month | DowOptional,\n\t\t\t\"\",\n\t\t},\n\t\t{\n\t\t\t\"TooManyFields\",\n\t\t\t[]string{\"0\", \"5\", \"*\", \"*\"},\n\t\t\tSecondOptional | Minute | Hour,\n\t\t\t\"\",\n\t\t},\n\t\t{\n\t\t\t\"NoFields\",\n\t\t\t[]string{},\n\t\t\tSecondOptional | Minute | Hour,\n\t\t\t\"\",\n\t\t},\n\t\t{\n\t\t\t\"TooFewFields\",\n\t\t\t[]string{\"*\"},\n\t\t\tSecondOptional | Minute | Hour,\n\t\t\t\"\",\n\t\t},\n\t}\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(*testing.T) {\n\t\t\tactual, err := normalizeFields(test.input, test.options)\n\t\t\tif err == nil {\n\t\t\t\tt.Errorf(\"expected an error, got none. results: %v\", actual)\n\t\t\t}\n\t\t\tif !strings.Contains(err.Error(), test.err) {\n\t\t\t\tt.Errorf(\"expected error %q, got %q\", test.err, err.Error())\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestStandardSpecSchedule(t *testing.T) {\n\tentries := []struct {\n\t\texpr     string\n\t\texpected Schedule\n\t\terr      string\n\t}{\n\t\t{\n\t\t\texpr:     \"5 * * * *\",\n\t\t\texpected: &SpecSchedule{1 << seconds.min, 1 << 5, all(hours), all(dom), all(months), all(dow), time.Local},\n\t\t},\n\t\t{\n\t\t\texpr:     \"@every 5m\",\n\t\t\texpected: ConstantDelaySchedule{time.Duration(5) * time.Minute},\n\t\t},\n\t\t{\n\t\t\texpr: \"5 j * * *\",\n\t\t\terr:  `failed to parse number: strconv.Atoi: parsing \"j\": invalid syntax`,\n\t\t},\n\t\t{\n\t\t\texpr: \"* * * *\",\n\t\t\terr:  \"incorrect number of fields\",\n\t\t},\n\t}\n\n\tfor _, c := range entries {\n\t\tactual, err := ParseStandard(c.expr)\n\t\tif len(c.err) != 0 && (err == nil || !strings.Contains(err.Error(), c.err)) {\n\t\t\tt.Errorf(\"%s => expected %v, got %v\", c.expr, c.err, err)\n\t\t}\n\t\tif len(c.err) == 0 && err != nil {\n\t\t\tt.Errorf(\"%s => unexpected error %v\", c.expr, err)\n\t\t}\n\t\tif !reflect.DeepEqual(actual, c.expected) {\n\t\t\tt.Errorf(\"%s => expected %b, got %b\", c.expr, c.expected, actual)\n\t\t}\n\t}\n}\n\nfunc TestNoDescriptorParser(t *testing.T) {\n\tparser := NewParser(Minute | Hour)\n\t_, err := parser.Parse(\"@every 1m\")\n\tif err == nil {\n\t\tt.Error(\"expected an error, got none\")\n\t}\n}\n\nfunc every5min(loc *time.Location) *SpecSchedule {\n\treturn &SpecSchedule{1 << 0, 1 << 5, all(hours), all(dom), all(months), all(dow), loc}\n}\n\nfunc every5min5s(loc *time.Location) *SpecSchedule {\n\treturn &SpecSchedule{1 << 5, 1 << 5, all(hours), all(dom), all(months), all(dow), loc}\n}\n\nfunc midnight(loc *time.Location) *SpecSchedule {\n\treturn &SpecSchedule{1, 1, 1, all(dom), all(months), all(dow), loc}\n}\n\nfunc annual(loc *time.Location) *SpecSchedule {\n\treturn &SpecSchedule{\n\t\tSecond:   1 << seconds.min,\n\t\tMinute:   1 << minutes.min,\n\t\tHour:     1 << hours.min,\n\t\tDom:      1 << dom.min,\n\t\tMonth:    1 << months.min,\n\t\tDow:      all(dow),\n\t\tLocation: loc,\n\t}\n}\n"
  },
  {
    "path": "plugin/cron/spec.go",
    "content": "package cron\n\nimport \"time\"\n\n// SpecSchedule specifies a duty cycle (to the second granularity), based on a\n// traditional crontab specification. It is computed initially and stored as bit sets.\ntype SpecSchedule struct {\n\tSecond, Minute, Hour, Dom, Month, Dow uint64\n\n\t// Override location for this schedule.\n\tLocation *time.Location\n}\n\n// bounds provides a range of acceptable values (plus a map of name to value).\ntype bounds struct {\n\tmin, max uint\n\tnames    map[string]uint\n}\n\n// The bounds for each field.\nvar (\n\tseconds = bounds{0, 59, nil}\n\tminutes = bounds{0, 59, nil}\n\thours   = bounds{0, 23, nil}\n\tdom     = bounds{1, 31, nil}\n\tmonths  = bounds{1, 12, map[string]uint{\n\t\t\"jan\": 1,\n\t\t\"feb\": 2,\n\t\t\"mar\": 3,\n\t\t\"apr\": 4,\n\t\t\"may\": 5,\n\t\t\"jun\": 6,\n\t\t\"jul\": 7,\n\t\t\"aug\": 8,\n\t\t\"sep\": 9,\n\t\t\"oct\": 10,\n\t\t\"nov\": 11,\n\t\t\"dec\": 12,\n\t}}\n\tdow = bounds{0, 6, map[string]uint{\n\t\t\"sun\": 0,\n\t\t\"mon\": 1,\n\t\t\"tue\": 2,\n\t\t\"wed\": 3,\n\t\t\"thu\": 4,\n\t\t\"fri\": 5,\n\t\t\"sat\": 6,\n\t}}\n)\n\nconst (\n\t// Set the top bit if a star was included in the expression.\n\tstarBit = 1 << 63\n)\n\n// Next returns the next time this schedule is activated, greater than the given\n// time.  If no time can be found to satisfy the schedule, return the zero time.\nfunc (s *SpecSchedule) Next(t time.Time) time.Time {\n\t// General approach\n\t//\n\t// For Month, Day, Hour, Minute, Second:\n\t// Check if the time value matches.  If yes, continue to the next field.\n\t// If the field doesn't match the schedule, then increment the field until it matches.\n\t// While incrementing the field, a wrap-around brings it back to the beginning\n\t// of the field list (since it is necessary to re-verify previous field\n\t// values)\n\n\t// Convert the given time into the schedule's timezone, if one is specified.\n\t// Save the original timezone so we can convert back after we find a time.\n\t// Note that schedules without a time zone specified (time.Local) are treated\n\t// as local to the time provided.\n\torigLocation := t.Location()\n\tloc := s.Location\n\tif loc == time.Local {\n\t\tloc = t.Location()\n\t}\n\tif s.Location != time.Local {\n\t\tt = t.In(s.Location)\n\t}\n\n\t// Start at the earliest possible time (the upcoming second).\n\tt = t.Add(1*time.Second - time.Duration(t.Nanosecond())*time.Nanosecond)\n\n\t// This flag indicates whether a field has been incremented.\n\tadded := false\n\n\t// If no time is found within five years, return zero.\n\tyearLimit := t.Year() + 5\n\nWRAP:\n\tif t.Year() > yearLimit {\n\t\treturn time.Time{}\n\t}\n\n\t// Find the first applicable month.\n\t// If it's this month, then do nothing.\n\tfor 1<<uint(t.Month())&s.Month == 0 {\n\t\t// If we have to add a month, reset the other parts to 0.\n\t\tif !added {\n\t\t\tadded = true\n\t\t\t// Otherwise, set the date at the beginning (since the current time is irrelevant).\n\t\t\tt = time.Date(t.Year(), t.Month(), 1, 0, 0, 0, 0, loc)\n\t\t}\n\t\tt = t.AddDate(0, 1, 0)\n\n\t\t// Wrapped around.\n\t\tif t.Month() == time.January {\n\t\t\tgoto WRAP\n\t\t}\n\t}\n\n\t// Now get a day in that month.\n\t//\n\t// NOTE: This causes issues for daylight savings regimes where midnight does\n\t// not exist.  For example: Sao Paulo has DST that transforms midnight on\n\t// 11/3 into 1am. Handle that by noticing when the Hour ends up != 0.\n\tfor !dayMatches(s, t) {\n\t\tif !added {\n\t\t\tadded = true\n\t\t\tt = time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, loc)\n\t\t}\n\t\tt = t.AddDate(0, 0, 1)\n\t\t// Notice if the hour is no longer midnight due to DST.\n\t\t// Add an hour if it's 23, subtract an hour if it's 1.\n\t\tif t.Hour() != 0 {\n\t\t\tif t.Hour() > 12 {\n\t\t\t\tt = t.Add(time.Duration(24-t.Hour()) * time.Hour)\n\t\t\t} else {\n\t\t\t\tt = t.Add(time.Duration(-t.Hour()) * time.Hour)\n\t\t\t}\n\t\t}\n\n\t\tif t.Day() == 1 {\n\t\t\tgoto WRAP\n\t\t}\n\t}\n\n\tfor 1<<uint(t.Hour())&s.Hour == 0 {\n\t\tif !added {\n\t\t\tadded = true\n\t\t\tt = time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), 0, 0, 0, loc)\n\t\t}\n\t\tt = t.Add(1 * time.Hour)\n\n\t\tif t.Hour() == 0 {\n\t\t\tgoto WRAP\n\t\t}\n\t}\n\n\tfor 1<<uint(t.Minute())&s.Minute == 0 {\n\t\tif !added {\n\t\t\tadded = true\n\t\t\tt = t.Truncate(time.Minute)\n\t\t}\n\t\tt = t.Add(1 * time.Minute)\n\n\t\tif t.Minute() == 0 {\n\t\t\tgoto WRAP\n\t\t}\n\t}\n\n\tfor 1<<uint(t.Second())&s.Second == 0 {\n\t\tif !added {\n\t\t\tadded = true\n\t\t\tt = t.Truncate(time.Second)\n\t\t}\n\t\tt = t.Add(1 * time.Second)\n\n\t\tif t.Second() == 0 {\n\t\t\tgoto WRAP\n\t\t}\n\t}\n\n\treturn t.In(origLocation)\n}\n\n// dayMatches returns true if the schedule's day-of-week and day-of-month\n// restrictions are satisfied by the given time.\nfunc dayMatches(s *SpecSchedule, t time.Time) bool {\n\tvar (\n\t\tdomMatch = 1<<uint(t.Day())&s.Dom > 0\n\t\tdowMatch = 1<<uint(t.Weekday())&s.Dow > 0\n\t)\n\tif s.Dom&starBit > 0 || s.Dow&starBit > 0 {\n\t\treturn domMatch && dowMatch\n\t}\n\treturn domMatch || dowMatch\n}\n"
  },
  {
    "path": "plugin/cron/spec_test.go",
    "content": "//nolint:all\npackage cron\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n)\n\nfunc TestActivation(t *testing.T) {\n\ttests := []struct {\n\t\ttime, spec string\n\t\texpected   bool\n\t}{\n\t\t// Every fifteen minutes.\n\t\t{\"Mon Jul 9 15:00 2012\", \"0/15 * * * *\", true},\n\t\t{\"Mon Jul 9 15:45 2012\", \"0/15 * * * *\", true},\n\t\t{\"Mon Jul 9 15:40 2012\", \"0/15 * * * *\", false},\n\n\t\t// Every fifteen minutes, starting at 5 minutes.\n\t\t{\"Mon Jul 9 15:05 2012\", \"5/15 * * * *\", true},\n\t\t{\"Mon Jul 9 15:20 2012\", \"5/15 * * * *\", true},\n\t\t{\"Mon Jul 9 15:50 2012\", \"5/15 * * * *\", true},\n\n\t\t// Named months\n\t\t{\"Sun Jul 15 15:00 2012\", \"0/15 * * Jul *\", true},\n\t\t{\"Sun Jul 15 15:00 2012\", \"0/15 * * Jun *\", false},\n\n\t\t// Everything set.\n\t\t{\"Sun Jul 15 08:30 2012\", \"30 08 ? Jul Sun\", true},\n\t\t{\"Sun Jul 15 08:30 2012\", \"30 08 15 Jul ?\", true},\n\t\t{\"Mon Jul 16 08:30 2012\", \"30 08 ? Jul Sun\", false},\n\t\t{\"Mon Jul 16 08:30 2012\", \"30 08 15 Jul ?\", false},\n\n\t\t// Predefined schedules\n\t\t{\"Mon Jul 9 15:00 2012\", \"@hourly\", true},\n\t\t{\"Mon Jul 9 15:04 2012\", \"@hourly\", false},\n\t\t{\"Mon Jul 9 15:00 2012\", \"@daily\", false},\n\t\t{\"Mon Jul 9 00:00 2012\", \"@daily\", true},\n\t\t{\"Mon Jul 9 00:00 2012\", \"@weekly\", false},\n\t\t{\"Sun Jul 8 00:00 2012\", \"@weekly\", true},\n\t\t{\"Sun Jul 8 01:00 2012\", \"@weekly\", false},\n\t\t{\"Sun Jul 8 00:00 2012\", \"@monthly\", false},\n\t\t{\"Sun Jul 1 00:00 2012\", \"@monthly\", true},\n\n\t\t// Test interaction of DOW and DOM.\n\t\t// If both are restricted, then only one needs to match.\n\t\t{\"Sun Jul 15 00:00 2012\", \"* * 1,15 * Sun\", true},\n\t\t{\"Fri Jun 15 00:00 2012\", \"* * 1,15 * Sun\", true},\n\t\t{\"Wed Aug 1 00:00 2012\", \"* * 1,15 * Sun\", true},\n\t\t{\"Sun Jul 15 00:00 2012\", \"* * */10 * Sun\", true}, // verifies #70\n\n\t\t// However, if one has a star, then both need to match.\n\t\t{\"Sun Jul 15 00:00 2012\", \"* * * * Mon\", false},\n\t\t{\"Mon Jul 9 00:00 2012\", \"* * 1,15 * *\", false},\n\t\t{\"Sun Jul 15 00:00 2012\", \"* * 1,15 * *\", true},\n\t\t{\"Sun Jul 15 00:00 2012\", \"* * */2 * Sun\", true},\n\t}\n\n\tfor _, test := range tests {\n\t\tsched, err := ParseStandard(test.spec)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\t\tactual := sched.Next(getTime(test.time).Add(-1 * time.Second))\n\t\texpected := getTime(test.time)\n\t\tif test.expected && expected != actual || !test.expected && expected == actual {\n\t\t\tt.Errorf(\"Fail evaluating %s on %s: (expected) %s != %s (actual)\",\n\t\t\t\ttest.spec, test.time, expected, actual)\n\t\t}\n\t}\n}\n\nfunc TestNext(t *testing.T) {\n\truns := []struct {\n\t\ttime, spec string\n\t\texpected   string\n\t}{\n\t\t// Simple cases\n\t\t{\"Mon Jul 9 14:45 2012\", \"0 0/15 * * * *\", \"Mon Jul 9 15:00 2012\"},\n\t\t{\"Mon Jul 9 14:59 2012\", \"0 0/15 * * * *\", \"Mon Jul 9 15:00 2012\"},\n\t\t{\"Mon Jul 9 14:59:59 2012\", \"0 0/15 * * * *\", \"Mon Jul 9 15:00 2012\"},\n\n\t\t// Wrap around hours\n\t\t{\"Mon Jul 9 15:45 2012\", \"0 20-35/15 * * * *\", \"Mon Jul 9 16:20 2012\"},\n\n\t\t// Wrap around days\n\t\t{\"Mon Jul 9 23:46 2012\", \"0 */15 * * * *\", \"Tue Jul 10 00:00 2012\"},\n\t\t{\"Mon Jul 9 23:45 2012\", \"0 20-35/15 * * * *\", \"Tue Jul 10 00:20 2012\"},\n\t\t{\"Mon Jul 9 23:35:51 2012\", \"15/35 20-35/15 * * * *\", \"Tue Jul 10 00:20:15 2012\"},\n\t\t{\"Mon Jul 9 23:35:51 2012\", \"15/35 20-35/15 1/2 * * *\", \"Tue Jul 10 01:20:15 2012\"},\n\t\t{\"Mon Jul 9 23:35:51 2012\", \"15/35 20-35/15 10-12 * * *\", \"Tue Jul 10 10:20:15 2012\"},\n\n\t\t{\"Mon Jul 9 23:35:51 2012\", \"15/35 20-35/15 1/2 */2 * *\", \"Thu Jul 11 01:20:15 2012\"},\n\t\t{\"Mon Jul 9 23:35:51 2012\", \"15/35 20-35/15 * 9-20 * *\", \"Wed Jul 10 00:20:15 2012\"},\n\t\t{\"Mon Jul 9 23:35:51 2012\", \"15/35 20-35/15 * 9-20 Jul *\", \"Wed Jul 10 00:20:15 2012\"},\n\n\t\t// Wrap around months\n\t\t{\"Mon Jul 9 23:35 2012\", \"0 0 0 9 Apr-Oct ?\", \"Thu Aug 9 00:00 2012\"},\n\t\t{\"Mon Jul 9 23:35 2012\", \"0 0 0 */5 Apr,Aug,Oct Mon\", \"Tue Aug 1 00:00 2012\"},\n\t\t{\"Mon Jul 9 23:35 2012\", \"0 0 0 */5 Oct Mon\", \"Mon Oct 1 00:00 2012\"},\n\n\t\t// Wrap around years\n\t\t{\"Mon Jul 9 23:35 2012\", \"0 0 0 * Feb Mon\", \"Mon Feb 4 00:00 2013\"},\n\t\t{\"Mon Jul 9 23:35 2012\", \"0 0 0 * Feb Mon/2\", \"Fri Feb 1 00:00 2013\"},\n\n\t\t// Wrap around minute, hour, day, month, and year\n\t\t{\"Mon Dec 31 23:59:45 2012\", \"0 * * * * *\", \"Tue Jan 1 00:00:00 2013\"},\n\n\t\t// Leap year\n\t\t{\"Mon Jul 9 23:35 2012\", \"0 0 0 29 Feb ?\", \"Mon Feb 29 00:00 2016\"},\n\n\t\t// Daylight savings time 2am EST (-5) -> 3am EDT (-4)\n\t\t{\"2012-03-11T00:00:00-0500\", \"TZ=America/New_York 0 30 2 11 Mar ?\", \"2013-03-11T02:30:00-0400\"},\n\n\t\t// hourly job\n\t\t{\"2012-03-11T00:00:00-0500\", \"TZ=America/New_York 0 0 * * * ?\", \"2012-03-11T01:00:00-0500\"},\n\t\t{\"2012-03-11T01:00:00-0500\", \"TZ=America/New_York 0 0 * * * ?\", \"2012-03-11T03:00:00-0400\"},\n\t\t{\"2012-03-11T03:00:00-0400\", \"TZ=America/New_York 0 0 * * * ?\", \"2012-03-11T04:00:00-0400\"},\n\t\t{\"2012-03-11T04:00:00-0400\", \"TZ=America/New_York 0 0 * * * ?\", \"2012-03-11T05:00:00-0400\"},\n\n\t\t// hourly job using CRON_TZ\n\t\t{\"2012-03-11T00:00:00-0500\", \"CRON_TZ=America/New_York 0 0 * * * ?\", \"2012-03-11T01:00:00-0500\"},\n\t\t{\"2012-03-11T01:00:00-0500\", \"CRON_TZ=America/New_York 0 0 * * * ?\", \"2012-03-11T03:00:00-0400\"},\n\t\t{\"2012-03-11T03:00:00-0400\", \"CRON_TZ=America/New_York 0 0 * * * ?\", \"2012-03-11T04:00:00-0400\"},\n\t\t{\"2012-03-11T04:00:00-0400\", \"CRON_TZ=America/New_York 0 0 * * * ?\", \"2012-03-11T05:00:00-0400\"},\n\n\t\t// 1am nightly job\n\t\t{\"2012-03-11T00:00:00-0500\", \"TZ=America/New_York 0 0 1 * * ?\", \"2012-03-11T01:00:00-0500\"},\n\t\t{\"2012-03-11T01:00:00-0500\", \"TZ=America/New_York 0 0 1 * * ?\", \"2012-03-12T01:00:00-0400\"},\n\n\t\t// 2am nightly job (skipped)\n\t\t{\"2012-03-11T00:00:00-0500\", \"TZ=America/New_York 0 0 2 * * ?\", \"2012-03-12T02:00:00-0400\"},\n\n\t\t// Daylight savings time 2am EDT (-4) => 1am EST (-5)\n\t\t{\"2012-11-04T00:00:00-0400\", \"TZ=America/New_York 0 30 2 04 Nov ?\", \"2012-11-04T02:30:00-0500\"},\n\t\t{\"2012-11-04T01:45:00-0400\", \"TZ=America/New_York 0 30 1 04 Nov ?\", \"2012-11-04T01:30:00-0500\"},\n\n\t\t// hourly job\n\t\t{\"2012-11-04T00:00:00-0400\", \"TZ=America/New_York 0 0 * * * ?\", \"2012-11-04T01:00:00-0400\"},\n\t\t{\"2012-11-04T01:00:00-0400\", \"TZ=America/New_York 0 0 * * * ?\", \"2012-11-04T01:00:00-0500\"},\n\t\t{\"2012-11-04T01:00:00-0500\", \"TZ=America/New_York 0 0 * * * ?\", \"2012-11-04T02:00:00-0500\"},\n\n\t\t// 1am nightly job (runs twice)\n\t\t{\"2012-11-04T00:00:00-0400\", \"TZ=America/New_York 0 0 1 * * ?\", \"2012-11-04T01:00:00-0400\"},\n\t\t{\"2012-11-04T01:00:00-0400\", \"TZ=America/New_York 0 0 1 * * ?\", \"2012-11-04T01:00:00-0500\"},\n\t\t{\"2012-11-04T01:00:00-0500\", \"TZ=America/New_York 0 0 1 * * ?\", \"2012-11-05T01:00:00-0500\"},\n\n\t\t// 2am nightly job\n\t\t{\"2012-11-04T00:00:00-0400\", \"TZ=America/New_York 0 0 2 * * ?\", \"2012-11-04T02:00:00-0500\"},\n\t\t{\"2012-11-04T02:00:00-0500\", \"TZ=America/New_York 0 0 2 * * ?\", \"2012-11-05T02:00:00-0500\"},\n\n\t\t// 3am nightly job\n\t\t{\"2012-11-04T00:00:00-0400\", \"TZ=America/New_York 0 0 3 * * ?\", \"2012-11-04T03:00:00-0500\"},\n\t\t{\"2012-11-04T03:00:00-0500\", \"TZ=America/New_York 0 0 3 * * ?\", \"2012-11-05T03:00:00-0500\"},\n\n\t\t// hourly job\n\t\t{\"TZ=America/New_York 2012-11-04T00:00:00-0400\", \"0 0 * * * ?\", \"2012-11-04T01:00:00-0400\"},\n\t\t{\"TZ=America/New_York 2012-11-04T01:00:00-0400\", \"0 0 * * * ?\", \"2012-11-04T01:00:00-0500\"},\n\t\t{\"TZ=America/New_York 2012-11-04T01:00:00-0500\", \"0 0 * * * ?\", \"2012-11-04T02:00:00-0500\"},\n\n\t\t// 1am nightly job (runs twice)\n\t\t{\"TZ=America/New_York 2012-11-04T00:00:00-0400\", \"0 0 1 * * ?\", \"2012-11-04T01:00:00-0400\"},\n\t\t{\"TZ=America/New_York 2012-11-04T01:00:00-0400\", \"0 0 1 * * ?\", \"2012-11-04T01:00:00-0500\"},\n\t\t{\"TZ=America/New_York 2012-11-04T01:00:00-0500\", \"0 0 1 * * ?\", \"2012-11-05T01:00:00-0500\"},\n\n\t\t// 2am nightly job\n\t\t{\"TZ=America/New_York 2012-11-04T00:00:00-0400\", \"0 0 2 * * ?\", \"2012-11-04T02:00:00-0500\"},\n\t\t{\"TZ=America/New_York 2012-11-04T02:00:00-0500\", \"0 0 2 * * ?\", \"2012-11-05T02:00:00-0500\"},\n\n\t\t// 3am nightly job\n\t\t{\"TZ=America/New_York 2012-11-04T00:00:00-0400\", \"0 0 3 * * ?\", \"2012-11-04T03:00:00-0500\"},\n\t\t{\"TZ=America/New_York 2012-11-04T03:00:00-0500\", \"0 0 3 * * ?\", \"2012-11-05T03:00:00-0500\"},\n\n\t\t// Unsatisfiable\n\t\t{\"Mon Jul 9 23:35 2012\", \"0 0 0 30 Feb ?\", \"\"},\n\t\t{\"Mon Jul 9 23:35 2012\", \"0 0 0 31 Apr ?\", \"\"},\n\n\t\t// Monthly job\n\t\t{\"TZ=America/New_York 2012-11-04T00:00:00-0400\", \"0 0 3 3 * ?\", \"2012-12-03T03:00:00-0500\"},\n\n\t\t// Test the scenario of DST resulting in midnight not being a valid time.\n\t\t// https://github.com/robfig/cron/issues/157\n\t\t{\"2018-10-17T05:00:00-0400\", \"TZ=America/Sao_Paulo 0 0 9 10 * ?\", \"2018-11-10T06:00:00-0500\"},\n\t\t{\"2018-02-14T05:00:00-0500\", \"TZ=America/Sao_Paulo 0 0 9 22 * ?\", \"2018-02-22T07:00:00-0500\"},\n\t}\n\n\tfor _, c := range runs {\n\t\tsched, err := secondParser.Parse(c.spec)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\t\tactual := sched.Next(getTime(c.time))\n\t\texpected := getTime(c.expected)\n\t\tif !actual.Equal(expected) {\n\t\t\tt.Errorf(\"%s, \\\"%s\\\": (expected) %v != %v (actual)\", c.time, c.spec, expected, actual)\n\t\t}\n\t}\n}\n\nfunc TestErrors(t *testing.T) {\n\tinvalidSpecs := []string{\n\t\t\"xyz\",\n\t\t\"60 0 * * *\",\n\t\t\"0 60 * * *\",\n\t\t\"0 0 * * XYZ\",\n\t}\n\tfor _, spec := range invalidSpecs {\n\t\t_, err := ParseStandard(spec)\n\t\tif err == nil {\n\t\t\tt.Error(\"expected an error parsing: \", spec)\n\t\t}\n\t}\n}\n\nfunc getTime(value string) time.Time {\n\tif value == \"\" {\n\t\treturn time.Time{}\n\t}\n\n\tvar location = time.Local\n\tif strings.HasPrefix(value, \"TZ=\") {\n\t\tparts := strings.Fields(value)\n\t\tloc, err := time.LoadLocation(parts[0][len(\"TZ=\"):])\n\t\tif err != nil {\n\t\t\tpanic(\"could not parse location:\" + err.Error())\n\t\t}\n\t\tlocation = loc\n\t\tvalue = parts[1]\n\t}\n\n\tvar layouts = []string{\n\t\t\"Mon Jan 2 15:04 2006\",\n\t\t\"Mon Jan 2 15:04:05 2006\",\n\t}\n\tfor _, layout := range layouts {\n\t\tif t, err := time.ParseInLocation(layout, value, location); err == nil {\n\t\t\treturn t\n\t\t}\n\t}\n\tif t, err := time.ParseInLocation(\"2006-01-02T15:04:05-0700\", value, location); err == nil {\n\t\treturn t\n\t}\n\tpanic(\"could not parse time value \" + value)\n}\n\nfunc TestNextWithTz(t *testing.T) {\n\truns := []struct {\n\t\ttime, spec string\n\t\texpected   string\n\t}{\n\t\t// Failing tests\n\t\t{\"2016-01-03T13:09:03+0530\", \"14 14 * * *\", \"2016-01-03T14:14:00+0530\"},\n\t\t{\"2016-01-03T04:09:03+0530\", \"14 14 * * ?\", \"2016-01-03T14:14:00+0530\"},\n\n\t\t// Passing tests\n\t\t{\"2016-01-03T14:09:03+0530\", \"14 14 * * *\", \"2016-01-03T14:14:00+0530\"},\n\t\t{\"2016-01-03T14:00:00+0530\", \"14 14 * * ?\", \"2016-01-03T14:14:00+0530\"},\n\t}\n\tfor _, c := range runs {\n\t\tsched, err := ParseStandard(c.spec)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\t\tactual := sched.Next(getTimeTZ(c.time))\n\t\texpected := getTimeTZ(c.expected)\n\t\tif !actual.Equal(expected) {\n\t\t\tt.Errorf(\"%s, \\\"%s\\\": (expected) %v != %v (actual)\", c.time, c.spec, expected, actual)\n\t\t}\n\t}\n}\n\nfunc getTimeTZ(value string) time.Time {\n\tif value == \"\" {\n\t\treturn time.Time{}\n\t}\n\tt, err := time.Parse(\"Mon Jan 2 15:04 2006\", value)\n\tif err != nil {\n\t\tt, err = time.Parse(\"Mon Jan 2 15:04:05 2006\", value)\n\t\tif err != nil {\n\t\t\tt, err = time.Parse(\"2006-01-02T15:04:05-0700\", value)\n\t\t\tif err != nil {\n\t\t\t\tpanic(err)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn t\n}\n\n// https://github.com/robfig/cron/issues/144\nfunc TestSlash0NoHang(t *testing.T) {\n\tschedule := \"TZ=America/New_York 15/0 * * * *\"\n\t_, err := ParseStandard(schedule)\n\tif err == nil {\n\t\tt.Error(\"expected an error on 0 increment\")\n\t}\n}\n"
  },
  {
    "path": "plugin/email/README.md",
    "content": "# Email Plugin\n\nSMTP email sending functionality for self-hosted Memos instances.\n\n## Overview\n\nThis plugin provides a simple, reliable email sending interface following industry-standard SMTP protocols. It's designed for self-hosted environments where instance administrators configure their own email service, similar to platforms like GitHub, GitLab, and Discourse.\n\n## Features\n\n- Standard SMTP protocol support\n- TLS/STARTTLS and SSL/TLS encryption\n- HTML and plain text emails\n- Multiple recipients (To, Cc, Bcc)\n- Synchronous and asynchronous sending\n- Detailed error reporting with context\n- Works with all major email providers\n- Reply-To header support\n- RFC 5322 compliant message formatting\n\n## Quick Start\n\n### 1. Configure SMTP Settings\n\n```go\nimport \"github.com/usememos/memos/plugin/email\"\n\nconfig := &email.Config{\n    SMTPHost:     \"smtp.gmail.com\",\n    SMTPPort:     587,\n    SMTPUsername: \"your-email@gmail.com\",\n    SMTPPassword: \"your-app-password\",\n    FromEmail:    \"noreply@yourdomain.com\",\n    FromName:     \"Memos\",\n    UseTLS:       true,\n}\n```\n\n### 2. Create and Send Email\n\n```go\nmessage := &email.Message{\n    To:      []string{\"user@example.com\"},\n    Subject: \"Welcome to Memos!\",\n    Body:    \"Thanks for signing up.\",\n    IsHTML:  false,\n}\n\n// Synchronous send (waits for result)\nerr := email.Send(config, message)\nif err != nil {\n    log.Printf(\"Failed to send email: %v\", err)\n}\n\n// Asynchronous send (returns immediately)\nemail.SendAsync(config, message)\n```\n\n## Provider Configuration\n\n### Gmail\n\nRequires an [App Password](https://support.google.com/accounts/answer/185833) (2FA must be enabled):\n\n```go\nconfig := &email.Config{\n    SMTPHost:     \"smtp.gmail.com\",\n    SMTPPort:     587,\n    SMTPUsername: \"your-email@gmail.com\",\n    SMTPPassword: \"your-16-char-app-password\",\n    FromEmail:    \"your-email@gmail.com\",\n    FromName:     \"Memos\",\n    UseTLS:       true,\n}\n```\n\n**Alternative (SSL):**\n```go\nconfig := &email.Config{\n    SMTPHost:     \"smtp.gmail.com\",\n    SMTPPort:     465,\n    SMTPUsername: \"your-email@gmail.com\",\n    SMTPPassword: \"your-16-char-app-password\",\n    FromEmail:    \"your-email@gmail.com\",\n    FromName:     \"Memos\",\n    UseSSL:       true,\n}\n```\n\n### SendGrid\n\n```go\nconfig := &email.Config{\n    SMTPHost:     \"smtp.sendgrid.net\",\n    SMTPPort:     587,\n    SMTPUsername: \"apikey\",\n    SMTPPassword: \"your-sendgrid-api-key\",\n    FromEmail:    \"noreply@yourdomain.com\",\n    FromName:     \"Memos\",\n    UseTLS:       true,\n}\n```\n\n### AWS SES\n\n```go\nconfig := &email.Config{\n    SMTPHost:     \"email-smtp.us-east-1.amazonaws.com\",\n    SMTPPort:     587,\n    SMTPUsername: \"your-smtp-username\",\n    SMTPPassword: \"your-smtp-password\",\n    FromEmail:    \"verified@yourdomain.com\",\n    FromName:     \"Memos\",\n    UseTLS:       true,\n}\n```\n\n**Note:** Replace `us-east-1` with your AWS region. Email must be verified in SES.\n\n### Mailgun\n\n```go\nconfig := &email.Config{\n    SMTPHost:     \"smtp.mailgun.org\",\n    SMTPPort:     587,\n    SMTPUsername: \"postmaster@yourdomain.com\",\n    SMTPPassword: \"your-mailgun-smtp-password\",\n    FromEmail:    \"noreply@yourdomain.com\",\n    FromName:     \"Memos\",\n    UseTLS:       true,\n}\n```\n\n### Self-Hosted SMTP (Postfix, Exim, etc.)\n\n```go\nconfig := &email.Config{\n    SMTPHost:     \"mail.yourdomain.com\",\n    SMTPPort:     587,\n    SMTPUsername: \"username\",\n    SMTPPassword: \"password\",\n    FromEmail:    \"noreply@yourdomain.com\",\n    FromName:     \"Memos\",\n    UseTLS:       true,\n}\n```\n\n## HTML Emails\n\n```go\nmessage := &email.Message{\n    To:      []string{\"user@example.com\"},\n    Subject: \"Welcome to Memos!\",\n    Body: `\n<!DOCTYPE html>\n<html>\n<head>\n    <meta charset=\"UTF-8\">\n</head>\n<body style=\"font-family: Arial, sans-serif;\">\n    <h1 style=\"color: #333;\">Welcome to Memos!</h1>\n    <p>We're excited to have you on board.</p>\n    <a href=\"https://yourdomain.com\" style=\"background-color: #4CAF50; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px;\">Get Started</a>\n</body>\n</html>\n    `,\n    IsHTML: true,\n}\n\nemail.Send(config, message)\n```\n\n## Multiple Recipients\n\n```go\nmessage := &email.Message{\n    To:      []string{\"user1@example.com\", \"user2@example.com\"},\n    Cc:      []string{\"manager@example.com\"},\n    Bcc:     []string{\"admin@example.com\"},\n    Subject: \"Team Update\",\n    Body:    \"Important team announcement...\",\n    ReplyTo: \"support@yourdomain.com\",\n}\n\nemail.Send(config, message)\n```\n\n## Testing\n\n### Run Tests\n\n```bash\n# All tests\ngo test ./plugin/email/... -v\n\n# With coverage\ngo test ./plugin/email/... -v -cover\n\n# With race detector\ngo test ./plugin/email/... -race\n```\n\n### Manual Testing\n\nCreate a simple test program:\n\n```go\npackage main\n\nimport (\n    \"log\"\n    \"github.com/usememos/memos/plugin/email\"\n)\n\nfunc main() {\n    config := &email.Config{\n        SMTPHost:     \"smtp.gmail.com\",\n        SMTPPort:     587,\n        SMTPUsername: \"your-email@gmail.com\",\n        SMTPPassword: \"your-app-password\",\n        FromEmail:    \"your-email@gmail.com\",\n        FromName:     \"Test\",\n        UseTLS:       true,\n    }\n\n    message := &email.Message{\n        To:      []string{\"recipient@example.com\"},\n        Subject: \"Test Email\",\n        Body:    \"This is a test email from Memos email plugin.\",\n    }\n\n    if err := email.Send(config, message); err != nil {\n        log.Fatalf(\"Failed to send email: %v\", err)\n    }\n\n    log.Println(\"Email sent successfully!\")\n}\n```\n\n## Security Best Practices\n\n### 1. Use TLS/SSL Encryption\n\nAlways enable encryption in production:\n\n```go\n// STARTTLS (port 587) - Recommended\nconfig.UseTLS = true\n\n// SSL/TLS (port 465)\nconfig.UseSSL = true\n```\n\n### 2. Secure Credential Storage\n\nNever hardcode credentials. Use environment variables:\n\n```go\nimport \"os\"\n\nconfig := &email.Config{\n    SMTPHost:     os.Getenv(\"SMTP_HOST\"),\n    SMTPPort:     587,\n    SMTPUsername: os.Getenv(\"SMTP_USERNAME\"),\n    SMTPPassword: os.Getenv(\"SMTP_PASSWORD\"),\n    FromEmail:    os.Getenv(\"SMTP_FROM_EMAIL\"),\n    UseTLS:       true,\n}\n```\n\n### 3. Use App-Specific Passwords\n\nFor Gmail and similar services, use app passwords instead of your main account password.\n\n### 4. Validate and Sanitize Input\n\nAlways validate email addresses and sanitize content:\n\n```go\n// Validate before sending\nif err := message.Validate(); err != nil {\n    return err\n}\n```\n\n### 5. Implement Rate Limiting\n\nPrevent abuse by limiting email sending:\n\n```go\n// Example using golang.org/x/time/rate\nlimiter := rate.NewLimiter(rate.Every(time.Second), 10) // 10 emails per second\n\nif !limiter.Allow() {\n    return errors.New(\"rate limit exceeded\")\n}\n```\n\n### 6. Monitor and Log\n\nLog email sending activity for security monitoring:\n\n```go\nif err := email.Send(config, message); err != nil {\n    slog.Error(\"Email send failed\",\n        slog.String(\"recipient\", message.To[0]),\n        slog.Any(\"error\", err))\n}\n```\n\n## Common Ports\n\n| Port | Protocol | Security | Use Case |\n|------|----------|----------|----------|\n| **587** | SMTP + STARTTLS | Encrypted | **Recommended** for most providers |\n| **465** | SMTP over SSL/TLS | Encrypted | Alternative secure option |\n| **25** | SMTP | Unencrypted | Legacy, often blocked by ISPs |\n| **2525** | SMTP + STARTTLS | Encrypted | Alternative when 587 is blocked |\n\n**Port 587 (STARTTLS)** is the recommended standard for modern SMTP:\n```go\nconfig := &email.Config{\n    SMTPPort: 587,\n    UseTLS:   true,\n}\n```\n\n**Port 465 (SSL/TLS)** is the alternative:\n```go\nconfig := &email.Config{\n    SMTPPort: 465,\n    UseSSL:   true,\n}\n```\n\n## Error Handling\n\nThe package provides detailed, contextual errors:\n\n```go\nerr := email.Send(config, message)\nif err != nil {\n    // Error messages include context:\n    switch {\n    case strings.Contains(err.Error(), \"invalid email configuration\"):\n        // Configuration error (missing host, invalid port, etc.)\n        log.Printf(\"Configuration error: %v\", err)\n\n    case strings.Contains(err.Error(), \"invalid email message\"):\n        // Message validation error (missing recipients, subject, body)\n        log.Printf(\"Message error: %v\", err)\n\n    case strings.Contains(err.Error(), \"authentication failed\"):\n        // SMTP authentication failed (wrong credentials)\n        log.Printf(\"Auth error: %v\", err)\n\n    case strings.Contains(err.Error(), \"failed to connect\"):\n        // Network/connection error\n        log.Printf(\"Connection error: %v\", err)\n\n    case strings.Contains(err.Error(), \"recipient rejected\"):\n        // SMTP server rejected recipient\n        log.Printf(\"Recipient error: %v\", err)\n\n    default:\n        log.Printf(\"Unknown error: %v\", err)\n    }\n}\n```\n\n### Common Error Messages\n\n```\n❌ \"invalid email configuration: SMTP host is required\"\n   → Fix: Set config.SMTPHost\n\n❌ \"invalid email configuration: SMTP port must be between 1 and 65535\"\n   → Fix: Set valid config.SMTPPort (usually 587 or 465)\n\n❌ \"invalid email configuration: from email is required\"\n   → Fix: Set config.FromEmail\n\n❌ \"invalid email message: at least one recipient is required\"\n   → Fix: Set message.To with at least one email address\n\n❌ \"invalid email message: subject is required\"\n   → Fix: Set message.Subject\n\n❌ \"invalid email message: body is required\"\n   → Fix: Set message.Body\n\n❌ \"SMTP authentication failed\"\n   → Fix: Check credentials (username/password)\n\n❌ \"failed to connect to SMTP server\"\n   → Fix: Verify host/port, check firewall, ensure TLS/SSL settings match server\n```\n\n### Async Error Handling\n\nFor async sending, errors are logged automatically:\n\n```go\nemail.SendAsync(config, message)\n// Errors logged as:\n// [WARN] Failed to send email asynchronously recipients=user@example.com error=...\n```\n\n## Dependencies\n\n### Required\n\n- **Go 1.25+**\n- Standard library: `net/smtp`, `crypto/tls`\n- `github.com/pkg/errors` - Error wrapping with context\n\n### No External SMTP Libraries\n\nThis plugin uses Go's standard `net/smtp` library for maximum compatibility and minimal dependencies.\n\n## API Reference\n\n### Types\n\n#### `Config`\n```go\ntype Config struct {\n    SMTPHost     string // SMTP server hostname\n    SMTPPort     int    // SMTP server port\n    SMTPUsername string // SMTP auth username\n    SMTPPassword string // SMTP auth password\n    FromEmail    string // From email address\n    FromName     string // From display name (optional)\n    UseTLS       bool   // Enable STARTTLS (port 587)\n    UseSSL       bool   // Enable SSL/TLS (port 465)\n}\n```\n\n#### `Message`\n```go\ntype Message struct {\n    To      []string // Recipients\n    Cc      []string // CC recipients (optional)\n    Bcc     []string // BCC recipients (optional)\n    Subject string   // Email subject\n    Body    string   // Email body (plain text or HTML)\n    IsHTML  bool     // true for HTML, false for plain text\n    ReplyTo string   // Reply-To address (optional)\n}\n```\n\n### Functions\n\n#### `Send(config *Config, message *Message) error`\nSends an email synchronously. Blocks until email is sent or error occurs.\n\n#### `SendAsync(config *Config, message *Message)`\nSends an email asynchronously in a goroutine. Returns immediately. Errors are logged.\n\n#### `NewClient(config *Config) *Client`\nCreates a new SMTP client for advanced usage.\n\n#### `Client.Send(message *Message) error`\nSends email using the client's configuration.\n\n## Architecture\n\n```\nplugin/email/\n├── config.go       # SMTP configuration types\n├── message.go      # Email message types and formatting\n├── client.go       # SMTP client implementation\n├── email.go        # High-level Send/SendAsync API\n├── doc.go          # Package documentation\n└── *_test.go       # Unit tests\n```\n\n## License\n\nPart of the Memos project. See main repository for license details.\n\n## Contributing\n\nThis plugin follows the Memos contribution guidelines. Please ensure:\n\n1. All code is tested (TDD approach)\n2. Tests pass: `go test ./plugin/email/... -v`\n3. Code is formatted: `go fmt ./plugin/email/...`\n4. No linting errors: `golangci-lint run ./plugin/email/...`\n\n## Support\n\nFor issues and questions:\n\n- Memos GitHub Issues: https://github.com/usememos/memos/issues\n- Memos Documentation: https://usememos.com/docs\n\n## Roadmap\n\nFuture enhancements may include:\n\n- Email template system\n- Attachment support\n- Inline image embedding\n- Email queuing system\n- Delivery status tracking\n- Bounce handling\n"
  },
  {
    "path": "plugin/email/client.go",
    "content": "package email\n\nimport (\n\t\"crypto/tls\"\n\t\"net/smtp\"\n\n\t\"github.com/pkg/errors\"\n)\n\n// Client represents an SMTP email client.\ntype Client struct {\n\tconfig *Config\n}\n\n// NewClient creates a new email client with the given configuration.\nfunc NewClient(config *Config) *Client {\n\treturn &Client{\n\t\tconfig: config,\n\t}\n}\n\n// validateConfig validates the client configuration.\nfunc (c *Client) validateConfig() error {\n\tif c.config == nil {\n\t\treturn errors.New(\"email configuration is required\")\n\t}\n\treturn c.config.Validate()\n}\n\n// createAuth creates an SMTP auth mechanism if credentials are provided.\nfunc (c *Client) createAuth() smtp.Auth {\n\tif c.config.SMTPUsername == \"\" && c.config.SMTPPassword == \"\" {\n\t\treturn nil\n\t}\n\treturn smtp.PlainAuth(\"\", c.config.SMTPUsername, c.config.SMTPPassword, c.config.SMTPHost)\n}\n\n// createTLSConfig creates a TLS configuration for secure connections.\nfunc (c *Client) createTLSConfig() *tls.Config {\n\treturn &tls.Config{\n\t\tServerName: c.config.SMTPHost,\n\t\tMinVersion: tls.VersionTLS12,\n\t}\n}\n\n// Send sends an email message via SMTP.\nfunc (c *Client) Send(message *Message) error {\n\t// Validate configuration\n\tif err := c.validateConfig(); err != nil {\n\t\treturn errors.Wrap(err, \"invalid email configuration\")\n\t}\n\n\t// Validate message\n\tif message == nil {\n\t\treturn errors.New(\"message is required\")\n\t}\n\tif err := message.Validate(); err != nil {\n\t\treturn errors.Wrap(err, \"invalid email message\")\n\t}\n\n\t// Format the message\n\tbody := message.Format(c.config.FromEmail, c.config.FromName)\n\n\t// Get all recipients\n\trecipients := message.GetAllRecipients()\n\n\t// Create auth\n\tauth := c.createAuth()\n\n\t// Send based on encryption type\n\tif c.config.UseSSL {\n\t\treturn c.sendWithSSL(auth, recipients, body)\n\t}\n\treturn c.sendWithTLS(auth, recipients, body)\n}\n\n// sendWithTLS sends email using STARTTLS (port 587).\nfunc (c *Client) sendWithTLS(auth smtp.Auth, recipients []string, body string) error {\n\tserverAddr := c.config.GetServerAddress()\n\n\tif c.config.UseTLS {\n\t\t// Use STARTTLS\n\t\treturn smtp.SendMail(serverAddr, auth, c.config.FromEmail, recipients, []byte(body))\n\t}\n\n\t// Send without encryption (not recommended)\n\treturn smtp.SendMail(serverAddr, auth, c.config.FromEmail, recipients, []byte(body))\n}\n\n// sendWithSSL sends email using SSL/TLS (port 465).\nfunc (c *Client) sendWithSSL(auth smtp.Auth, recipients []string, body string) error {\n\tserverAddr := c.config.GetServerAddress()\n\n\t// Create TLS connection\n\ttlsConfig := c.createTLSConfig()\n\tconn, err := tls.Dial(\"tcp\", serverAddr, tlsConfig)\n\tif err != nil {\n\t\treturn errors.Wrapf(err, \"failed to connect to SMTP server with SSL: %s\", serverAddr)\n\t}\n\tdefer conn.Close()\n\n\t// Create SMTP client\n\tclient, err := smtp.NewClient(conn, c.config.SMTPHost)\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"failed to create SMTP client\")\n\t}\n\tdefer client.Quit()\n\n\t// Authenticate\n\tif auth != nil {\n\t\tif err := client.Auth(auth); err != nil {\n\t\t\treturn errors.Wrap(err, \"SMTP authentication failed\")\n\t\t}\n\t}\n\n\t// Set sender\n\tif err := client.Mail(c.config.FromEmail); err != nil {\n\t\treturn errors.Wrap(err, \"failed to set sender\")\n\t}\n\n\t// Set recipients\n\tfor _, recipient := range recipients {\n\t\tif err := client.Rcpt(recipient); err != nil {\n\t\t\treturn errors.Wrapf(err, \"failed to set recipient: %s\", recipient)\n\t\t}\n\t}\n\n\t// Send message body\n\twriter, err := client.Data()\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"failed to send DATA command\")\n\t}\n\n\tif _, err := writer.Write([]byte(body)); err != nil {\n\t\treturn errors.Wrap(err, \"failed to write message body\")\n\t}\n\n\tif err := writer.Close(); err != nil {\n\t\treturn errors.Wrap(err, \"failed to close message writer\")\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "plugin/email/client_test.go",
    "content": "package email\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestNewClient(t *testing.T) {\n\tconfig := &Config{\n\t\tSMTPHost:     \"smtp.example.com\",\n\t\tSMTPPort:     587,\n\t\tSMTPUsername: \"user@example.com\",\n\t\tSMTPPassword: \"password\",\n\t\tFromEmail:    \"noreply@example.com\",\n\t\tFromName:     \"Test App\",\n\t\tUseTLS:       true,\n\t}\n\n\tclient := NewClient(config)\n\n\tassert.NotNil(t, client)\n\tassert.Equal(t, config, client.config)\n}\n\nfunc TestClientValidateConfig(t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\tconfig  *Config\n\t\twantErr bool\n\t}{\n\t\t{\n\t\t\tname: \"valid config\",\n\t\t\tconfig: &Config{\n\t\t\t\tSMTPHost:  \"smtp.example.com\",\n\t\t\t\tSMTPPort:  587,\n\t\t\t\tFromEmail: \"test@example.com\",\n\t\t\t},\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"nil config\",\n\t\t\tconfig:  nil,\n\t\t\twantErr: true,\n\t\t},\n\t\t{\n\t\t\tname: \"invalid config\",\n\t\t\tconfig: &Config{\n\t\t\t\tSMTPHost: \"\",\n\t\t\t\tSMTPPort: 587,\n\t\t\t},\n\t\t\twantErr: true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tclient := NewClient(tt.config)\n\t\t\terr := client.validateConfig()\n\t\t\tif tt.wantErr {\n\t\t\t\tassert.Error(t, err)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestClientSendValidation(t *testing.T) {\n\tconfig := &Config{\n\t\tSMTPHost:  \"smtp.example.com\",\n\t\tSMTPPort:  587,\n\t\tFromEmail: \"test@example.com\",\n\t}\n\tclient := NewClient(config)\n\n\ttests := []struct {\n\t\tname    string\n\t\tmessage *Message\n\t\twantErr bool\n\t}{\n\t\t{\n\t\t\tname: \"valid message\",\n\t\t\tmessage: &Message{\n\t\t\t\tTo:      []string{\"recipient@example.com\"},\n\t\t\t\tSubject: \"Test\",\n\t\t\t\tBody:    \"Test body\",\n\t\t\t},\n\t\t\twantErr: false, // Will fail on actual send, but passes validation\n\t\t},\n\t\t{\n\t\t\tname:    \"nil message\",\n\t\t\tmessage: nil,\n\t\t\twantErr: true,\n\t\t},\n\t\t{\n\t\t\tname: \"invalid message\",\n\t\t\tmessage: &Message{\n\t\t\t\tTo:      []string{},\n\t\t\t\tSubject: \"Test\",\n\t\t\t\tBody:    \"Test\",\n\t\t\t},\n\t\t\twantErr: true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\terr := client.Send(tt.message)\n\t\t\t// We expect validation errors for invalid messages\n\t\t\t// For valid messages, we'll get connection errors (which is expected in tests)\n\t\t\tif tt.wantErr {\n\t\t\t\tassert.Error(t, err)\n\t\t\t\t// Should fail validation before attempting connection\n\t\t\t\tassert.NotContains(t, err.Error(), \"dial\")\n\t\t\t}\n\t\t\t// Note: We don't assert NoError for valid messages because\n\t\t\t// we don't have a real SMTP server in tests\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "plugin/email/config.go",
    "content": "package email\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/pkg/errors\"\n)\n\n// Config represents the SMTP configuration for email sending.\n// These settings should be provided by the self-hosted instance administrator.\ntype Config struct {\n\t// SMTPHost is the SMTP server hostname (e.g., \"smtp.gmail.com\")\n\tSMTPHost string\n\t// SMTPPort is the SMTP server port (common: 587 for TLS, 465 for SSL, 25 for unencrypted)\n\tSMTPPort int\n\t// SMTPUsername is the SMTP authentication username (usually the email address)\n\tSMTPUsername string\n\t// SMTPPassword is the SMTP authentication password or app-specific password\n\tSMTPPassword string\n\t// FromEmail is the email address that will appear in the \"From\" field\n\tFromEmail string\n\t// FromName is the display name that will appear in the \"From\" field\n\tFromName string\n\t// UseTLS enables STARTTLS encryption (recommended for port 587)\n\tUseTLS bool\n\t// UseSSL enables SSL/TLS encryption (for port 465)\n\tUseSSL bool\n}\n\n// Validate checks if the configuration is valid.\nfunc (c *Config) Validate() error {\n\tif c.SMTPHost == \"\" {\n\t\treturn errors.New(\"SMTP host is required\")\n\t}\n\tif c.SMTPPort <= 0 || c.SMTPPort > 65535 {\n\t\treturn errors.New(\"SMTP port must be between 1 and 65535\")\n\t}\n\tif c.FromEmail == \"\" {\n\t\treturn errors.New(\"from email is required\")\n\t}\n\treturn nil\n}\n\n// GetServerAddress returns the SMTP server address in the format \"host:port\".\nfunc (c *Config) GetServerAddress() string {\n\treturn fmt.Sprintf(\"%s:%d\", c.SMTPHost, c.SMTPPort)\n}\n"
  },
  {
    "path": "plugin/email/config_test.go",
    "content": "package email\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestConfigValidation(t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\tconfig  *Config\n\t\twantErr bool\n\t}{\n\t\t{\n\t\t\tname: \"valid config\",\n\t\t\tconfig: &Config{\n\t\t\t\tSMTPHost:     \"smtp.gmail.com\",\n\t\t\t\tSMTPPort:     587,\n\t\t\t\tSMTPUsername: \"user@example.com\",\n\t\t\t\tSMTPPassword: \"password\",\n\t\t\t\tFromEmail:    \"noreply@example.com\",\n\t\t\t\tFromName:     \"Memos\",\n\t\t\t},\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"missing host\",\n\t\t\tconfig: &Config{\n\t\t\t\tSMTPPort:     587,\n\t\t\t\tSMTPUsername: \"user@example.com\",\n\t\t\t\tSMTPPassword: \"password\",\n\t\t\t\tFromEmail:    \"noreply@example.com\",\n\t\t\t},\n\t\t\twantErr: true,\n\t\t},\n\t\t{\n\t\t\tname: \"invalid port\",\n\t\t\tconfig: &Config{\n\t\t\t\tSMTPHost:     \"smtp.gmail.com\",\n\t\t\t\tSMTPPort:     0,\n\t\t\t\tSMTPUsername: \"user@example.com\",\n\t\t\t\tSMTPPassword: \"password\",\n\t\t\t\tFromEmail:    \"noreply@example.com\",\n\t\t\t},\n\t\t\twantErr: true,\n\t\t},\n\t\t{\n\t\t\tname: \"missing from email\",\n\t\t\tconfig: &Config{\n\t\t\t\tSMTPHost:     \"smtp.gmail.com\",\n\t\t\t\tSMTPPort:     587,\n\t\t\t\tSMTPUsername: \"user@example.com\",\n\t\t\t\tSMTPPassword: \"password\",\n\t\t\t},\n\t\t\twantErr: true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\terr := tt.config.Validate()\n\t\t\tif tt.wantErr {\n\t\t\t\tassert.Error(t, err)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestConfigGetServerAddress(t *testing.T) {\n\tconfig := &Config{\n\t\tSMTPHost: \"smtp.gmail.com\",\n\t\tSMTPPort: 587,\n\t}\n\n\texpected := \"smtp.gmail.com:587\"\n\tassert.Equal(t, expected, config.GetServerAddress())\n}\n"
  },
  {
    "path": "plugin/email/doc.go",
    "content": "// Package email provides SMTP email sending functionality for self-hosted Memos instances.\n//\n// This package is designed for self-hosted environments where instance administrators\n// configure their own SMTP servers. It follows industry-standard patterns used by\n// platforms like GitHub, GitLab, and Discourse.\n//\n// # Configuration\n//\n// The package requires SMTP server configuration provided by the instance administrator:\n//\n//\tconfig := &email.Config{\n//\t    SMTPHost:     \"smtp.gmail.com\",\n//\t    SMTPPort:     587,\n//\t    SMTPUsername: \"your-email@gmail.com\",\n//\t    SMTPPassword: \"your-app-password\",\n//\t    FromEmail:    \"noreply@yourdomain.com\",\n//\t    FromName:     \"Memos Notifications\",\n//\t    UseTLS:       true,\n//\t}\n//\n// # Common SMTP Settings\n//\n// Gmail (requires App Password):\n//   - Host: smtp.gmail.com\n//   - Port: 587 (TLS) or 465 (SSL)\n//   - Username: your-email@gmail.com\n//   - UseTLS: true (for port 587) or UseSSL: true (for port 465)\n//\n// SendGrid:\n//   - Host: smtp.sendgrid.net\n//   - Port: 587\n//   - Username: apikey\n//   - Password: your-sendgrid-api-key\n//   - UseTLS: true\n//\n// AWS SES:\n//   - Host: email-smtp.[region].amazonaws.com\n//   - Port: 587\n//   - Username: your-smtp-username\n//   - Password: your-smtp-password\n//   - UseTLS: true\n//\n// Mailgun:\n//   - Host: smtp.mailgun.org\n//   - Port: 587\n//   - Username: your-mailgun-smtp-username\n//   - Password: your-mailgun-smtp-password\n//   - UseTLS: true\n//\n// # Sending Email\n//\n// Synchronous (waits for completion):\n//\n//\tmessage := &email.Message{\n//\t    To:      []string{\"user@example.com\"},\n//\t    Subject: \"Welcome to Memos\",\n//\t    Body:    \"Thank you for joining!\",\n//\t    IsHTML:  false,\n//\t}\n//\n//\terr := email.Send(config, message)\n//\tif err != nil {\n//\t    // Handle error\n//\t}\n//\n// Asynchronous (returns immediately):\n//\n//\temail.SendAsync(config, message)\n//\t// Errors are logged but not returned\n//\n// # HTML Email\n//\n//\tmessage := &email.Message{\n//\t    To:      []string{\"user@example.com\"},\n//\t    Subject: \"Welcome!\",\n//\t    Body:    \"<html><body><h1>Welcome to Memos!</h1></body></html>\",\n//\t    IsHTML:  true,\n//\t}\n//\n// # Security Considerations\n//\n//   - Always use TLS (port 587) or SSL (port 465) for production\n//   - Store SMTP credentials securely (environment variables or secrets management)\n//   - Use app-specific passwords for services like Gmail\n//   - Validate and sanitize email content to prevent injection attacks\n//   - Rate limit email sending to prevent abuse\n//\n// # Error Handling\n//\n// The package returns descriptive errors for common issues:\n//   - Configuration validation errors (missing host, invalid port, etc.)\n//   - Message validation errors (missing recipients, subject, or body)\n//   - Connection errors (cannot reach SMTP server)\n//   - Authentication errors (invalid credentials)\n//   - SMTP protocol errors (recipient rejected, etc.)\n//\n// All errors are wrapped with context using github.com/pkg/errors for better debugging.\npackage email\n"
  },
  {
    "path": "plugin/email/email.go",
    "content": "package email\n\nimport (\n\t\"log/slog\"\n\n\t\"github.com/pkg/errors\"\n)\n\n// Send sends an email synchronously.\n// Returns an error if the email fails to send.\nfunc Send(config *Config, message *Message) error {\n\tif config == nil {\n\t\treturn errors.New(\"email configuration is required\")\n\t}\n\tif message == nil {\n\t\treturn errors.New(\"email message is required\")\n\t}\n\n\tclient := NewClient(config)\n\treturn client.Send(message)\n}\n\n// SendAsync sends an email asynchronously.\n// It spawns a new goroutine to handle the sending and does not wait for the response.\n// Any errors are logged but not returned.\nfunc SendAsync(config *Config, message *Message) {\n\tgo func() {\n\t\tif err := Send(config, message); err != nil {\n\t\t\t// Since we're in a goroutine, we can only log the error\n\t\t\trecipients := \"\"\n\t\t\tif message != nil && len(message.To) > 0 {\n\t\t\t\trecipients = message.To[0]\n\t\t\t\tif len(message.To) > 1 {\n\t\t\t\t\trecipients += \" and others\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tslog.Warn(\"Failed to send email asynchronously\",\n\t\t\t\tslog.String(\"recipients\", recipients),\n\t\t\t\tslog.Any(\"error\", err))\n\t\t}\n\t}()\n}\n"
  },
  {
    "path": "plugin/email/email_test.go",
    "content": "package email\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"golang.org/x/sync/errgroup\"\n)\n\nfunc TestSend(t *testing.T) {\n\tconfig := &Config{\n\t\tSMTPHost:  \"smtp.example.com\",\n\t\tSMTPPort:  587,\n\t\tFromEmail: \"test@example.com\",\n\t}\n\n\tmessage := &Message{\n\t\tTo:      []string{\"recipient@example.com\"},\n\t\tSubject: \"Test\",\n\t\tBody:    \"Test body\",\n\t}\n\n\t// This will fail to connect (no real server), but should validate inputs\n\terr := Send(config, message)\n\t// We expect an error because there's no real SMTP server\n\t// But it should be a connection error, not a validation error\n\tassert.Error(t, err)\n\tassert.Contains(t, err.Error(), \"dial\")\n}\n\nfunc TestSendValidation(t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\tconfig  *Config\n\t\tmessage *Message\n\t\twantErr bool\n\t\terrMsg  string\n\t}{\n\t\t{\n\t\t\tname:    \"nil config\",\n\t\t\tconfig:  nil,\n\t\t\tmessage: &Message{To: []string{\"test@example.com\"}, Subject: \"Test\", Body: \"Test\"},\n\t\t\twantErr: true,\n\t\t\terrMsg:  \"configuration is required\",\n\t\t},\n\t\t{\n\t\t\tname:    \"nil message\",\n\t\t\tconfig:  &Config{SMTPHost: \"smtp.example.com\", SMTPPort: 587, FromEmail: \"from@example.com\"},\n\t\t\tmessage: nil,\n\t\t\twantErr: true,\n\t\t\terrMsg:  \"message is required\",\n\t\t},\n\t\t{\n\t\t\tname: \"invalid config\",\n\t\t\tconfig: &Config{\n\t\t\t\tSMTPHost: \"\",\n\t\t\t\tSMTPPort: 587,\n\t\t\t},\n\t\t\tmessage: &Message{To: []string{\"test@example.com\"}, Subject: \"Test\", Body: \"Test\"},\n\t\t\twantErr: true,\n\t\t\terrMsg:  \"invalid email configuration\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\terr := Send(tt.config, tt.message)\n\t\t\tif tt.wantErr {\n\t\t\t\tassert.Error(t, err)\n\t\t\t\tassert.Contains(t, err.Error(), tt.errMsg)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSendAsync(t *testing.T) {\n\tconfig := &Config{\n\t\tSMTPHost:  \"smtp.example.com\",\n\t\tSMTPPort:  587,\n\t\tFromEmail: \"test@example.com\",\n\t}\n\n\tmessage := &Message{\n\t\tTo:      []string{\"recipient@example.com\"},\n\t\tSubject: \"Test Async\",\n\t\tBody:    \"Test async body\",\n\t}\n\n\t// SendAsync should not block\n\tstart := time.Now()\n\tSendAsync(config, message)\n\tduration := time.Since(start)\n\n\t// Should return almost immediately (< 100ms)\n\tassert.Less(t, duration, 100*time.Millisecond)\n\n\t// Give goroutine time to start\n\ttime.Sleep(50 * time.Millisecond)\n}\n\nfunc TestSendAsyncConcurrent(t *testing.T) {\n\tconfig := &Config{\n\t\tSMTPHost:  \"smtp.example.com\",\n\t\tSMTPPort:  587,\n\t\tFromEmail: \"test@example.com\",\n\t}\n\n\tg := errgroup.Group{}\n\tcount := 5\n\n\tfor i := 0; i < count; i++ {\n\t\tg.Go(func() error {\n\t\t\tmessage := &Message{\n\t\t\t\tTo:      []string{\"recipient@example.com\"},\n\t\t\t\tSubject: \"Concurrent Test\",\n\t\t\t\tBody:    \"Test body\",\n\t\t\t}\n\t\t\tSendAsync(config, message)\n\t\t\treturn nil\n\t\t})\n\t}\n\n\tif err := g.Wait(); err != nil {\n\t\tt.Fatalf(\"SendAsync calls failed: %v\", err)\n\t}\n}\n"
  },
  {
    "path": "plugin/email/message.go",
    "content": "package email\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"strings\"\n\t\"time\"\n)\n\n// Message represents an email message to be sent.\ntype Message struct {\n\tTo      []string // Required: recipient email addresses\n\tCc      []string // Optional: carbon copy recipients\n\tBcc     []string // Optional: blind carbon copy recipients\n\tSubject string   // Required: email subject\n\tBody    string   // Required: email body content\n\tIsHTML  bool     // Whether the body is HTML (default: false for plain text)\n\tReplyTo string   // Optional: reply-to address\n}\n\n// Validate checks that the message has all required fields.\nfunc (m *Message) Validate() error {\n\tif len(m.To) == 0 {\n\t\treturn errors.New(\"at least one recipient is required\")\n\t}\n\tif m.Subject == \"\" {\n\t\treturn errors.New(\"subject is required\")\n\t}\n\tif m.Body == \"\" {\n\t\treturn errors.New(\"body is required\")\n\t}\n\treturn nil\n}\n\n// Format creates an RFC 5322 formatted email message.\nfunc (m *Message) Format(fromEmail, fromName string) string {\n\tvar sb strings.Builder\n\n\t// From header\n\tif fromName != \"\" {\n\t\tfmt.Fprintf(&sb, \"From: %s <%s>\\r\\n\", fromName, fromEmail)\n\t} else {\n\t\tfmt.Fprintf(&sb, \"From: %s\\r\\n\", fromEmail)\n\t}\n\n\t// To header\n\tfmt.Fprintf(&sb, \"To: %s\\r\\n\", strings.Join(m.To, \", \"))\n\n\t// Cc header (optional)\n\tif len(m.Cc) > 0 {\n\t\tfmt.Fprintf(&sb, \"Cc: %s\\r\\n\", strings.Join(m.Cc, \", \"))\n\t}\n\n\t// Reply-To header (optional)\n\tif m.ReplyTo != \"\" {\n\t\tfmt.Fprintf(&sb, \"Reply-To: %s\\r\\n\", m.ReplyTo)\n\t}\n\n\t// Subject header\n\tfmt.Fprintf(&sb, \"Subject: %s\\r\\n\", m.Subject)\n\n\t// Date header (RFC 5322 format)\n\tfmt.Fprintf(&sb, \"Date: %s\\r\\n\", time.Now().Format(time.RFC1123Z))\n\n\t// MIME headers\n\tsb.WriteString(\"MIME-Version: 1.0\\r\\n\")\n\n\t// Content-Type header\n\tif m.IsHTML {\n\t\tsb.WriteString(\"Content-Type: text/html; charset=utf-8\\r\\n\")\n\t} else {\n\t\tsb.WriteString(\"Content-Type: text/plain; charset=utf-8\\r\\n\")\n\t}\n\n\t// Empty line separating headers from body\n\tsb.WriteString(\"\\r\\n\")\n\n\t// Body\n\tsb.WriteString(m.Body)\n\n\treturn sb.String()\n}\n\n// GetAllRecipients returns all recipients (To, Cc, Bcc) as a single slice.\nfunc (m *Message) GetAllRecipients() []string {\n\tvar recipients []string\n\trecipients = append(recipients, m.To...)\n\trecipients = append(recipients, m.Cc...)\n\trecipients = append(recipients, m.Bcc...)\n\treturn recipients\n}\n"
  },
  {
    "path": "plugin/email/message_test.go",
    "content": "package email\n\nimport (\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc TestMessageValidation(t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\tmsg     Message\n\t\twantErr bool\n\t}{\n\t\t{\n\t\t\tname: \"valid message\",\n\t\t\tmsg: Message{\n\t\t\t\tTo:      []string{\"user@example.com\"},\n\t\t\t\tSubject: \"Test Subject\",\n\t\t\t\tBody:    \"Test Body\",\n\t\t\t},\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"no recipients\",\n\t\t\tmsg: Message{\n\t\t\t\tTo:      []string{},\n\t\t\t\tSubject: \"Test Subject\",\n\t\t\t\tBody:    \"Test Body\",\n\t\t\t},\n\t\t\twantErr: true,\n\t\t},\n\t\t{\n\t\t\tname: \"no subject\",\n\t\t\tmsg: Message{\n\t\t\t\tTo:      []string{\"user@example.com\"},\n\t\t\t\tSubject: \"\",\n\t\t\t\tBody:    \"Test Body\",\n\t\t\t},\n\t\t\twantErr: true,\n\t\t},\n\t\t{\n\t\t\tname: \"no body\",\n\t\t\tmsg: Message{\n\t\t\t\tTo:      []string{\"user@example.com\"},\n\t\t\t\tSubject: \"Test Subject\",\n\t\t\t\tBody:    \"\",\n\t\t\t},\n\t\t\twantErr: true,\n\t\t},\n\t\t{\n\t\t\tname: \"multiple recipients\",\n\t\t\tmsg: Message{\n\t\t\t\tTo:      []string{\"user1@example.com\", \"user2@example.com\"},\n\t\t\t\tCc:      []string{\"cc@example.com\"},\n\t\t\t\tSubject: \"Test Subject\",\n\t\t\t\tBody:    \"Test Body\",\n\t\t\t},\n\t\t\twantErr: false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\terr := tt.msg.Validate()\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"Validate() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestMessageFormatPlainText(t *testing.T) {\n\tmsg := Message{\n\t\tTo:      []string{\"user@example.com\"},\n\t\tSubject: \"Test Subject\",\n\t\tBody:    \"Test Body\",\n\t\tIsHTML:  false,\n\t}\n\n\tformatted := msg.Format(\"sender@example.com\", \"Sender Name\")\n\n\t// Check required headers\n\tif !strings.Contains(formatted, \"From: Sender Name <sender@example.com>\") {\n\t\tt.Error(\"Missing or incorrect From header\")\n\t}\n\tif !strings.Contains(formatted, \"To: user@example.com\") {\n\t\tt.Error(\"Missing or incorrect To header\")\n\t}\n\tif !strings.Contains(formatted, \"Subject: Test Subject\") {\n\t\tt.Error(\"Missing or incorrect Subject header\")\n\t}\n\tif !strings.Contains(formatted, \"Content-Type: text/plain; charset=utf-8\") {\n\t\tt.Error(\"Missing or incorrect Content-Type header for plain text\")\n\t}\n\tif !strings.Contains(formatted, \"Test Body\") {\n\t\tt.Error(\"Missing message body\")\n\t}\n}\n\nfunc TestMessageFormatHTML(t *testing.T) {\n\tmsg := Message{\n\t\tTo:      []string{\"user@example.com\"},\n\t\tSubject: \"Test Subject\",\n\t\tBody:    \"<html><body>Test Body</body></html>\",\n\t\tIsHTML:  true,\n\t}\n\n\tformatted := msg.Format(\"sender@example.com\", \"Sender Name\")\n\n\t// Check HTML content-type\n\tif !strings.Contains(formatted, \"Content-Type: text/html; charset=utf-8\") {\n\t\tt.Error(\"Missing or incorrect Content-Type header for HTML\")\n\t}\n\tif !strings.Contains(formatted, \"<html><body>Test Body</body></html>\") {\n\t\tt.Error(\"Missing HTML body\")\n\t}\n}\n\nfunc TestMessageFormatMultipleRecipients(t *testing.T) {\n\tmsg := Message{\n\t\tTo:      []string{\"user1@example.com\", \"user2@example.com\"},\n\t\tCc:      []string{\"cc1@example.com\", \"cc2@example.com\"},\n\t\tBcc:     []string{\"bcc@example.com\"},\n\t\tSubject: \"Test Subject\",\n\t\tBody:    \"Test Body\",\n\t\tReplyTo: \"reply@example.com\",\n\t}\n\n\tformatted := msg.Format(\"sender@example.com\", \"Sender Name\")\n\n\t// Check To header formatting\n\tif !strings.Contains(formatted, \"To: user1@example.com, user2@example.com\") {\n\t\tt.Error(\"Missing or incorrect To header with multiple recipients\")\n\t}\n\t// Check Cc header formatting\n\tif !strings.Contains(formatted, \"Cc: cc1@example.com, cc2@example.com\") {\n\t\tt.Error(\"Missing or incorrect Cc header\")\n\t}\n\t// Bcc should NOT appear in the formatted message\n\tif strings.Contains(formatted, \"Bcc:\") {\n\t\tt.Error(\"Bcc header should not appear in formatted message\")\n\t}\n\t// Check Reply-To header\n\tif !strings.Contains(formatted, \"Reply-To: reply@example.com\") {\n\t\tt.Error(\"Missing or incorrect Reply-To header\")\n\t}\n}\n\nfunc TestGetAllRecipients(t *testing.T) {\n\tmsg := Message{\n\t\tTo:  []string{\"user1@example.com\", \"user2@example.com\"},\n\t\tCc:  []string{\"cc@example.com\"},\n\t\tBcc: []string{\"bcc@example.com\"},\n\t}\n\n\trecipients := msg.GetAllRecipients()\n\n\t// Should have all 4 recipients\n\tif len(recipients) != 4 {\n\t\tt.Errorf(\"GetAllRecipients() returned %d recipients, want 4\", len(recipients))\n\t}\n\n\t// Check all recipients are present\n\texpectedRecipients := map[string]bool{\n\t\t\"user1@example.com\": true,\n\t\t\"user2@example.com\": true,\n\t\t\"cc@example.com\":    true,\n\t\t\"bcc@example.com\":   true,\n\t}\n\n\tfor _, recipient := range recipients {\n\t\tif !expectedRecipients[recipient] {\n\t\t\tt.Errorf(\"Unexpected recipient: %s\", recipient)\n\t\t}\n\t\tdelete(expectedRecipients, recipient)\n\t}\n\n\tif len(expectedRecipients) > 0 {\n\t\tt.Error(\"Not all expected recipients were returned\")\n\t}\n}\n"
  },
  {
    "path": "plugin/filter/MAINTENANCE.md",
    "content": "# Maintaining the Memo Filter Engine\n\nThe engine is memo-specific; any future field or behavior changes must stay\nconsistent with the memo schema and store implementations. Use this guide when\nextending or debugging the package.\n\n## Adding a New Memo Field\n\n1. **Update the schema**  \n   - Add the field entry in `schema.go`.  \n   - Define the backing column (`Column`), JSON path (if applicable), type, and\n     allowed operators.  \n   - Include the CEL variable in `EnvOptions`.\n2. **Adjust parser or renderer (if needed)**  \n   - For non-scalar fields (JSON booleans, lists), add handling in\n     `parser.go` or extend the renderer helpers.  \n   - Keep validation in the parser (e.g., reject unsupported operators).\n3. **Write a golden test**  \n   - Extend the dialect-specific memo filter tests under\n     `store/db/{sqlite,mysql,postgres}/memo_filter_test.go` with a case that\n     exercises the new field.\n4. **Run `go test ./...`** to ensure the SQL output matches expectations across\n   all dialects.\n\n## Supporting Dialect Nuances\n\n- Centralize differences inside `render.go`. If a new dialect-specific behavior\n  emerges (e.g., JSON operators), add the logic there rather than leaking it\n  into store code.\n- Use the renderer helpers (`jsonExtractExpr`, `jsonArrayExpr`, etc.) rather than\n  sprinkling ad-hoc SQL strings.\n- When placeholders change, adjust `addArg` so that argument numbering stays in\n  sync with store queries.\n\n## Debugging Tips\n\n- **Parser errors** – Most originate in `buildCondition` or schema validation.\n  Enable logging around `parser.go` when diagnosing unknown identifier/operator\n  messages.\n- **Renderer output** – Temporary printf/log statements in `renderCondition` help\n  identify which IR node produced unexpected SQL.\n- **Store integration** – Ensure drivers call `filter.DefaultEngine()` exactly once\n  per process; the singleton caches the parsed CEL environment.\n\n## Testing Checklist\n\n- `go test ./store/...` ensures all dialect tests consume the engine correctly.\n- Add targeted unit tests whenever new IR nodes or renderer paths are introduced.\n- When changing boolean or JSON handling, verify all three dialect test suites\n  (SQLite, MySQL, Postgres) to avoid regression.\n"
  },
  {
    "path": "plugin/filter/README.md",
    "content": "# Memo Filter Engine\n\nThis package houses the memo-only filter engine that turns CEL expressions into\nSQL fragments. The engine follows a three phase pipeline inspired by systems\nsuch as Calcite or Prisma:\n\n1. **Parsing** – CEL expressions are parsed with `cel-go` and validated against\n   the memo-specific environment declared in `schema.go`. Only fields that\n   exist in the schema can surface in the filter.\n2. **Normalization** – the raw CEL AST is converted into an intermediate\n   representation (IR) defined in `ir.go`. The IR is a dialect-agnostic tree of\n   conditions (logical operators, comparisons, list membership, etc.). This\n   step enforces schema rules (e.g. operator compatibility, type checks).\n3. **Rendering** – the renderer in `render.go` walks the IR and produces a SQL\n   fragment plus placeholder arguments tailored to a target dialect\n   (`sqlite`, `mysql`, or `postgres`). Dialect differences such as JSON access,\n   boolean semantics, placeholders, and `LIKE` vs `ILIKE` are encapsulated in\n   renderer helpers.\n\nThe entry point is `filter.DefaultEngine()` from `engine.go`. It lazily constructs\nan `Engine` configured with the memo schema and exposes:\n\n```go\nengine, _ := filter.DefaultEngine()\nstmt, _ := engine.CompileToStatement(ctx, `has_task_list && visibility == \"PUBLIC\"`, filter.RenderOptions{\n\tDialect: filter.DialectPostgres,\n})\n// stmt.SQL  -> \"((memo.payload->'property'->>'hasTaskList')::boolean IS TRUE AND memo.visibility = $1)\"\n// stmt.Args -> [\"PUBLIC\"]\n```\n\n## Core Files\n\n| File          | Responsibility                                                                  |\n| ------------- | ------------------------------------------------------------------------------- |\n| `schema.go`   | Declares memo fields, their types, backing columns, CEL environment options     |\n| `ir.go`       | IR node definitions used across the pipeline                                    |\n| `parser.go`   | Converts CEL `Expr` into IR while applying schema validation                    |\n| `render.go`   | Translates IR into SQL, handling dialect-specific behavior                      |\n| `engine.go`   | Glue between the phases; exposes `Compile`, `CompileToStatement`, and `DefaultEngine` |\n| `helpers.go`  | Convenience helpers for store integration (appending conditions)                |\n\n## SQL Generation Notes\n\n- **Placeholders** — `?` is used for SQLite/MySQL, `$n` for Postgres. The renderer\n  tracks offsets to compose queries with pre-existing arguments.\n- **JSON Fields** — Memo metadata lives in `memo.payload`. The renderer handles\n  `JSON_EXTRACT`/`json_extract`/`->`/`->>` variations and boolean coercion.\n- **Tag Operations** — `tag in [...]` and `\"tag\" in tags` become JSON array\n  predicates. SQLite uses `LIKE` patterns, MySQL uses `JSON_CONTAINS`, and\n  Postgres uses `@>`.\n- **Boolean Flags** — Fields such as `has_task_list` render as `IS TRUE` equality\n  checks, or comparisons against `CAST('true' AS JSON)` depending on the dialect.\n\n## Typical Integration\n\n1. Fetch the engine with `filter.DefaultEngine()`.\n2. Call `CompileToStatement` using the appropriate dialect enum.\n3. Append the emitted SQL fragment/args to the existing `WHERE` clause.\n4. Execute the resulting query through the store driver.\n\nThe `helpers.AppendConditions` helper encapsulates steps 2–3 when a driver needs\nto process an array of filters.\n"
  },
  {
    "path": "plugin/filter/engine.go",
    "content": "package filter\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/google/cel-go/cel\"\n\t\"github.com/pkg/errors\"\n)\n\n// Engine parses CEL filters into a dialect-agnostic condition tree.\ntype Engine struct {\n\tschema Schema\n\tenv    *cel.Env\n}\n\n// NewEngine builds a new Engine for the provided schema.\nfunc NewEngine(schema Schema) (*Engine, error) {\n\tenv, err := cel.NewEnv(schema.EnvOptions...)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"failed to create CEL environment\")\n\t}\n\treturn &Engine{\n\t\tschema: schema,\n\t\tenv:    env,\n\t}, nil\n}\n\n// Program stores a compiled filter condition.\ntype Program struct {\n\tschema    Schema\n\tcondition Condition\n}\n\n// ConditionTree exposes the underlying condition tree.\nfunc (p *Program) ConditionTree() Condition {\n\treturn p.condition\n}\n\n// Compile parses the filter string into an executable program.\nfunc (e *Engine) Compile(_ context.Context, filter string) (*Program, error) {\n\tif strings.TrimSpace(filter) == \"\" {\n\t\treturn nil, errors.New(\"filter expression is empty\")\n\t}\n\n\tfilter = normalizeLegacyFilter(filter)\n\n\tast, issues := e.env.Compile(filter)\n\tif issues != nil && issues.Err() != nil {\n\t\treturn nil, errors.Wrap(issues.Err(), \"failed to compile filter\")\n\t}\n\tparsed, err := cel.AstToParsedExpr(ast)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"failed to convert AST\")\n\t}\n\n\tcond, err := buildCondition(parsed.GetExpr(), e.schema)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &Program{\n\t\tschema:    e.schema,\n\t\tcondition: cond,\n\t}, nil\n}\n\n// CompileToStatement compiles and renders the filter in a single step.\nfunc (e *Engine) CompileToStatement(ctx context.Context, filter string, opts RenderOptions) (Statement, error) {\n\tprogram, err := e.Compile(ctx, filter)\n\tif err != nil {\n\t\treturn Statement{}, err\n\t}\n\treturn program.Render(opts)\n}\n\n// RenderOptions configure SQL rendering.\ntype RenderOptions struct {\n\tDialect           DialectName\n\tPlaceholderOffset int\n\tDisableNullChecks bool\n}\n\n// Statement contains the rendered SQL fragment and its args.\ntype Statement struct {\n\tSQL  string\n\tArgs []any\n}\n\n// Render converts the program into a dialect-specific SQL fragment.\nfunc (p *Program) Render(opts RenderOptions) (Statement, error) {\n\trenderer := newRenderer(p.schema, opts)\n\treturn renderer.Render(p.condition)\n}\n\nvar (\n\tdefaultOnce           sync.Once\n\tdefaultInst           *Engine\n\tdefaultErr            error\n\tdefaultAttachmentOnce sync.Once\n\tdefaultAttachmentInst *Engine\n\tdefaultAttachmentErr  error\n)\n\n// DefaultEngine returns the process-wide memo filter engine.\nfunc DefaultEngine() (*Engine, error) {\n\tdefaultOnce.Do(func() {\n\t\tdefaultInst, defaultErr = NewEngine(NewSchema())\n\t})\n\treturn defaultInst, defaultErr\n}\n\n// DefaultAttachmentEngine returns the process-wide attachment filter engine.\nfunc DefaultAttachmentEngine() (*Engine, error) {\n\tdefaultAttachmentOnce.Do(func() {\n\t\tdefaultAttachmentInst, defaultAttachmentErr = NewEngine(NewAttachmentSchema())\n\t})\n\treturn defaultAttachmentInst, defaultAttachmentErr\n}\n\nfunc normalizeLegacyFilter(expr string) string {\n\texpr = rewriteNumericLogicalOperand(expr, \"&&\")\n\texpr = rewriteNumericLogicalOperand(expr, \"||\")\n\treturn expr\n}\n\nfunc rewriteNumericLogicalOperand(expr, op string) string {\n\tvar builder strings.Builder\n\tn := len(expr)\n\ti := 0\n\tvar inQuote rune\n\n\tfor i < n {\n\t\tch := expr[i]\n\n\t\tif inQuote != 0 {\n\t\t\tbuilder.WriteByte(ch)\n\t\t\tif ch == '\\\\' && i+1 < n {\n\t\t\t\tbuilder.WriteByte(expr[i+1])\n\t\t\t\ti += 2\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif ch == byte(inQuote) {\n\t\t\t\tinQuote = 0\n\t\t\t}\n\t\t\ti++\n\t\t\tcontinue\n\t\t}\n\n\t\tif ch == '\\'' || ch == '\"' {\n\t\t\tinQuote = rune(ch)\n\t\t\tbuilder.WriteByte(ch)\n\t\t\ti++\n\t\t\tcontinue\n\t\t}\n\n\t\tif strings.HasPrefix(expr[i:], op) {\n\t\t\tbuilder.WriteString(op)\n\t\t\ti += len(op)\n\n\t\t\t// Preserve whitespace following the operator.\n\t\t\twsStart := i\n\t\t\tfor i < n && (expr[i] == ' ' || expr[i] == '\\t') {\n\t\t\t\ti++\n\t\t\t}\n\t\t\tbuilder.WriteString(expr[wsStart:i])\n\n\t\t\tsignStart := i\n\t\t\tif i < n && (expr[i] == '+' || expr[i] == '-') {\n\t\t\t\ti++\n\t\t\t}\n\t\t\tfor i < n && expr[i] >= '0' && expr[i] <= '9' {\n\t\t\t\ti++\n\t\t\t}\n\t\t\tif i > signStart {\n\t\t\t\tnumLiteral := expr[signStart:i]\n\t\t\t\tfmt.Fprintf(&builder, \"(%s != 0)\", numLiteral)\n\t\t\t} else {\n\t\t\t\tbuilder.WriteString(expr[signStart:i])\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tbuilder.WriteByte(ch)\n\t\ti++\n\t}\n\n\treturn builder.String()\n}\n"
  },
  {
    "path": "plugin/filter/helpers.go",
    "content": "package filter\n\nimport (\n\t\"context\"\n\t\"fmt\"\n)\n\n// AppendConditions compiles the provided filters and appends the resulting SQL fragments and args.\nfunc AppendConditions(ctx context.Context, engine *Engine, filters []string, dialect DialectName, where *[]string, args *[]any) error {\n\tfor _, filterStr := range filters {\n\t\tstmt, err := engine.CompileToStatement(ctx, filterStr, RenderOptions{\n\t\t\tDialect:           dialect,\n\t\t\tPlaceholderOffset: len(*args),\n\t\t})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif stmt.SQL == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\t*where = append(*where, fmt.Sprintf(\"(%s)\", stmt.SQL))\n\t\t*args = append(*args, stmt.Args...)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "plugin/filter/ir.go",
    "content": "package filter\n\n// Condition represents a boolean expression derived from the CEL filter.\ntype Condition interface {\n\tisCondition()\n}\n\n// LogicalOperator enumerates the supported logical operators.\ntype LogicalOperator string\n\nconst (\n\tLogicalAnd LogicalOperator = \"AND\"\n\tLogicalOr  LogicalOperator = \"OR\"\n)\n\n// LogicalCondition composes two conditions with a logical operator.\ntype LogicalCondition struct {\n\tOperator LogicalOperator\n\tLeft     Condition\n\tRight    Condition\n}\n\nfunc (*LogicalCondition) isCondition() {}\n\n// NotCondition negates a child condition.\ntype NotCondition struct {\n\tExpr Condition\n}\n\nfunc (*NotCondition) isCondition() {}\n\n// FieldPredicateCondition asserts that a field evaluates to true.\ntype FieldPredicateCondition struct {\n\tField string\n}\n\nfunc (*FieldPredicateCondition) isCondition() {}\n\n// ComparisonOperator lists supported comparison operators.\ntype ComparisonOperator string\n\nconst (\n\tCompareEq  ComparisonOperator = \"=\"\n\tCompareNeq ComparisonOperator = \"!=\"\n\tCompareLt  ComparisonOperator = \"<\"\n\tCompareLte ComparisonOperator = \"<=\"\n\tCompareGt  ComparisonOperator = \">\"\n\tCompareGte ComparisonOperator = \">=\"\n)\n\n// ComparisonCondition represents a binary comparison.\ntype ComparisonCondition struct {\n\tLeft     ValueExpr\n\tOperator ComparisonOperator\n\tRight    ValueExpr\n}\n\nfunc (*ComparisonCondition) isCondition() {}\n\n// InCondition represents an IN predicate with literal list values.\ntype InCondition struct {\n\tLeft   ValueExpr\n\tValues []ValueExpr\n}\n\nfunc (*InCondition) isCondition() {}\n\n// ElementInCondition represents the CEL syntax `\"value\" in field`.\ntype ElementInCondition struct {\n\tElement ValueExpr\n\tField   string\n}\n\nfunc (*ElementInCondition) isCondition() {}\n\n// ContainsCondition models the <field>.contains(<value>) call.\ntype ContainsCondition struct {\n\tField string\n\tValue string\n}\n\nfunc (*ContainsCondition) isCondition() {}\n\n// ConstantCondition captures a literal boolean outcome.\ntype ConstantCondition struct {\n\tValue bool\n}\n\nfunc (*ConstantCondition) isCondition() {}\n\n// ValueExpr models arithmetic or scalar expressions whose result feeds a comparison.\ntype ValueExpr interface {\n\tisValueExpr()\n}\n\n// FieldRef references a named schema field.\ntype FieldRef struct {\n\tName string\n}\n\nfunc (*FieldRef) isValueExpr() {}\n\n// LiteralValue holds a literal scalar.\ntype LiteralValue struct {\n\tValue interface{}\n}\n\nfunc (*LiteralValue) isValueExpr() {}\n\n// FunctionValue captures simple function calls like size(tags).\ntype FunctionValue struct {\n\tName string\n\tArgs []ValueExpr\n}\n\nfunc (*FunctionValue) isValueExpr() {}\n\n// ListComprehensionCondition represents CEL macros like exists(), all(), filter().\ntype ListComprehensionCondition struct {\n\tKind      ComprehensionKind\n\tField     string        // The list field to iterate over (e.g., \"tags\")\n\tIterVar   string        // The iteration variable name (e.g., \"t\")\n\tPredicate PredicateExpr // The predicate to evaluate for each element\n}\n\nfunc (*ListComprehensionCondition) isCondition() {}\n\n// ComprehensionKind enumerates the types of list comprehensions.\ntype ComprehensionKind string\n\nconst (\n\tComprehensionExists ComprehensionKind = \"exists\"\n)\n\n// PredicateExpr represents predicates used in comprehensions.\ntype PredicateExpr interface {\n\tisPredicateExpr()\n}\n\n// StartsWithPredicate represents t.startsWith(\"prefix\").\ntype StartsWithPredicate struct {\n\tPrefix string\n}\n\nfunc (*StartsWithPredicate) isPredicateExpr() {}\n\n// EndsWithPredicate represents t.endsWith(\"suffix\").\ntype EndsWithPredicate struct {\n\tSuffix string\n}\n\nfunc (*EndsWithPredicate) isPredicateExpr() {}\n\n// ContainsPredicate represents t.contains(\"substring\").\ntype ContainsPredicate struct {\n\tSubstring string\n}\n\nfunc (*ContainsPredicate) isPredicateExpr() {}\n"
  },
  {
    "path": "plugin/filter/parser.go",
    "content": "package filter\n\nimport (\n\t\"time\"\n\n\t\"github.com/pkg/errors\"\n\texprv1 \"google.golang.org/genproto/googleapis/api/expr/v1alpha1\"\n)\n\nfunc buildCondition(expr *exprv1.Expr, schema Schema) (Condition, error) {\n\tswitch v := expr.ExprKind.(type) {\n\tcase *exprv1.Expr_CallExpr:\n\t\treturn buildCallCondition(v.CallExpr, schema)\n\tcase *exprv1.Expr_ConstExpr:\n\t\tval, err := getConstValue(expr)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tswitch v := val.(type) {\n\t\tcase bool:\n\t\t\treturn &ConstantCondition{Value: v}, nil\n\t\tcase int64:\n\t\t\treturn &ConstantCondition{Value: v != 0}, nil\n\t\tcase float64:\n\t\t\treturn &ConstantCondition{Value: v != 0}, nil\n\t\tdefault:\n\t\t\treturn nil, errors.New(\"filter must evaluate to a boolean value\")\n\t\t}\n\tcase *exprv1.Expr_IdentExpr:\n\t\tname := v.IdentExpr.GetName()\n\t\tfield, ok := schema.Field(name)\n\t\tif !ok {\n\t\t\treturn nil, errors.Errorf(\"unknown identifier %q\", name)\n\t\t}\n\t\tif field.Type != FieldTypeBool {\n\t\t\treturn nil, errors.Errorf(\"identifier %q is not boolean\", name)\n\t\t}\n\t\treturn &FieldPredicateCondition{Field: name}, nil\n\tcase *exprv1.Expr_ComprehensionExpr:\n\t\treturn buildComprehensionCondition(v.ComprehensionExpr, schema)\n\tdefault:\n\t\treturn nil, errors.New(\"unsupported top-level expression\")\n\t}\n}\n\nfunc buildCallCondition(call *exprv1.Expr_Call, schema Schema) (Condition, error) {\n\tswitch call.Function {\n\tcase \"_&&_\":\n\t\tif len(call.Args) != 2 {\n\t\t\treturn nil, errors.New(\"logical AND expects two arguments\")\n\t\t}\n\t\tleft, err := buildCondition(call.Args[0], schema)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tright, err := buildCondition(call.Args[1], schema)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn &LogicalCondition{\n\t\t\tOperator: LogicalAnd,\n\t\t\tLeft:     left,\n\t\t\tRight:    right,\n\t\t}, nil\n\tcase \"_||_\":\n\t\tif len(call.Args) != 2 {\n\t\t\treturn nil, errors.New(\"logical OR expects two arguments\")\n\t\t}\n\t\tleft, err := buildCondition(call.Args[0], schema)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tright, err := buildCondition(call.Args[1], schema)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn &LogicalCondition{\n\t\t\tOperator: LogicalOr,\n\t\t\tLeft:     left,\n\t\t\tRight:    right,\n\t\t}, nil\n\tcase \"!_\":\n\t\tif len(call.Args) != 1 {\n\t\t\treturn nil, errors.New(\"logical NOT expects one argument\")\n\t\t}\n\t\tchild, err := buildCondition(call.Args[0], schema)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn &NotCondition{Expr: child}, nil\n\tcase \"_==_\", \"_!=_\", \"_<_\", \"_>_\", \"_<=_\", \"_>=_\":\n\t\treturn buildComparisonCondition(call, schema)\n\tcase \"@in\":\n\t\treturn buildInCondition(call, schema)\n\tcase \"contains\":\n\t\treturn buildContainsCondition(call, schema)\n\tdefault:\n\t\tval, ok, err := evaluateBool(call)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif ok {\n\t\t\treturn &ConstantCondition{Value: val}, nil\n\t\t}\n\t\treturn nil, errors.Errorf(\"unsupported call expression %q\", call.Function)\n\t}\n}\n\nfunc buildComparisonCondition(call *exprv1.Expr_Call, schema Schema) (Condition, error) {\n\tif len(call.Args) != 2 {\n\t\treturn nil, errors.New(\"comparison expects two arguments\")\n\t}\n\top, err := toComparisonOperator(call.Function)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tleft, err := buildValueExpr(call.Args[0], schema)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tright, err := buildValueExpr(call.Args[1], schema)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// If the left side is a field, validate allowed operators.\n\tif field, ok := left.(*FieldRef); ok {\n\t\tdef, exists := schema.Field(field.Name)\n\t\tif !exists {\n\t\t\treturn nil, errors.Errorf(\"unknown identifier %q\", field.Name)\n\t\t}\n\t\tif def.Kind == FieldKindVirtualAlias {\n\t\t\tdef, exists = schema.ResolveAlias(field.Name)\n\t\t\tif !exists {\n\t\t\t\treturn nil, errors.Errorf(\"invalid alias %q\", field.Name)\n\t\t\t}\n\t\t}\n\t\tif def.AllowedComparisonOps != nil {\n\t\t\tif _, allowed := def.AllowedComparisonOps[op]; !allowed {\n\t\t\t\treturn nil, errors.Errorf(\"operator %s not allowed for field %q\", op, field.Name)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn &ComparisonCondition{\n\t\tLeft:     left,\n\t\tOperator: op,\n\t\tRight:    right,\n\t}, nil\n}\n\nfunc buildInCondition(call *exprv1.Expr_Call, schema Schema) (Condition, error) {\n\tif len(call.Args) != 2 {\n\t\treturn nil, errors.New(\"in operator expects two arguments\")\n\t}\n\n\t// Handle identifier in list syntax.\n\tif identName, err := getIdentName(call.Args[0]); err == nil {\n\t\tif field, ok := schema.Field(identName); ok && field.Kind == FieldKindVirtualAlias {\n\t\t\tif _, aliasOk := schema.ResolveAlias(identName); !aliasOk {\n\t\t\t\treturn nil, errors.Errorf(\"invalid alias %q\", identName)\n\t\t\t}\n\t\t} else if !ok {\n\t\t\treturn nil, errors.Errorf(\"unknown identifier %q\", identName)\n\t\t}\n\n\t\tif listExpr := call.Args[1].GetListExpr(); listExpr != nil {\n\t\t\tvalues := make([]ValueExpr, 0, len(listExpr.Elements))\n\t\t\tfor _, element := range listExpr.Elements {\n\t\t\t\tvalue, err := buildValueExpr(element, schema)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t\tvalues = append(values, value)\n\t\t\t}\n\t\t\treturn &InCondition{\n\t\t\t\tLeft:   &FieldRef{Name: identName},\n\t\t\t\tValues: values,\n\t\t\t}, nil\n\t\t}\n\t}\n\n\t// Handle \"value in identifier\" syntax.\n\tif identName, err := getIdentName(call.Args[1]); err == nil {\n\t\tif _, ok := schema.Field(identName); !ok {\n\t\t\treturn nil, errors.Errorf(\"unknown identifier %q\", identName)\n\t\t}\n\t\telement, err := buildValueExpr(call.Args[0], schema)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn &ElementInCondition{\n\t\t\tElement: element,\n\t\t\tField:   identName,\n\t\t}, nil\n\t}\n\n\treturn nil, errors.New(\"invalid use of in operator\")\n}\n\nfunc buildContainsCondition(call *exprv1.Expr_Call, schema Schema) (Condition, error) {\n\tif call.Target == nil {\n\t\treturn nil, errors.New(\"contains requires a target\")\n\t}\n\ttargetName, err := getIdentName(call.Target)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfield, ok := schema.Field(targetName)\n\tif !ok {\n\t\treturn nil, errors.Errorf(\"unknown identifier %q\", targetName)\n\t}\n\tif !field.SupportsContains {\n\t\treturn nil, errors.Errorf(\"identifier %q does not support contains()\", targetName)\n\t}\n\tif len(call.Args) != 1 {\n\t\treturn nil, errors.New(\"contains expects exactly one argument\")\n\t}\n\tvalue, err := getConstValue(call.Args[0])\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"contains only supports literal arguments\")\n\t}\n\tstr, ok := value.(string)\n\tif !ok {\n\t\treturn nil, errors.New(\"contains argument must be a string\")\n\t}\n\treturn &ContainsCondition{\n\t\tField: targetName,\n\t\tValue: str,\n\t}, nil\n}\n\nfunc buildValueExpr(expr *exprv1.Expr, schema Schema) (ValueExpr, error) {\n\tif identName, err := getIdentName(expr); err == nil {\n\t\tif _, ok := schema.Field(identName); !ok {\n\t\t\treturn nil, errors.Errorf(\"unknown identifier %q\", identName)\n\t\t}\n\t\treturn &FieldRef{Name: identName}, nil\n\t}\n\n\tif literal, err := getConstValue(expr); err == nil {\n\t\treturn &LiteralValue{Value: literal}, nil\n\t}\n\n\tif value, ok, err := evaluateNumeric(expr); err != nil {\n\t\treturn nil, err\n\t} else if ok {\n\t\treturn &LiteralValue{Value: value}, nil\n\t}\n\n\tif boolVal, ok, err := evaluateBoolExpr(expr); err != nil {\n\t\treturn nil, err\n\t} else if ok {\n\t\treturn &LiteralValue{Value: boolVal}, nil\n\t}\n\n\tif call := expr.GetCallExpr(); call != nil {\n\t\tswitch call.Function {\n\t\tcase \"size\":\n\t\t\tif len(call.Args) != 1 {\n\t\t\t\treturn nil, errors.New(\"size() expects one argument\")\n\t\t\t}\n\t\t\targ, err := buildValueExpr(call.Args[0], schema)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\treturn &FunctionValue{\n\t\t\t\tName: \"size\",\n\t\t\t\tArgs: []ValueExpr{arg},\n\t\t\t}, nil\n\t\tcase \"now\":\n\t\t\treturn &LiteralValue{Value: timeNowUnix()}, nil\n\t\tcase \"_+_\", \"_-_\", \"_*_\":\n\t\t\tvalue, ok, err := evaluateNumeric(expr)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tif ok {\n\t\t\t\treturn &LiteralValue{Value: value}, nil\n\t\t\t}\n\t\tdefault:\n\t\t\t// Fall through to error return below\n\t\t}\n\t}\n\n\treturn nil, errors.New(\"unsupported value expression\")\n}\n\nfunc toComparisonOperator(fn string) (ComparisonOperator, error) {\n\tswitch fn {\n\tcase \"_==_\":\n\t\treturn CompareEq, nil\n\tcase \"_!=_\":\n\t\treturn CompareNeq, nil\n\tcase \"_<_\":\n\t\treturn CompareLt, nil\n\tcase \"_>_\":\n\t\treturn CompareGt, nil\n\tcase \"_<=_\":\n\t\treturn CompareLte, nil\n\tcase \"_>=_\":\n\t\treturn CompareGte, nil\n\tdefault:\n\t\treturn \"\", errors.Errorf(\"unsupported comparison operator %q\", fn)\n\t}\n}\n\nfunc getIdentName(expr *exprv1.Expr) (string, error) {\n\tif ident := expr.GetIdentExpr(); ident != nil {\n\t\treturn ident.GetName(), nil\n\t}\n\treturn \"\", errors.New(\"expression is not an identifier\")\n}\n\nfunc getConstValue(expr *exprv1.Expr) (interface{}, error) {\n\tv, ok := expr.ExprKind.(*exprv1.Expr_ConstExpr)\n\tif !ok {\n\t\treturn nil, errors.New(\"expression is not a literal\")\n\t}\n\tswitch x := v.ConstExpr.ConstantKind.(type) {\n\tcase *exprv1.Constant_StringValue:\n\t\treturn v.ConstExpr.GetStringValue(), nil\n\tcase *exprv1.Constant_Int64Value:\n\t\treturn v.ConstExpr.GetInt64Value(), nil\n\tcase *exprv1.Constant_Uint64Value:\n\t\treturn int64(v.ConstExpr.GetUint64Value()), nil\n\tcase *exprv1.Constant_DoubleValue:\n\t\treturn v.ConstExpr.GetDoubleValue(), nil\n\tcase *exprv1.Constant_BoolValue:\n\t\treturn v.ConstExpr.GetBoolValue(), nil\n\tcase *exprv1.Constant_NullValue:\n\t\treturn nil, nil\n\tdefault:\n\t\treturn nil, errors.Errorf(\"unsupported constant %T\", x)\n\t}\n}\n\nfunc evaluateBool(call *exprv1.Expr_Call) (bool, bool, error) {\n\tval, ok, err := evaluateBoolExpr(&exprv1.Expr{ExprKind: &exprv1.Expr_CallExpr{CallExpr: call}})\n\treturn val, ok, err\n}\n\nfunc evaluateBoolExpr(expr *exprv1.Expr) (bool, bool, error) {\n\tif literal, err := getConstValue(expr); err == nil {\n\t\tif b, ok := literal.(bool); ok {\n\t\t\treturn b, true, nil\n\t\t}\n\t\treturn false, false, nil\n\t}\n\tif call := expr.GetCallExpr(); call != nil && call.Function == \"!_\" {\n\t\tif len(call.Args) != 1 {\n\t\t\treturn false, false, errors.New(\"NOT expects exactly one argument\")\n\t\t}\n\t\tval, ok, err := evaluateBoolExpr(call.Args[0])\n\t\tif err != nil || !ok {\n\t\t\treturn false, false, err\n\t\t}\n\t\treturn !val, true, nil\n\t}\n\treturn false, false, nil\n}\n\nfunc evaluateNumeric(expr *exprv1.Expr) (int64, bool, error) {\n\tif literal, err := getConstValue(expr); err == nil {\n\t\tswitch v := literal.(type) {\n\t\tcase int64:\n\t\t\treturn v, true, nil\n\t\tcase float64:\n\t\t\treturn int64(v), true, nil\n\t\t}\n\t\treturn 0, false, nil\n\t}\n\n\tcall := expr.GetCallExpr()\n\tif call == nil {\n\t\treturn 0, false, nil\n\t}\n\n\tswitch call.Function {\n\tcase \"now\":\n\t\treturn timeNowUnix(), true, nil\n\tcase \"_+_\", \"_-_\", \"_*_\":\n\t\tif len(call.Args) != 2 {\n\t\t\treturn 0, false, errors.New(\"arithmetic requires two arguments\")\n\t\t}\n\t\tleft, ok, err := evaluateNumeric(call.Args[0])\n\t\tif err != nil {\n\t\t\treturn 0, false, err\n\t\t}\n\t\tif !ok {\n\t\t\treturn 0, false, nil\n\t\t}\n\t\tright, ok, err := evaluateNumeric(call.Args[1])\n\t\tif err != nil {\n\t\t\treturn 0, false, err\n\t\t}\n\t\tif !ok {\n\t\t\treturn 0, false, nil\n\t\t}\n\t\tswitch call.Function {\n\t\tcase \"_+_\":\n\t\t\treturn left + right, true, nil\n\t\tcase \"_-_\":\n\t\t\treturn left - right, true, nil\n\t\tcase \"_*_\":\n\t\t\treturn left * right, true, nil\n\t\tdefault:\n\t\t\treturn 0, false, errors.Errorf(\"unsupported arithmetic operator %q\", call.Function)\n\t\t}\n\tdefault:\n\t\treturn 0, false, nil\n\t}\n}\n\nfunc timeNowUnix() int64 {\n\treturn time.Now().Unix()\n}\n\n// buildComprehensionCondition handles CEL comprehension expressions (exists, all, etc.).\nfunc buildComprehensionCondition(comp *exprv1.Expr_Comprehension, schema Schema) (Condition, error) {\n\t// Determine the comprehension kind by examining the loop initialization and step\n\tkind, err := detectComprehensionKind(comp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Get the field being iterated over\n\titerRangeIdent := comp.IterRange.GetIdentExpr()\n\tif iterRangeIdent == nil {\n\t\treturn nil, errors.New(\"comprehension range must be a field identifier\")\n\t}\n\tfieldName := iterRangeIdent.GetName()\n\n\t// Validate the field\n\tfield, ok := schema.Field(fieldName)\n\tif !ok {\n\t\treturn nil, errors.Errorf(\"unknown field %q in comprehension\", fieldName)\n\t}\n\tif field.Kind != FieldKindJSONList {\n\t\treturn nil, errors.Errorf(\"field %q does not support comprehension (must be a list)\", fieldName)\n\t}\n\n\t// Extract the predicate from the loop step\n\tpredicate, err := extractPredicate(comp, schema)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &ListComprehensionCondition{\n\t\tKind:      kind,\n\t\tField:     fieldName,\n\t\tIterVar:   comp.IterVar,\n\t\tPredicate: predicate,\n\t}, nil\n}\n\n// detectComprehensionKind determines if this is an exists() macro.\n// Only exists() is currently supported.\nfunc detectComprehensionKind(comp *exprv1.Expr_Comprehension) (ComprehensionKind, error) {\n\t// Check the accumulator initialization\n\taccuInit := comp.AccuInit.GetConstExpr()\n\tif accuInit == nil {\n\t\treturn \"\", errors.New(\"comprehension accumulator must be initialized with a constant\")\n\t}\n\n\t// exists() starts with false and uses OR (||) in loop step\n\tif !accuInit.GetBoolValue() {\n\t\tif step := comp.LoopStep.GetCallExpr(); step != nil && step.Function == \"_||_\" {\n\t\t\treturn ComprehensionExists, nil\n\t\t}\n\t}\n\n\t// all() starts with true and uses AND (&&) - not supported\n\tif accuInit.GetBoolValue() {\n\t\tif step := comp.LoopStep.GetCallExpr(); step != nil && step.Function == \"_&&_\" {\n\t\t\treturn \"\", errors.New(\"all() comprehension is not supported; use exists() instead\")\n\t\t}\n\t}\n\n\treturn \"\", errors.New(\"unsupported comprehension type; only exists() is supported\")\n}\n\n// extractPredicate extracts the predicate expression from the comprehension loop step.\nfunc extractPredicate(comp *exprv1.Expr_Comprehension, _ Schema) (PredicateExpr, error) {\n\t// The loop step is: @result || predicate(t) for exists\n\t//                or: @result && predicate(t) for all\n\tstep := comp.LoopStep.GetCallExpr()\n\tif step == nil {\n\t\treturn nil, errors.New(\"comprehension loop step must be a call expression\")\n\t}\n\n\tif len(step.Args) != 2 {\n\t\treturn nil, errors.New(\"comprehension loop step must have two arguments\")\n\t}\n\n\t// The predicate is the second argument\n\tpredicateExpr := step.Args[1]\n\tpredicateCall := predicateExpr.GetCallExpr()\n\tif predicateCall == nil {\n\t\treturn nil, errors.New(\"comprehension predicate must be a function call\")\n\t}\n\n\t// Handle different predicate functions\n\tswitch predicateCall.Function {\n\tcase \"startsWith\":\n\t\treturn buildStartsWithPredicate(predicateCall, comp.IterVar)\n\tcase \"endsWith\":\n\t\treturn buildEndsWithPredicate(predicateCall, comp.IterVar)\n\tcase \"contains\":\n\t\treturn buildContainsPredicate(predicateCall, comp.IterVar)\n\tdefault:\n\t\treturn nil, errors.Errorf(\"unsupported predicate function %q in comprehension (supported: startsWith, endsWith, contains)\", predicateCall.Function)\n\t}\n}\n\n// buildStartsWithPredicate extracts the pattern from t.startsWith(\"prefix\").\nfunc buildStartsWithPredicate(call *exprv1.Expr_Call, iterVar string) (PredicateExpr, error) {\n\t// Verify the target is the iteration variable\n\tif target := call.Target.GetIdentExpr(); target == nil || target.GetName() != iterVar {\n\t\treturn nil, errors.Errorf(\"startsWith target must be the iteration variable %q\", iterVar)\n\t}\n\n\tif len(call.Args) != 1 {\n\t\treturn nil, errors.New(\"startsWith expects exactly one argument\")\n\t}\n\n\tprefix, err := getConstValue(call.Args[0])\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"startsWith argument must be a constant string\")\n\t}\n\n\tprefixStr, ok := prefix.(string)\n\tif !ok {\n\t\treturn nil, errors.New(\"startsWith argument must be a string\")\n\t}\n\n\treturn &StartsWithPredicate{Prefix: prefixStr}, nil\n}\n\n// buildEndsWithPredicate extracts the pattern from t.endsWith(\"suffix\").\nfunc buildEndsWithPredicate(call *exprv1.Expr_Call, iterVar string) (PredicateExpr, error) {\n\tif target := call.Target.GetIdentExpr(); target == nil || target.GetName() != iterVar {\n\t\treturn nil, errors.Errorf(\"endsWith target must be the iteration variable %q\", iterVar)\n\t}\n\n\tif len(call.Args) != 1 {\n\t\treturn nil, errors.New(\"endsWith expects exactly one argument\")\n\t}\n\n\tsuffix, err := getConstValue(call.Args[0])\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"endsWith argument must be a constant string\")\n\t}\n\n\tsuffixStr, ok := suffix.(string)\n\tif !ok {\n\t\treturn nil, errors.New(\"endsWith argument must be a string\")\n\t}\n\n\treturn &EndsWithPredicate{Suffix: suffixStr}, nil\n}\n\n// buildContainsPredicate extracts the pattern from t.contains(\"substring\").\nfunc buildContainsPredicate(call *exprv1.Expr_Call, iterVar string) (PredicateExpr, error) {\n\tif target := call.Target.GetIdentExpr(); target == nil || target.GetName() != iterVar {\n\t\treturn nil, errors.Errorf(\"contains target must be the iteration variable %q\", iterVar)\n\t}\n\n\tif len(call.Args) != 1 {\n\t\treturn nil, errors.New(\"contains expects exactly one argument\")\n\t}\n\n\tsubstring, err := getConstValue(call.Args[0])\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"contains argument must be a constant string\")\n\t}\n\n\tsubstringStr, ok := substring.(string)\n\tif !ok {\n\t\treturn nil, errors.New(\"contains argument must be a string\")\n\t}\n\n\treturn &ContainsPredicate{Substring: substringStr}, nil\n}\n"
  },
  {
    "path": "plugin/filter/render.go",
    "content": "package filter\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/pkg/errors\"\n)\n\ntype renderer struct {\n\tschema             Schema\n\tdialect            DialectName\n\tplaceholderOffset  int\n\tplaceholderCounter int\n\targs               []any\n}\n\ntype renderResult struct {\n\tsql           string\n\ttrivial       bool\n\tunsatisfiable bool\n}\n\nfunc newRenderer(schema Schema, opts RenderOptions) *renderer {\n\treturn &renderer{\n\t\tschema:            schema,\n\t\tdialect:           opts.Dialect,\n\t\tplaceholderOffset: opts.PlaceholderOffset,\n\t}\n}\n\nfunc (r *renderer) Render(cond Condition) (Statement, error) {\n\tresult, err := r.renderCondition(cond)\n\tif err != nil {\n\t\treturn Statement{}, err\n\t}\n\targs := r.args\n\tif args == nil {\n\t\targs = []any{}\n\t}\n\n\tswitch {\n\tcase result.unsatisfiable:\n\t\treturn Statement{\n\t\t\tSQL:  \"1 = 0\",\n\t\t\tArgs: args,\n\t\t}, nil\n\tcase result.trivial:\n\t\treturn Statement{\n\t\t\tSQL:  \"\",\n\t\t\tArgs: args,\n\t\t}, nil\n\tdefault:\n\t\treturn Statement{\n\t\t\tSQL:  result.sql,\n\t\t\tArgs: args,\n\t\t}, nil\n\t}\n}\n\nfunc (r *renderer) renderCondition(cond Condition) (renderResult, error) {\n\tswitch c := cond.(type) {\n\tcase *LogicalCondition:\n\t\treturn r.renderLogicalCondition(c)\n\tcase *NotCondition:\n\t\treturn r.renderNotCondition(c)\n\tcase *FieldPredicateCondition:\n\t\treturn r.renderFieldPredicate(c)\n\tcase *ComparisonCondition:\n\t\treturn r.renderComparison(c)\n\tcase *InCondition:\n\t\treturn r.renderInCondition(c)\n\tcase *ElementInCondition:\n\t\treturn r.renderElementInCondition(c)\n\tcase *ContainsCondition:\n\t\treturn r.renderContainsCondition(c)\n\tcase *ListComprehensionCondition:\n\t\treturn r.renderListComprehension(c)\n\tcase *ConstantCondition:\n\t\tif c.Value {\n\t\t\treturn renderResult{trivial: true}, nil\n\t\t}\n\t\treturn renderResult{sql: \"1 = 0\", unsatisfiable: true}, nil\n\tdefault:\n\t\treturn renderResult{}, errors.Errorf(\"unsupported condition type %T\", c)\n\t}\n}\n\nfunc (r *renderer) renderLogicalCondition(cond *LogicalCondition) (renderResult, error) {\n\tleft, err := r.renderCondition(cond.Left)\n\tif err != nil {\n\t\treturn renderResult{}, err\n\t}\n\tright, err := r.renderCondition(cond.Right)\n\tif err != nil {\n\t\treturn renderResult{}, err\n\t}\n\n\tswitch cond.Operator {\n\tcase LogicalAnd:\n\t\treturn combineAnd(left, right), nil\n\tcase LogicalOr:\n\t\treturn combineOr(left, right), nil\n\tdefault:\n\t\treturn renderResult{}, errors.Errorf(\"unsupported logical operator %s\", cond.Operator)\n\t}\n}\n\nfunc (r *renderer) renderNotCondition(cond *NotCondition) (renderResult, error) {\n\tchild, err := r.renderCondition(cond.Expr)\n\tif err != nil {\n\t\treturn renderResult{}, err\n\t}\n\n\tif child.trivial {\n\t\treturn renderResult{sql: \"1 = 0\", unsatisfiable: true}, nil\n\t}\n\tif child.unsatisfiable {\n\t\treturn renderResult{trivial: true}, nil\n\t}\n\treturn renderResult{\n\t\tsql: fmt.Sprintf(\"NOT (%s)\", child.sql),\n\t}, nil\n}\n\nfunc (r *renderer) renderFieldPredicate(cond *FieldPredicateCondition) (renderResult, error) {\n\tfield, ok := r.schema.Field(cond.Field)\n\tif !ok {\n\t\treturn renderResult{}, errors.Errorf(\"unknown field %q\", cond.Field)\n\t}\n\n\tswitch field.Kind {\n\tcase FieldKindBoolColumn:\n\t\tcolumn := qualifyColumn(r.dialect, field.Column)\n\t\treturn renderResult{\n\t\t\tsql: fmt.Sprintf(\"%s IS TRUE\", column),\n\t\t}, nil\n\tcase FieldKindJSONBool:\n\t\tsql, err := r.jsonBoolPredicate(field)\n\t\tif err != nil {\n\t\t\treturn renderResult{}, err\n\t\t}\n\t\treturn renderResult{sql: sql}, nil\n\tdefault:\n\t\treturn renderResult{}, errors.Errorf(\"field %q cannot be used as a predicate\", cond.Field)\n\t}\n}\n\nfunc (r *renderer) renderComparison(cond *ComparisonCondition) (renderResult, error) {\n\tswitch left := cond.Left.(type) {\n\tcase *FieldRef:\n\t\tfield, ok := r.schema.Field(left.Name)\n\t\tif !ok {\n\t\t\treturn renderResult{}, errors.Errorf(\"unknown field %q\", left.Name)\n\t\t}\n\t\tswitch field.Kind {\n\t\tcase FieldKindBoolColumn:\n\t\t\treturn r.renderBoolColumnComparison(field, cond.Operator, cond.Right)\n\t\tcase FieldKindJSONBool:\n\t\t\treturn r.renderJSONBoolComparison(field, cond.Operator, cond.Right)\n\t\tcase FieldKindScalar:\n\t\t\treturn r.renderScalarComparison(field, cond.Operator, cond.Right)\n\t\tdefault:\n\t\t\treturn renderResult{}, errors.Errorf(\"field %q does not support comparison\", field.Name)\n\t\t}\n\tcase *FunctionValue:\n\t\treturn r.renderFunctionComparison(left, cond.Operator, cond.Right)\n\tdefault:\n\t\treturn renderResult{}, errors.New(\"comparison must start with a field reference or supported function\")\n\t}\n}\n\nfunc (r *renderer) renderFunctionComparison(fn *FunctionValue, op ComparisonOperator, right ValueExpr) (renderResult, error) {\n\tif fn.Name != \"size\" {\n\t\treturn renderResult{}, errors.Errorf(\"unsupported function %s in comparison\", fn.Name)\n\t}\n\tif len(fn.Args) != 1 {\n\t\treturn renderResult{}, errors.New(\"size() expects one argument\")\n\t}\n\tfieldArg, ok := fn.Args[0].(*FieldRef)\n\tif !ok {\n\t\treturn renderResult{}, errors.New(\"size() argument must be a field\")\n\t}\n\n\tfield, ok := r.schema.Field(fieldArg.Name)\n\tif !ok {\n\t\treturn renderResult{}, errors.Errorf(\"unknown field %q\", fieldArg.Name)\n\t}\n\tif field.Kind != FieldKindJSONList {\n\t\treturn renderResult{}, errors.Errorf(\"size() only supports tag lists, got %q\", field.Name)\n\t}\n\n\tvalue, err := expectNumericLiteral(right)\n\tif err != nil {\n\t\treturn renderResult{}, err\n\t}\n\n\texpr := jsonArrayLengthExpr(r.dialect, field)\n\tplaceholder := r.addArg(value)\n\treturn renderResult{\n\t\tsql: fmt.Sprintf(\"%s %s %s\", expr, sqlOperator(op), placeholder),\n\t}, nil\n}\n\nfunc (r *renderer) renderScalarComparison(field Field, op ComparisonOperator, right ValueExpr) (renderResult, error) {\n\tlit, err := expectLiteral(right)\n\tif err != nil {\n\t\treturn renderResult{}, err\n\t}\n\n\tcolumnExpr := field.columnExpr(r.dialect)\n\tif lit == nil {\n\t\tswitch op {\n\t\tcase CompareEq:\n\t\t\treturn renderResult{sql: fmt.Sprintf(\"%s IS NULL\", columnExpr)}, nil\n\t\tcase CompareNeq:\n\t\t\treturn renderResult{sql: fmt.Sprintf(\"%s IS NOT NULL\", columnExpr)}, nil\n\t\tdefault:\n\t\t\treturn renderResult{}, errors.Errorf(\"operator %s not supported for null comparison\", op)\n\t\t}\n\t}\n\n\tplaceholder := \"\"\n\tswitch field.Type {\n\tcase FieldTypeString:\n\t\tvalue, ok := lit.(string)\n\t\tif !ok {\n\t\t\treturn renderResult{}, errors.Errorf(\"field %q expects string value\", field.Name)\n\t\t}\n\t\tplaceholder = r.addArg(value)\n\tcase FieldTypeInt, FieldTypeTimestamp:\n\t\tnum, err := toInt64(lit)\n\t\tif err != nil {\n\t\t\treturn renderResult{}, errors.Wrapf(err, \"field %q expects integer value\", field.Name)\n\t\t}\n\t\tplaceholder = r.addArg(num)\n\tdefault:\n\t\treturn renderResult{}, errors.Errorf(\"unsupported data type %q for field %s\", field.Type, field.Name)\n\t}\n\n\treturn renderResult{\n\t\tsql: fmt.Sprintf(\"%s %s %s\", columnExpr, sqlOperator(op), placeholder),\n\t}, nil\n}\n\nfunc (r *renderer) renderBoolColumnComparison(field Field, op ComparisonOperator, right ValueExpr) (renderResult, error) {\n\tvalue, err := expectBool(right)\n\tif err != nil {\n\t\treturn renderResult{}, err\n\t}\n\tplaceholder := r.addBoolArg(value)\n\tcolumn := qualifyColumn(r.dialect, field.Column)\n\treturn renderResult{\n\t\tsql: fmt.Sprintf(\"%s %s %s\", column, sqlOperator(op), placeholder),\n\t}, nil\n}\n\nfunc (r *renderer) renderJSONBoolComparison(field Field, op ComparisonOperator, right ValueExpr) (renderResult, error) {\n\tvalue, err := expectBool(right)\n\tif err != nil {\n\t\treturn renderResult{}, err\n\t}\n\n\tjsonExpr := jsonExtractExpr(r.dialect, field)\n\tswitch r.dialect {\n\tcase DialectSQLite:\n\t\tswitch op {\n\t\tcase CompareEq:\n\t\t\tif field.Name == \"has_task_list\" {\n\t\t\t\ttarget := \"0\"\n\t\t\t\tif value {\n\t\t\t\t\ttarget = \"1\"\n\t\t\t\t}\n\t\t\t\treturn renderResult{sql: fmt.Sprintf(\"%s = %s\", jsonExpr, target)}, nil\n\t\t\t}\n\t\t\tif value {\n\t\t\t\treturn renderResult{sql: fmt.Sprintf(\"%s IS TRUE\", jsonExpr)}, nil\n\t\t\t}\n\t\t\treturn renderResult{sql: fmt.Sprintf(\"NOT(%s IS TRUE)\", jsonExpr)}, nil\n\t\tcase CompareNeq:\n\t\t\tif field.Name == \"has_task_list\" {\n\t\t\t\ttarget := \"0\"\n\t\t\t\tif value {\n\t\t\t\t\ttarget = \"1\"\n\t\t\t\t}\n\t\t\t\treturn renderResult{sql: fmt.Sprintf(\"%s != %s\", jsonExpr, target)}, nil\n\t\t\t}\n\t\t\tif value {\n\t\t\t\treturn renderResult{sql: fmt.Sprintf(\"NOT(%s IS TRUE)\", jsonExpr)}, nil\n\t\t\t}\n\t\t\treturn renderResult{sql: fmt.Sprintf(\"%s IS TRUE\", jsonExpr)}, nil\n\t\tdefault:\n\t\t\treturn renderResult{}, errors.Errorf(\"operator %s not supported for boolean JSON field\", op)\n\t\t}\n\tcase DialectMySQL:\n\t\tboolStr := \"false\"\n\t\tif value {\n\t\t\tboolStr = \"true\"\n\t\t}\n\t\treturn renderResult{\n\t\t\tsql: fmt.Sprintf(\"%s %s CAST('%s' AS JSON)\", jsonExpr, sqlOperator(op), boolStr),\n\t\t}, nil\n\tcase DialectPostgres:\n\t\tplaceholder := r.addArg(value)\n\t\treturn renderResult{\n\t\t\tsql: fmt.Sprintf(\"(%s)::boolean %s %s\", jsonExpr, sqlOperator(op), placeholder),\n\t\t}, nil\n\tdefault:\n\t\treturn renderResult{}, errors.Errorf(\"unsupported dialect %s\", r.dialect)\n\t}\n}\n\nfunc (r *renderer) renderInCondition(cond *InCondition) (renderResult, error) {\n\tfieldRef, ok := cond.Left.(*FieldRef)\n\tif !ok {\n\t\treturn renderResult{}, errors.New(\"IN operator requires a field on the left-hand side\")\n\t}\n\n\tif fieldRef.Name == \"tag\" {\n\t\treturn r.renderTagInList(cond.Values)\n\t}\n\n\tfield, ok := r.schema.Field(fieldRef.Name)\n\tif !ok {\n\t\treturn renderResult{}, errors.Errorf(\"unknown field %q\", fieldRef.Name)\n\t}\n\n\tif field.Kind != FieldKindScalar {\n\t\treturn renderResult{}, errors.Errorf(\"field %q does not support IN()\", fieldRef.Name)\n\t}\n\n\treturn r.renderScalarInCondition(field, cond.Values)\n}\n\nfunc (r *renderer) renderTagInList(values []ValueExpr) (renderResult, error) {\n\tfield, ok := r.schema.ResolveAlias(\"tag\")\n\tif !ok {\n\t\treturn renderResult{}, errors.New(\"tag attribute is not configured\")\n\t}\n\n\tconditions := make([]string, 0, len(values))\n\tfor _, v := range values {\n\t\tlit, err := expectLiteral(v)\n\t\tif err != nil {\n\t\t\treturn renderResult{}, err\n\t\t}\n\t\tstr, ok := lit.(string)\n\t\tif !ok {\n\t\t\treturn renderResult{}, errors.New(\"tags must be compared with string literals\")\n\t\t}\n\n\t\tswitch r.dialect {\n\t\tcase DialectSQLite:\n\t\t\t// Support hierarchical tags: match exact tag OR tags with this prefix (e.g., \"book\" matches \"book\" and \"book/something\")\n\t\t\texactMatch := fmt.Sprintf(\"%s LIKE %s\", jsonArrayExpr(r.dialect, field), r.addArg(fmt.Sprintf(`%%\"%s\"%%`, str)))\n\t\t\tprefixMatch := fmt.Sprintf(\"%s LIKE %s\", jsonArrayExpr(r.dialect, field), r.addArg(fmt.Sprintf(`%%\"%s/%%`, str)))\n\t\t\texpr := fmt.Sprintf(\"(%s OR %s)\", exactMatch, prefixMatch)\n\t\t\tconditions = append(conditions, expr)\n\t\tcase DialectMySQL:\n\t\t\t// Support hierarchical tags: match exact tag OR tags with this prefix\n\t\t\texactMatch := fmt.Sprintf(\"JSON_CONTAINS(%s, %s)\", jsonArrayExpr(r.dialect, field), r.addArg(fmt.Sprintf(`\"%s\"`, str)))\n\t\t\tprefixMatch := fmt.Sprintf(\"%s LIKE %s\", jsonArrayExpr(r.dialect, field), r.addArg(fmt.Sprintf(`%%\"%s/%%`, str)))\n\t\t\texpr := fmt.Sprintf(\"(%s OR %s)\", exactMatch, prefixMatch)\n\t\t\tconditions = append(conditions, expr)\n\t\tcase DialectPostgres:\n\t\t\t// Support hierarchical tags: match exact tag OR tags with this prefix\n\t\t\texactMatch := fmt.Sprintf(\"%s @> jsonb_build_array(%s::json)\", jsonArrayExpr(r.dialect, field), r.addArg(fmt.Sprintf(`\"%s\"`, str)))\n\t\t\tprefixMatch := fmt.Sprintf(\"(%s)::text LIKE %s\", jsonArrayExpr(r.dialect, field), r.addArg(fmt.Sprintf(`%%\"%s/%%`, str)))\n\t\t\texpr := fmt.Sprintf(\"(%s OR %s)\", exactMatch, prefixMatch)\n\t\t\tconditions = append(conditions, expr)\n\t\tdefault:\n\t\t\treturn renderResult{}, errors.Errorf(\"unsupported dialect %s\", r.dialect)\n\t\t}\n\t}\n\n\tif len(conditions) == 1 {\n\t\treturn renderResult{sql: conditions[0]}, nil\n\t}\n\treturn renderResult{\n\t\tsql: fmt.Sprintf(\"(%s)\", strings.Join(conditions, \" OR \")),\n\t}, nil\n}\n\nfunc (r *renderer) renderElementInCondition(cond *ElementInCondition) (renderResult, error) {\n\tfield, ok := r.schema.Field(cond.Field)\n\tif !ok {\n\t\treturn renderResult{}, errors.Errorf(\"unknown field %q\", cond.Field)\n\t}\n\tif field.Kind != FieldKindJSONList {\n\t\treturn renderResult{}, errors.Errorf(\"field %q is not a tag list\", cond.Field)\n\t}\n\n\tlit, err := expectLiteral(cond.Element)\n\tif err != nil {\n\t\treturn renderResult{}, err\n\t}\n\tstr, ok := lit.(string)\n\tif !ok {\n\t\treturn renderResult{}, errors.New(\"tags membership requires string literal\")\n\t}\n\n\tswitch r.dialect {\n\tcase DialectSQLite:\n\t\tsql := fmt.Sprintf(\"%s LIKE %s\", jsonArrayExpr(r.dialect, field), r.addArg(fmt.Sprintf(`%%\"%s\"%%`, str)))\n\t\treturn renderResult{sql: sql}, nil\n\tcase DialectMySQL:\n\t\tsql := fmt.Sprintf(\"JSON_CONTAINS(%s, %s)\", jsonArrayExpr(r.dialect, field), r.addArg(fmt.Sprintf(`\"%s\"`, str)))\n\t\treturn renderResult{sql: sql}, nil\n\tcase DialectPostgres:\n\t\tsql := fmt.Sprintf(\"%s @> jsonb_build_array(%s::json)\", jsonArrayExpr(r.dialect, field), r.addArg(fmt.Sprintf(`\"%s\"`, str)))\n\t\treturn renderResult{sql: sql}, nil\n\tdefault:\n\t\treturn renderResult{}, errors.Errorf(\"unsupported dialect %s\", r.dialect)\n\t}\n}\n\nfunc (r *renderer) renderScalarInCondition(field Field, values []ValueExpr) (renderResult, error) {\n\tplaceholders := make([]string, 0, len(values))\n\n\tfor _, v := range values {\n\t\tlit, err := expectLiteral(v)\n\t\tif err != nil {\n\t\t\treturn renderResult{}, err\n\t\t}\n\t\tswitch field.Type {\n\t\tcase FieldTypeString:\n\t\t\tstr, ok := lit.(string)\n\t\t\tif !ok {\n\t\t\t\treturn renderResult{}, errors.Errorf(\"field %q expects string values\", field.Name)\n\t\t\t}\n\t\t\tplaceholders = append(placeholders, r.addArg(str))\n\t\tcase FieldTypeInt:\n\t\t\tnum, err := toInt64(lit)\n\t\t\tif err != nil {\n\t\t\t\treturn renderResult{}, err\n\t\t\t}\n\t\t\tplaceholders = append(placeholders, r.addArg(num))\n\t\tdefault:\n\t\t\treturn renderResult{}, errors.Errorf(\"field %q does not support IN() comparisons\", field.Name)\n\t\t}\n\t}\n\n\tcolumn := field.columnExpr(r.dialect)\n\treturn renderResult{\n\t\tsql: fmt.Sprintf(\"%s IN (%s)\", column, strings.Join(placeholders, \",\")),\n\t}, nil\n}\n\nfunc (r *renderer) renderContainsCondition(cond *ContainsCondition) (renderResult, error) {\n\tfield, ok := r.schema.Field(cond.Field)\n\tif !ok {\n\t\treturn renderResult{}, errors.Errorf(\"unknown field %q\", cond.Field)\n\t}\n\tcolumn := field.columnExpr(r.dialect)\n\targ := fmt.Sprintf(\"%%%s%%\", cond.Value)\n\tswitch r.dialect {\n\tcase DialectSQLite:\n\t\t// Use custom Unicode-aware case folding function for case-insensitive comparison.\n\t\t// This overcomes SQLite's ASCII-only LOWER() limitation.\n\t\tsql := fmt.Sprintf(\"memos_unicode_lower(%s) LIKE memos_unicode_lower(%s)\", column, r.addArg(arg))\n\t\treturn renderResult{sql: sql}, nil\n\tcase DialectPostgres:\n\t\tsql := fmt.Sprintf(\"%s ILIKE %s\", column, r.addArg(arg))\n\t\treturn renderResult{sql: sql}, nil\n\tdefault:\n\t\tsql := fmt.Sprintf(\"%s LIKE %s\", column, r.addArg(arg))\n\t\treturn renderResult{sql: sql}, nil\n\t}\n}\n\nfunc (r *renderer) renderListComprehension(cond *ListComprehensionCondition) (renderResult, error) {\n\tfield, ok := r.schema.Field(cond.Field)\n\tif !ok {\n\t\treturn renderResult{}, errors.Errorf(\"unknown field %q\", cond.Field)\n\t}\n\n\tif field.Kind != FieldKindJSONList {\n\t\treturn renderResult{}, errors.Errorf(\"field %q is not a JSON list\", cond.Field)\n\t}\n\n\t// Render based on predicate type\n\tswitch pred := cond.Predicate.(type) {\n\tcase *StartsWithPredicate:\n\t\treturn r.renderTagStartsWith(field, pred.Prefix, cond.Kind)\n\tcase *EndsWithPredicate:\n\t\treturn r.renderTagEndsWith(field, pred.Suffix, cond.Kind)\n\tcase *ContainsPredicate:\n\t\treturn r.renderTagContains(field, pred.Substring, cond.Kind)\n\tdefault:\n\t\treturn renderResult{}, errors.Errorf(\"unsupported predicate type %T in comprehension\", pred)\n\t}\n}\n\n// renderTagStartsWith generates SQL for tags.exists(t, t.startsWith(\"prefix\")).\nfunc (r *renderer) renderTagStartsWith(field Field, prefix string, _ ComprehensionKind) (renderResult, error) {\n\tarrayExpr := jsonArrayExpr(r.dialect, field)\n\n\tswitch r.dialect {\n\tcase DialectSQLite, DialectMySQL:\n\t\t// Match exact tag or tags with this prefix (hierarchical support)\n\t\texactMatch := r.buildJSONArrayLike(arrayExpr, fmt.Sprintf(`%%\"%s\"%%`, prefix))\n\t\tprefixMatch := r.buildJSONArrayLike(arrayExpr, fmt.Sprintf(`%%\"%s%%`, prefix))\n\t\tcondition := fmt.Sprintf(\"(%s OR %s)\", exactMatch, prefixMatch)\n\t\treturn renderResult{sql: r.wrapWithNullCheck(arrayExpr, condition)}, nil\n\n\tcase DialectPostgres:\n\t\t// Use PostgreSQL's powerful JSON operators\n\t\texactMatch := fmt.Sprintf(\"%s @> jsonb_build_array(%s::json)\", arrayExpr, r.addArg(fmt.Sprintf(`\"%s\"`, prefix)))\n\t\tprefixMatch := fmt.Sprintf(\"(%s)::text LIKE %s\", arrayExpr, r.addArg(fmt.Sprintf(`%%\"%s%%`, prefix)))\n\t\tcondition := fmt.Sprintf(\"(%s OR %s)\", exactMatch, prefixMatch)\n\t\treturn renderResult{sql: r.wrapWithNullCheck(arrayExpr, condition)}, nil\n\n\tdefault:\n\t\treturn renderResult{}, errors.Errorf(\"unsupported dialect %s\", r.dialect)\n\t}\n}\n\n// renderTagEndsWith generates SQL for tags.exists(t, t.endsWith(\"suffix\")).\nfunc (r *renderer) renderTagEndsWith(field Field, suffix string, _ ComprehensionKind) (renderResult, error) {\n\tarrayExpr := jsonArrayExpr(r.dialect, field)\n\tpattern := fmt.Sprintf(`%%%s\"%%`, suffix)\n\n\tlikeExpr := r.buildJSONArrayLike(arrayExpr, pattern)\n\treturn renderResult{sql: r.wrapWithNullCheck(arrayExpr, likeExpr)}, nil\n}\n\n// renderTagContains generates SQL for tags.exists(t, t.contains(\"substring\")).\nfunc (r *renderer) renderTagContains(field Field, substring string, _ ComprehensionKind) (renderResult, error) {\n\tarrayExpr := jsonArrayExpr(r.dialect, field)\n\tpattern := fmt.Sprintf(`%%%s%%`, substring)\n\n\tlikeExpr := r.buildJSONArrayLike(arrayExpr, pattern)\n\treturn renderResult{sql: r.wrapWithNullCheck(arrayExpr, likeExpr)}, nil\n}\n\n// buildJSONArrayLike builds a LIKE expression for matching within a JSON array.\n// Returns the LIKE clause without NULL/empty checks.\nfunc (r *renderer) buildJSONArrayLike(arrayExpr, pattern string) string {\n\tswitch r.dialect {\n\tcase DialectSQLite, DialectMySQL:\n\t\treturn fmt.Sprintf(\"%s LIKE %s\", arrayExpr, r.addArg(pattern))\n\tcase DialectPostgres:\n\t\treturn fmt.Sprintf(\"(%s)::text LIKE %s\", arrayExpr, r.addArg(pattern))\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\n// wrapWithNullCheck wraps a condition with NULL and empty array checks.\n// This ensures we don't match against NULL or empty JSON arrays.\nfunc (r *renderer) wrapWithNullCheck(arrayExpr, condition string) string {\n\tvar nullCheck string\n\tswitch r.dialect {\n\tcase DialectSQLite:\n\t\tnullCheck = fmt.Sprintf(\"%s IS NOT NULL AND %s != '[]'\", arrayExpr, arrayExpr)\n\tcase DialectMySQL:\n\t\tnullCheck = fmt.Sprintf(\"%s IS NOT NULL AND JSON_LENGTH(%s) > 0\", arrayExpr, arrayExpr)\n\tcase DialectPostgres:\n\t\tnullCheck = fmt.Sprintf(\"%s IS NOT NULL AND jsonb_array_length(%s) > 0\", arrayExpr, arrayExpr)\n\tdefault:\n\t\treturn condition\n\t}\n\treturn fmt.Sprintf(\"(%s AND %s)\", condition, nullCheck)\n}\n\nfunc (r *renderer) jsonBoolPredicate(field Field) (string, error) {\n\texpr := jsonExtractExpr(r.dialect, field)\n\tswitch r.dialect {\n\tcase DialectSQLite:\n\t\treturn fmt.Sprintf(\"%s IS TRUE\", expr), nil\n\tcase DialectMySQL:\n\t\treturn fmt.Sprintf(\"COALESCE(%s, CAST('false' AS JSON)) = CAST('true' AS JSON)\", expr), nil\n\tcase DialectPostgres:\n\t\treturn fmt.Sprintf(\"(%s)::boolean IS TRUE\", expr), nil\n\tdefault:\n\t\treturn \"\", errors.Errorf(\"unsupported dialect %s\", r.dialect)\n\t}\n}\n\nfunc combineAnd(left, right renderResult) renderResult {\n\tif left.unsatisfiable || right.unsatisfiable {\n\t\treturn renderResult{sql: \"1 = 0\", unsatisfiable: true}\n\t}\n\tif left.trivial {\n\t\treturn right\n\t}\n\tif right.trivial {\n\t\treturn left\n\t}\n\treturn renderResult{\n\t\tsql: fmt.Sprintf(\"(%s AND %s)\", left.sql, right.sql),\n\t}\n}\n\nfunc combineOr(left, right renderResult) renderResult {\n\tif left.trivial || right.trivial {\n\t\treturn renderResult{trivial: true}\n\t}\n\tif left.unsatisfiable {\n\t\treturn right\n\t}\n\tif right.unsatisfiable {\n\t\treturn left\n\t}\n\treturn renderResult{\n\t\tsql: fmt.Sprintf(\"(%s OR %s)\", left.sql, right.sql),\n\t}\n}\n\nfunc (r *renderer) addArg(value any) string {\n\tr.placeholderCounter++\n\tr.args = append(r.args, value)\n\tif r.dialect == DialectPostgres {\n\t\treturn fmt.Sprintf(\"$%d\", r.placeholderOffset+r.placeholderCounter)\n\t}\n\treturn \"?\"\n}\n\nfunc (r *renderer) addBoolArg(value bool) string {\n\tvar v any\n\tswitch r.dialect {\n\tcase DialectSQLite:\n\t\tif value {\n\t\t\tv = 1\n\t\t} else {\n\t\t\tv = 0\n\t\t}\n\tdefault:\n\t\tv = value\n\t}\n\treturn r.addArg(v)\n}\n\nfunc expectLiteral(expr ValueExpr) (any, error) {\n\tlit, ok := expr.(*LiteralValue)\n\tif !ok {\n\t\treturn nil, errors.New(\"expression must be a literal\")\n\t}\n\treturn lit.Value, nil\n}\n\nfunc expectBool(expr ValueExpr) (bool, error) {\n\tlit, err := expectLiteral(expr)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tvalue, ok := lit.(bool)\n\tif !ok {\n\t\treturn false, errors.New(\"boolean literal required\")\n\t}\n\treturn value, nil\n}\n\nfunc expectNumericLiteral(expr ValueExpr) (int64, error) {\n\tlit, err := expectLiteral(expr)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\treturn toInt64(lit)\n}\n\nfunc toInt64(value any) (int64, error) {\n\tswitch v := value.(type) {\n\tcase int:\n\t\treturn int64(v), nil\n\tcase int32:\n\t\treturn int64(v), nil\n\tcase int64:\n\t\treturn v, nil\n\tcase uint32:\n\t\treturn int64(v), nil\n\tcase uint64:\n\t\treturn int64(v), nil\n\tcase float32:\n\t\treturn int64(v), nil\n\tcase float64:\n\t\treturn int64(v), nil\n\tdefault:\n\t\treturn 0, errors.Errorf(\"cannot convert %T to int64\", value)\n\t}\n}\n\nfunc sqlOperator(op ComparisonOperator) string {\n\treturn string(op)\n}\n\nfunc qualifyColumn(d DialectName, col Column) string {\n\tswitch d {\n\tcase DialectPostgres:\n\t\treturn fmt.Sprintf(\"%s.%s\", col.Table, col.Name)\n\tdefault:\n\t\treturn fmt.Sprintf(\"`%s`.`%s`\", col.Table, col.Name)\n\t}\n}\n\nfunc jsonPath(field Field) string {\n\treturn \"$.\" + strings.Join(field.JSONPath, \".\")\n}\n\nfunc jsonExtractExpr(d DialectName, field Field) string {\n\tcolumn := qualifyColumn(d, field.Column)\n\tswitch d {\n\tcase DialectSQLite, DialectMySQL:\n\t\treturn fmt.Sprintf(\"JSON_EXTRACT(%s, '%s')\", column, jsonPath(field))\n\tcase DialectPostgres:\n\t\treturn buildPostgresJSONAccessor(column, field.JSONPath, true)\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\nfunc jsonArrayExpr(d DialectName, field Field) string {\n\tcolumn := qualifyColumn(d, field.Column)\n\tswitch d {\n\tcase DialectSQLite, DialectMySQL:\n\t\treturn fmt.Sprintf(\"JSON_EXTRACT(%s, '%s')\", column, jsonPath(field))\n\tcase DialectPostgres:\n\t\treturn buildPostgresJSONAccessor(column, field.JSONPath, false)\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\nfunc jsonArrayLengthExpr(d DialectName, field Field) string {\n\tarrayExpr := jsonArrayExpr(d, field)\n\tswitch d {\n\tcase DialectSQLite:\n\t\treturn fmt.Sprintf(\"JSON_ARRAY_LENGTH(COALESCE(%s, JSON_ARRAY()))\", arrayExpr)\n\tcase DialectMySQL:\n\t\treturn fmt.Sprintf(\"JSON_LENGTH(COALESCE(%s, JSON_ARRAY()))\", arrayExpr)\n\tcase DialectPostgres:\n\t\treturn fmt.Sprintf(\"jsonb_array_length(COALESCE(%s, '[]'::jsonb))\", arrayExpr)\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\nfunc buildPostgresJSONAccessor(base string, path []string, terminalText bool) string {\n\texpr := base\n\tfor idx, part := range path {\n\t\tif idx == len(path)-1 && terminalText {\n\t\t\texpr = fmt.Sprintf(\"%s->>'%s'\", expr, part)\n\t\t} else {\n\t\t\texpr = fmt.Sprintf(\"%s->'%s'\", expr, part)\n\t\t}\n\t}\n\treturn expr\n}\n"
  },
  {
    "path": "plugin/filter/schema.go",
    "content": "package filter\n\nimport (\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/google/cel-go/cel\"\n\t\"github.com/google/cel-go/common/types\"\n\t\"github.com/google/cel-go/common/types/ref\"\n)\n\n// DialectName enumerates supported SQL dialects.\ntype DialectName string\n\nconst (\n\tDialectSQLite   DialectName = \"sqlite\"\n\tDialectMySQL    DialectName = \"mysql\"\n\tDialectPostgres DialectName = \"postgres\"\n)\n\n// FieldType represents the logical type of a field.\ntype FieldType string\n\nconst (\n\tFieldTypeString    FieldType = \"string\"\n\tFieldTypeInt       FieldType = \"int\"\n\tFieldTypeBool      FieldType = \"bool\"\n\tFieldTypeTimestamp FieldType = \"timestamp\"\n)\n\n// FieldKind describes how a field is stored.\ntype FieldKind string\n\nconst (\n\tFieldKindScalar       FieldKind = \"scalar\"\n\tFieldKindBoolColumn   FieldKind = \"bool_column\"\n\tFieldKindJSONBool     FieldKind = \"json_bool\"\n\tFieldKindJSONList     FieldKind = \"json_list\"\n\tFieldKindVirtualAlias FieldKind = \"virtual_alias\"\n)\n\n// Column identifies the backing table column.\ntype Column struct {\n\tTable string\n\tName  string\n}\n\n// Field captures the schema metadata for an exposed CEL identifier.\ntype Field struct {\n\tName                 string\n\tKind                 FieldKind\n\tType                 FieldType\n\tColumn               Column\n\tJSONPath             []string\n\tAliasFor             string\n\tSupportsContains     bool\n\tExpressions          map[DialectName]string\n\tAllowedComparisonOps map[ComparisonOperator]bool\n}\n\n// Schema collects CEL environment options and field metadata.\ntype Schema struct {\n\tName       string\n\tFields     map[string]Field\n\tEnvOptions []cel.EnvOption\n}\n\n// Field returns the field metadata if present.\nfunc (s Schema) Field(name string) (Field, bool) {\n\tf, ok := s.Fields[name]\n\treturn f, ok\n}\n\n// ResolveAlias resolves a virtual alias to its target field.\nfunc (s Schema) ResolveAlias(name string) (Field, bool) {\n\tfield, ok := s.Fields[name]\n\tif !ok {\n\t\treturn Field{}, false\n\t}\n\tif field.Kind == FieldKindVirtualAlias {\n\t\ttarget, ok := s.Fields[field.AliasFor]\n\t\tif !ok {\n\t\t\treturn Field{}, false\n\t\t}\n\t\treturn target, true\n\t}\n\treturn field, true\n}\n\nvar nowFunction = cel.Function(\"now\",\n\tcel.Overload(\"now\",\n\t\t[]*cel.Type{},\n\t\tcel.IntType,\n\t\tcel.FunctionBinding(func(_ ...ref.Val) ref.Val {\n\t\t\treturn types.Int(time.Now().Unix())\n\t\t}),\n\t),\n)\n\n// NewSchema constructs the memo filter schema and CEL environment.\nfunc NewSchema() Schema {\n\tfields := map[string]Field{\n\t\t\"content\": {\n\t\t\tName:             \"content\",\n\t\t\tKind:             FieldKindScalar,\n\t\t\tType:             FieldTypeString,\n\t\t\tColumn:           Column{Table: \"memo\", Name: \"content\"},\n\t\t\tSupportsContains: true,\n\t\t\tExpressions:      map[DialectName]string{},\n\t\t},\n\t\t\"creator_id\": {\n\t\t\tName:        \"creator_id\",\n\t\t\tKind:        FieldKindScalar,\n\t\t\tType:        FieldTypeInt,\n\t\t\tColumn:      Column{Table: \"memo\", Name: \"creator_id\"},\n\t\t\tExpressions: map[DialectName]string{},\n\t\t\tAllowedComparisonOps: map[ComparisonOperator]bool{\n\t\t\t\tCompareEq:  true,\n\t\t\t\tCompareNeq: true,\n\t\t\t},\n\t\t},\n\t\t\"created_ts\": {\n\t\t\tName:   \"created_ts\",\n\t\t\tKind:   FieldKindScalar,\n\t\t\tType:   FieldTypeTimestamp,\n\t\t\tColumn: Column{Table: \"memo\", Name: \"created_ts\"},\n\t\t\tExpressions: map[DialectName]string{\n\t\t\t\t// MySQL stores created_ts as TIMESTAMP, needs conversion to epoch\n\t\t\t\tDialectMySQL: \"UNIX_TIMESTAMP(%s)\",\n\t\t\t\t// PostgreSQL and SQLite store created_ts as BIGINT (epoch), no conversion needed\n\t\t\t\tDialectPostgres: \"%s\",\n\t\t\t\tDialectSQLite:   \"%s\",\n\t\t\t},\n\t\t},\n\t\t\"updated_ts\": {\n\t\t\tName:   \"updated_ts\",\n\t\t\tKind:   FieldKindScalar,\n\t\t\tType:   FieldTypeTimestamp,\n\t\t\tColumn: Column{Table: \"memo\", Name: \"updated_ts\"},\n\t\t\tExpressions: map[DialectName]string{\n\t\t\t\t// MySQL stores updated_ts as TIMESTAMP, needs conversion to epoch\n\t\t\t\tDialectMySQL: \"UNIX_TIMESTAMP(%s)\",\n\t\t\t\t// PostgreSQL and SQLite store updated_ts as BIGINT (epoch), no conversion needed\n\t\t\t\tDialectPostgres: \"%s\",\n\t\t\t\tDialectSQLite:   \"%s\",\n\t\t\t},\n\t\t},\n\t\t\"pinned\": {\n\t\t\tName:        \"pinned\",\n\t\t\tKind:        FieldKindBoolColumn,\n\t\t\tType:        FieldTypeBool,\n\t\t\tColumn:      Column{Table: \"memo\", Name: \"pinned\"},\n\t\t\tExpressions: map[DialectName]string{},\n\t\t\tAllowedComparisonOps: map[ComparisonOperator]bool{\n\t\t\t\tCompareEq:  true,\n\t\t\t\tCompareNeq: true,\n\t\t\t},\n\t\t},\n\t\t\"visibility\": {\n\t\t\tName:        \"visibility\",\n\t\t\tKind:        FieldKindScalar,\n\t\t\tType:        FieldTypeString,\n\t\t\tColumn:      Column{Table: \"memo\", Name: \"visibility\"},\n\t\t\tExpressions: map[DialectName]string{},\n\t\t\tAllowedComparisonOps: map[ComparisonOperator]bool{\n\t\t\t\tCompareEq:  true,\n\t\t\t\tCompareNeq: true,\n\t\t\t},\n\t\t},\n\t\t\"tags\": {\n\t\t\tName:     \"tags\",\n\t\t\tKind:     FieldKindJSONList,\n\t\t\tType:     FieldTypeString,\n\t\t\tColumn:   Column{Table: \"memo\", Name: \"payload\"},\n\t\t\tJSONPath: []string{\"tags\"},\n\t\t},\n\t\t\"tag\": {\n\t\t\tName:     \"tag\",\n\t\t\tKind:     FieldKindVirtualAlias,\n\t\t\tType:     FieldTypeString,\n\t\t\tAliasFor: \"tags\",\n\t\t},\n\t\t\"has_task_list\": {\n\t\t\tName:     \"has_task_list\",\n\t\t\tKind:     FieldKindJSONBool,\n\t\t\tType:     FieldTypeBool,\n\t\t\tColumn:   Column{Table: \"memo\", Name: \"payload\"},\n\t\t\tJSONPath: []string{\"property\", \"hasTaskList\"},\n\t\t\tAllowedComparisonOps: map[ComparisonOperator]bool{\n\t\t\t\tCompareEq:  true,\n\t\t\t\tCompareNeq: true,\n\t\t\t},\n\t\t},\n\t\t\"has_link\": {\n\t\t\tName:     \"has_link\",\n\t\t\tKind:     FieldKindJSONBool,\n\t\t\tType:     FieldTypeBool,\n\t\t\tColumn:   Column{Table: \"memo\", Name: \"payload\"},\n\t\t\tJSONPath: []string{\"property\", \"hasLink\"},\n\t\t\tAllowedComparisonOps: map[ComparisonOperator]bool{\n\t\t\t\tCompareEq:  true,\n\t\t\t\tCompareNeq: true,\n\t\t\t},\n\t\t},\n\t\t\"has_code\": {\n\t\t\tName:     \"has_code\",\n\t\t\tKind:     FieldKindJSONBool,\n\t\t\tType:     FieldTypeBool,\n\t\t\tColumn:   Column{Table: \"memo\", Name: \"payload\"},\n\t\t\tJSONPath: []string{\"property\", \"hasCode\"},\n\t\t\tAllowedComparisonOps: map[ComparisonOperator]bool{\n\t\t\t\tCompareEq:  true,\n\t\t\t\tCompareNeq: true,\n\t\t\t},\n\t\t},\n\t\t\"has_incomplete_tasks\": {\n\t\t\tName:     \"has_incomplete_tasks\",\n\t\t\tKind:     FieldKindJSONBool,\n\t\t\tType:     FieldTypeBool,\n\t\t\tColumn:   Column{Table: \"memo\", Name: \"payload\"},\n\t\t\tJSONPath: []string{\"property\", \"hasIncompleteTasks\"},\n\t\t\tAllowedComparisonOps: map[ComparisonOperator]bool{\n\t\t\t\tCompareEq:  true,\n\t\t\t\tCompareNeq: true,\n\t\t\t},\n\t\t},\n\t}\n\n\tenvOptions := []cel.EnvOption{\n\t\tcel.Variable(\"content\", cel.StringType),\n\t\tcel.Variable(\"creator_id\", cel.IntType),\n\t\tcel.Variable(\"created_ts\", cel.IntType),\n\t\tcel.Variable(\"updated_ts\", cel.IntType),\n\t\tcel.Variable(\"pinned\", cel.BoolType),\n\t\tcel.Variable(\"tag\", cel.StringType),\n\t\tcel.Variable(\"tags\", cel.ListType(cel.StringType)),\n\t\tcel.Variable(\"visibility\", cel.StringType),\n\t\tcel.Variable(\"has_task_list\", cel.BoolType),\n\t\tcel.Variable(\"has_link\", cel.BoolType),\n\t\tcel.Variable(\"has_code\", cel.BoolType),\n\t\tcel.Variable(\"has_incomplete_tasks\", cel.BoolType),\n\t\tnowFunction,\n\t}\n\n\treturn Schema{\n\t\tName:       \"memo\",\n\t\tFields:     fields,\n\t\tEnvOptions: envOptions,\n\t}\n}\n\n// NewAttachmentSchema constructs the attachment filter schema and CEL environment.\nfunc NewAttachmentSchema() Schema {\n\tfields := map[string]Field{\n\t\t\"filename\": {\n\t\t\tName:             \"filename\",\n\t\t\tKind:             FieldKindScalar,\n\t\t\tType:             FieldTypeString,\n\t\t\tColumn:           Column{Table: \"attachment\", Name: \"filename\"},\n\t\t\tSupportsContains: true,\n\t\t\tExpressions:      map[DialectName]string{},\n\t\t},\n\t\t\"mime_type\": {\n\t\t\tName:        \"mime_type\",\n\t\t\tKind:        FieldKindScalar,\n\t\t\tType:        FieldTypeString,\n\t\t\tColumn:      Column{Table: \"attachment\", Name: \"type\"},\n\t\t\tExpressions: map[DialectName]string{},\n\t\t},\n\t\t\"create_time\": {\n\t\t\tName:   \"create_time\",\n\t\t\tKind:   FieldKindScalar,\n\t\t\tType:   FieldTypeTimestamp,\n\t\t\tColumn: Column{Table: \"attachment\", Name: \"created_ts\"},\n\t\t\tExpressions: map[DialectName]string{\n\t\t\t\t// MySQL stores created_ts as TIMESTAMP, needs conversion to epoch\n\t\t\t\tDialectMySQL: \"UNIX_TIMESTAMP(%s)\",\n\t\t\t\t// PostgreSQL and SQLite store created_ts as BIGINT (epoch), no conversion needed\n\t\t\t\tDialectPostgres: \"%s\",\n\t\t\t\tDialectSQLite:   \"%s\",\n\t\t\t},\n\t\t},\n\t\t\"memo_id\": {\n\t\t\tName:        \"memo_id\",\n\t\t\tKind:        FieldKindScalar,\n\t\t\tType:        FieldTypeInt,\n\t\t\tColumn:      Column{Table: \"attachment\", Name: \"memo_id\"},\n\t\t\tExpressions: map[DialectName]string{},\n\t\t\tAllowedComparisonOps: map[ComparisonOperator]bool{\n\t\t\t\tCompareEq:  true,\n\t\t\t\tCompareNeq: true,\n\t\t\t},\n\t\t},\n\t}\n\n\tenvOptions := []cel.EnvOption{\n\t\tcel.Variable(\"filename\", cel.StringType),\n\t\tcel.Variable(\"mime_type\", cel.StringType),\n\t\tcel.Variable(\"create_time\", cel.IntType),\n\t\tcel.Variable(\"memo_id\", cel.AnyType),\n\t\tnowFunction,\n\t}\n\n\treturn Schema{\n\t\tName:       \"attachment\",\n\t\tFields:     fields,\n\t\tEnvOptions: envOptions,\n\t}\n}\n\n// columnExpr returns the field expression for the given dialect, applying\n// any schema-specific overrides (e.g. UNIX timestamp conversions).\nfunc (f Field) columnExpr(d DialectName) string {\n\tbase := qualifyColumn(d, f.Column)\n\tif expr, ok := f.Expressions[d]; ok && expr != \"\" {\n\t\treturn fmt.Sprintf(expr, base)\n\t}\n\treturn base\n}\n"
  },
  {
    "path": "plugin/httpgetter/html_meta.go",
    "content": "package httpgetter\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/url\"\n\n\t\"github.com/pkg/errors\"\n\t\"golang.org/x/net/html\"\n\t\"golang.org/x/net/html/atom\"\n)\n\nvar ErrInternalIP = errors.New(\"internal IP addresses are not allowed\")\n\nvar httpClient = &http.Client{\n\tCheckRedirect: func(req *http.Request, via []*http.Request) error {\n\t\tif err := validateURL(req.URL.String()); err != nil {\n\t\t\treturn errors.Wrap(err, \"redirect to internal IP\")\n\t\t}\n\t\tif len(via) >= 10 {\n\t\t\treturn errors.New(\"too many redirects\")\n\t\t}\n\t\treturn nil\n\t},\n}\n\ntype HTMLMeta struct {\n\tTitle       string `json:\"title\"`\n\tDescription string `json:\"description\"`\n\tImage       string `json:\"image\"`\n}\n\nfunc GetHTMLMeta(urlStr string) (*HTMLMeta, error) {\n\tif err := validateURL(urlStr); err != nil {\n\t\treturn nil, err\n\t}\n\n\tresponse, err := httpClient.Get(urlStr)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer response.Body.Close()\n\n\tmediatype, err := getMediatype(response)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif mediatype != \"text/html\" {\n\t\treturn nil, errors.New(\"not a HTML page\")\n\t}\n\n\t// TODO: limit the size of the response body\n\n\thtmlMeta := extractHTMLMeta(response.Body)\n\tenrichSiteMeta(response.Request.URL, htmlMeta)\n\treturn htmlMeta, nil\n}\n\nfunc extractHTMLMeta(resp io.Reader) *HTMLMeta {\n\ttokenizer := html.NewTokenizer(resp)\n\thtmlMeta := new(HTMLMeta)\n\n\tfor {\n\t\ttokenType := tokenizer.Next()\n\t\tif tokenType == html.ErrorToken {\n\t\t\tbreak\n\t\t} else if tokenType == html.StartTagToken || tokenType == html.SelfClosingTagToken {\n\t\t\ttoken := tokenizer.Token()\n\t\t\tif token.DataAtom == atom.Body {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tif token.DataAtom == atom.Title {\n\t\t\t\ttokenizer.Next()\n\t\t\t\ttoken := tokenizer.Token()\n\t\t\t\thtmlMeta.Title = token.Data\n\t\t\t} else if token.DataAtom == atom.Meta {\n\t\t\t\tdescription, ok := extractMetaProperty(token, \"description\")\n\t\t\t\tif ok {\n\t\t\t\t\thtmlMeta.Description = description\n\t\t\t\t}\n\n\t\t\t\togTitle, ok := extractMetaProperty(token, \"og:title\")\n\t\t\t\tif ok {\n\t\t\t\t\thtmlMeta.Title = ogTitle\n\t\t\t\t}\n\n\t\t\t\togDescription, ok := extractMetaProperty(token, \"og:description\")\n\t\t\t\tif ok {\n\t\t\t\t\thtmlMeta.Description = ogDescription\n\t\t\t\t}\n\n\t\t\t\togImage, ok := extractMetaProperty(token, \"og:image\")\n\t\t\t\tif ok {\n\t\t\t\t\thtmlMeta.Image = ogImage\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn htmlMeta\n}\n\nfunc extractMetaProperty(token html.Token, prop string) (content string, ok bool) {\n\tcontent, ok = \"\", false\n\tfor _, attr := range token.Attr {\n\t\tif attr.Key == \"property\" && attr.Val == prop {\n\t\t\tok = true\n\t\t}\n\t\tif attr.Key == \"content\" {\n\t\t\tcontent = attr.Val\n\t\t}\n\t}\n\treturn content, ok\n}\n\nfunc validateURL(urlStr string) error {\n\tu, err := url.Parse(urlStr)\n\tif err != nil {\n\t\treturn errors.New(\"invalid URL format\")\n\t}\n\n\tif u.Scheme != \"http\" && u.Scheme != \"https\" {\n\t\treturn errors.New(\"only http/https protocols are allowed\")\n\t}\n\n\thost := u.Hostname()\n\tif host == \"\" {\n\t\treturn errors.New(\"empty hostname\")\n\t}\n\n\t// check if the hostname is an IP\n\tif ip := net.ParseIP(host); ip != nil {\n\t\tif ip.IsLoopback() || ip.IsPrivate() || ip.IsLinkLocalUnicast() {\n\t\t\treturn errors.Wrap(ErrInternalIP, ip.String())\n\t\t}\n\t\treturn nil\n\t}\n\n\t// check if it's a hostname, resolve it and check all returned IPs\n\tips, err := net.LookupIP(host)\n\tif err != nil {\n\t\treturn errors.Errorf(\"failed to resolve hostname: %v\", err)\n\t}\n\n\tfor _, ip := range ips {\n\t\tif ip.IsLoopback() || ip.IsPrivate() || ip.IsLinkLocalUnicast() {\n\t\t\treturn errors.Wrapf(ErrInternalIP, \"host=%s, ip=%s\", host, ip.String())\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc enrichSiteMeta(url *url.URL, meta *HTMLMeta) {\n\tif url.Hostname() == \"www.youtube.com\" {\n\t\tif url.Path == \"/watch\" {\n\t\t\tvid := url.Query().Get(\"v\")\n\t\t\tif vid != \"\" {\n\t\t\t\tmeta.Image = fmt.Sprintf(\"https://img.youtube.com/vi/%s/mqdefault.jpg\", vid)\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "plugin/httpgetter/html_meta_test.go",
    "content": "package httpgetter\n\nimport (\n\t\"errors\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestGetHTMLMeta(t *testing.T) {\n\ttests := []struct {\n\t\turlStr   string\n\t\thtmlMeta HTMLMeta\n\t}{}\n\tfor _, test := range tests {\n\t\tmetadata, err := GetHTMLMeta(test.urlStr)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, test.htmlMeta, *metadata)\n\t}\n}\n\nfunc TestGetHTMLMetaForInternal(t *testing.T) {\n\t// test for internal IP\n\tif _, err := GetHTMLMeta(\"http://192.168.0.1\"); !errors.Is(err, ErrInternalIP) {\n\t\tt.Errorf(\"Expected error for internal IP, got %v\", err)\n\t}\n\n\t// test for resolved internal IP\n\tif _, err := GetHTMLMeta(\"http://localhost\"); !errors.Is(err, ErrInternalIP) {\n\t\tt.Errorf(\"Expected error for resolved internal IP, got %v\", err)\n\t}\n}\n"
  },
  {
    "path": "plugin/httpgetter/http_getter.go",
    "content": "package httpgetter\n"
  },
  {
    "path": "plugin/httpgetter/image.go",
    "content": "package httpgetter\n\nimport (\n\t\"errors\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n)\n\ntype Image struct {\n\tBlob      []byte\n\tMediatype string\n}\n\nfunc GetImage(urlStr string) (*Image, error) {\n\tif _, err := url.Parse(urlStr); err != nil {\n\t\treturn nil, err\n\t}\n\n\tresponse, err := http.Get(urlStr)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer response.Body.Close()\n\n\tmediatype, err := getMediatype(response)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif !strings.HasPrefix(mediatype, \"image/\") {\n\t\treturn nil, errors.New(\"wrong image mediatype\")\n\t}\n\n\tbodyBytes, err := io.ReadAll(response.Body)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\timage := &Image{\n\t\tBlob:      bodyBytes,\n\t\tMediatype: mediatype,\n\t}\n\treturn image, nil\n}\n"
  },
  {
    "path": "plugin/httpgetter/util.go",
    "content": "package httpgetter\n\nimport (\n\t\"mime\"\n\t\"net/http\"\n)\n\nfunc getMediatype(response *http.Response) (string, error) {\n\tcontentType := response.Header.Get(\"content-type\")\n\tmediatype, _, err := mime.ParseMediaType(contentType)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn mediatype, nil\n}\n"
  },
  {
    "path": "plugin/idp/idp.go",
    "content": "package idp\n\ntype IdentityProviderUserInfo struct {\n\tIdentifier  string\n\tDisplayName string\n\tEmail       string\n\tAvatarURL   string\n}\n"
  },
  {
    "path": "plugin/idp/oauth2/oauth2.go",
    "content": "// Package oauth2 is the plugin for OAuth2 Identity Provider.\npackage oauth2\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"log/slog\"\n\t\"net/http\"\n\n\t\"github.com/pkg/errors\"\n\t\"golang.org/x/oauth2\"\n\n\t\"github.com/usememos/memos/plugin/idp\"\n\tstorepb \"github.com/usememos/memos/proto/gen/store\"\n)\n\n// IdentityProvider represents an OAuth2 Identity Provider.\ntype IdentityProvider struct {\n\tconfig *storepb.OAuth2Config\n}\n\n// NewIdentityProvider initializes a new OAuth2 Identity Provider with the given configuration.\nfunc NewIdentityProvider(config *storepb.OAuth2Config) (*IdentityProvider, error) {\n\tfor v, field := range map[string]string{\n\t\tconfig.ClientId:                \"clientId\",\n\t\tconfig.ClientSecret:            \"clientSecret\",\n\t\tconfig.TokenUrl:                \"tokenUrl\",\n\t\tconfig.UserInfoUrl:             \"userInfoUrl\",\n\t\tconfig.FieldMapping.Identifier: \"fieldMapping.identifier\",\n\t} {\n\t\tif v == \"\" {\n\t\t\treturn nil, errors.Errorf(`the field \"%s\" is empty but required`, field)\n\t\t}\n\t}\n\n\treturn &IdentityProvider{\n\t\tconfig: config,\n\t}, nil\n}\n\n// ExchangeToken returns the exchanged OAuth2 token using the given authorization code.\n// If codeVerifier is provided, it will be used for PKCE (Proof Key for Code Exchange) validation.\nfunc (p *IdentityProvider) ExchangeToken(ctx context.Context, redirectURL, code, codeVerifier string) (string, error) {\n\tconf := &oauth2.Config{\n\t\tClientID:     p.config.ClientId,\n\t\tClientSecret: p.config.ClientSecret,\n\t\tRedirectURL:  redirectURL,\n\t\tScopes:       p.config.Scopes,\n\t\tEndpoint: oauth2.Endpoint{\n\t\t\tAuthURL:   p.config.AuthUrl,\n\t\t\tTokenURL:  p.config.TokenUrl,\n\t\t\tAuthStyle: oauth2.AuthStyleInParams,\n\t\t},\n\t}\n\n\t// Prepare token exchange options\n\topts := []oauth2.AuthCodeOption{}\n\n\t// Add PKCE code_verifier if provided\n\tif codeVerifier != \"\" {\n\t\topts = append(opts, oauth2.SetAuthURLParam(\"code_verifier\", codeVerifier))\n\t}\n\n\ttoken, err := conf.Exchange(ctx, code, opts...)\n\tif err != nil {\n\t\treturn \"\", errors.Wrap(err, \"failed to exchange access token\")\n\t}\n\n\t// Use the standard AccessToken field instead of Extra()\n\t// This is more reliable across different OAuth providers\n\tif token.AccessToken == \"\" {\n\t\treturn \"\", errors.New(\"missing access token from authorization response\")\n\t}\n\n\treturn token.AccessToken, nil\n}\n\n// UserInfo returns the parsed user information using the given OAuth2 token.\nfunc (p *IdentityProvider) UserInfo(token string) (*idp.IdentityProviderUserInfo, error) {\n\tclient := &http.Client{}\n\treq, err := http.NewRequest(http.MethodGet, p.config.UserInfoUrl, nil)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"failed to create http request\")\n\t}\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\treq.Header.Set(\"Authorization\", fmt.Sprintf(\"Bearer %s\", token))\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"failed to get user information\")\n\t}\n\tdefer resp.Body.Close()\n\n\tbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"failed to read response body\")\n\t}\n\n\tvar claims map[string]any\n\tif err := json.Unmarshal(body, &claims); err != nil {\n\t\treturn nil, errors.Wrap(err, \"failed to unmarshal response body\")\n\t}\n\tslog.Info(\"user info claims\", \"claims\", claims)\n\tuserInfo := &idp.IdentityProviderUserInfo{}\n\tif v, ok := claims[p.config.FieldMapping.Identifier].(string); ok {\n\t\tuserInfo.Identifier = v\n\t}\n\tif userInfo.Identifier == \"\" {\n\t\treturn nil, errors.Errorf(\"the field %q is not found in claims or has empty value\", p.config.FieldMapping.Identifier)\n\t}\n\n\t// Best effort to map optional fields\n\tif p.config.FieldMapping.DisplayName != \"\" {\n\t\tif v, ok := claims[p.config.FieldMapping.DisplayName].(string); ok {\n\t\t\tuserInfo.DisplayName = v\n\t\t}\n\t}\n\tif userInfo.DisplayName == \"\" {\n\t\tuserInfo.DisplayName = userInfo.Identifier\n\t}\n\tif p.config.FieldMapping.Email != \"\" {\n\t\tif v, ok := claims[p.config.FieldMapping.Email].(string); ok {\n\t\t\tuserInfo.Email = v\n\t\t}\n\t}\n\tif p.config.FieldMapping.AvatarUrl != \"\" {\n\t\tif v, ok := claims[p.config.FieldMapping.AvatarUrl].(string); ok {\n\t\t\tuserInfo.AvatarURL = v\n\t\t}\n\t}\n\tslog.Info(\"user info\", \"userInfo\", userInfo)\n\treturn userInfo, nil\n}\n"
  },
  {
    "path": "plugin/idp/oauth2/oauth2_test.go",
    "content": "package oauth2\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/usememos/memos/plugin/idp\"\n\tstorepb \"github.com/usememos/memos/proto/gen/store\"\n)\n\nfunc TestNewIdentityProvider(t *testing.T) {\n\ttests := []struct {\n\t\tname        string\n\t\tconfig      *storepb.OAuth2Config\n\t\tcontainsErr string\n\t}{\n\t\t{\n\t\t\tname: \"no tokenUrl\",\n\t\t\tconfig: &storepb.OAuth2Config{\n\t\t\t\tClientId:     \"test-client-id\",\n\t\t\t\tClientSecret: \"test-client-secret\",\n\t\t\t\tAuthUrl:      \"\",\n\t\t\t\tTokenUrl:     \"\",\n\t\t\t\tUserInfoUrl:  \"https://example.com/api/user\",\n\t\t\t\tFieldMapping: &storepb.FieldMapping{\n\t\t\t\t\tIdentifier: \"login\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tcontainsErr: `the field \"tokenUrl\" is empty but required`,\n\t\t},\n\t\t{\n\t\t\tname: \"no userInfoUrl\",\n\t\t\tconfig: &storepb.OAuth2Config{\n\t\t\t\tClientId:     \"test-client-id\",\n\t\t\t\tClientSecret: \"test-client-secret\",\n\t\t\t\tAuthUrl:      \"\",\n\t\t\t\tTokenUrl:     \"https://example.com/token\",\n\t\t\t\tUserInfoUrl:  \"\",\n\t\t\t\tFieldMapping: &storepb.FieldMapping{\n\t\t\t\t\tIdentifier: \"login\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tcontainsErr: `the field \"userInfoUrl\" is empty but required`,\n\t\t},\n\t\t{\n\t\t\tname: \"no field mapping identifier\",\n\t\t\tconfig: &storepb.OAuth2Config{\n\t\t\t\tClientId:     \"test-client-id\",\n\t\t\t\tClientSecret: \"test-client-secret\",\n\t\t\t\tAuthUrl:      \"\",\n\t\t\t\tTokenUrl:     \"https://example.com/token\",\n\t\t\t\tUserInfoUrl:  \"https://example.com/api/user\",\n\t\t\t\tFieldMapping: &storepb.FieldMapping{\n\t\t\t\t\tIdentifier: \"\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tcontainsErr: `the field \"fieldMapping.identifier\" is empty but required`,\n\t\t},\n\t}\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(*testing.T) {\n\t\t\t_, err := NewIdentityProvider(test.config)\n\t\t\tassert.ErrorContains(t, err, test.containsErr)\n\t\t})\n\t}\n}\n\nfunc newMockServer(t *testing.T, code, accessToken string, userinfo []byte) *httptest.Server {\n\tmux := http.NewServeMux()\n\n\tvar rawIDToken string\n\tmux.HandleFunc(\"/oauth2/token\", func(w http.ResponseWriter, r *http.Request) {\n\t\trequire.Equal(t, http.MethodPost, r.Method)\n\n\t\tbody, err := io.ReadAll(r.Body)\n\t\trequire.NoError(t, err)\n\t\tvals, err := url.ParseQuery(string(body))\n\t\trequire.NoError(t, err)\n\n\t\trequire.Equal(t, code, vals.Get(\"code\"))\n\t\trequire.Equal(t, \"authorization_code\", vals.Get(\"grant_type\"))\n\n\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\terr = json.NewEncoder(w).Encode(map[string]any{\n\t\t\t\"access_token\": accessToken,\n\t\t\t\"token_type\":   \"Bearer\",\n\t\t\t\"expires_in\":   3600,\n\t\t\t\"id_token\":     rawIDToken,\n\t\t})\n\t\trequire.NoError(t, err)\n\t})\n\tmux.HandleFunc(\"/oauth2/userinfo\", func(w http.ResponseWriter, _ *http.Request) {\n\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\t_, err := w.Write(userinfo)\n\t\trequire.NoError(t, err)\n\t})\n\n\ts := httptest.NewServer(mux)\n\n\treturn s\n}\n\nfunc TestIdentityProvider(t *testing.T) {\n\tctx := context.Background()\n\n\tconst (\n\t\ttestClientID    = \"test-client-id\"\n\t\ttestCode        = \"test-code\"\n\t\ttestAccessToken = \"test-access-token\"\n\t\ttestSubject     = \"123456789\"\n\t\ttestName        = \"John Doe\"\n\t\ttestEmail       = \"john.doe@example.com\"\n\t)\n\tuserInfo, err := json.Marshal(\n\t\tmap[string]any{\n\t\t\t\"sub\":   testSubject,\n\t\t\t\"name\":  testName,\n\t\t\t\"email\": testEmail,\n\t\t},\n\t)\n\trequire.NoError(t, err)\n\n\ts := newMockServer(t, testCode, testAccessToken, userInfo)\n\n\toauth2, err := NewIdentityProvider(\n\t\t&storepb.OAuth2Config{\n\t\t\tClientId:     testClientID,\n\t\t\tClientSecret: \"test-client-secret\",\n\t\t\tTokenUrl:     fmt.Sprintf(\"%s/oauth2/token\", s.URL),\n\t\t\tUserInfoUrl:  fmt.Sprintf(\"%s/oauth2/userinfo\", s.URL),\n\t\t\tFieldMapping: &storepb.FieldMapping{\n\t\t\t\tIdentifier:  \"sub\",\n\t\t\t\tDisplayName: \"name\",\n\t\t\t\tEmail:       \"email\",\n\t\t\t},\n\t\t},\n\t)\n\trequire.NoError(t, err)\n\n\tredirectURL := \"https://example.com/oauth/callback\"\n\t// Test without PKCE (backward compatibility)\n\toauthToken, err := oauth2.ExchangeToken(ctx, redirectURL, testCode, \"\")\n\trequire.NoError(t, err)\n\trequire.Equal(t, testAccessToken, oauthToken)\n\n\tuserInfoResult, err := oauth2.UserInfo(oauthToken)\n\trequire.NoError(t, err)\n\n\twantUserInfo := &idp.IdentityProviderUserInfo{\n\t\tIdentifier:  testSubject,\n\t\tDisplayName: testName,\n\t\tEmail:       testEmail,\n\t}\n\tassert.Equal(t, wantUserInfo, userInfoResult)\n}\n"
  },
  {
    "path": "plugin/markdown/ast/tag.go",
    "content": "package ast\n\nimport (\n\tgast \"github.com/yuin/goldmark/ast\"\n)\n\n// TagNode represents a #tag in the markdown AST.\ntype TagNode struct {\n\tgast.BaseInline\n\n\t// Tag name without the # prefix\n\tTag []byte\n}\n\n// KindTag is the NodeKind for TagNode.\nvar KindTag = gast.NewNodeKind(\"Tag\")\n\n// Kind returns KindTag.\nfunc (*TagNode) Kind() gast.NodeKind {\n\treturn KindTag\n}\n\n// Dump implements Node.Dump for debugging.\nfunc (n *TagNode) Dump(source []byte, level int) {\n\tgast.DumpHelper(n, source, level, map[string]string{\n\t\t\"Tag\": string(n.Tag),\n\t}, nil)\n}\n"
  },
  {
    "path": "plugin/markdown/extensions/tag.go",
    "content": "package extensions\n\nimport (\n\t\"github.com/yuin/goldmark\"\n\t\"github.com/yuin/goldmark/parser\"\n\t\"github.com/yuin/goldmark/util\"\n\n\tmparser \"github.com/usememos/memos/plugin/markdown/parser\"\n)\n\ntype tagExtension struct{}\n\n// TagExtension is a goldmark extension for #tag syntax.\nvar TagExtension = &tagExtension{}\n\n// Extend extends the goldmark parser with tag support.\nfunc (*tagExtension) Extend(m goldmark.Markdown) {\n\tm.Parser().AddOptions(\n\t\tparser.WithInlineParsers(\n\t\t\t// Priority 200 - run before standard link parser (500)\n\t\t\tutil.Prioritized(mparser.NewTagParser(), 200),\n\t\t),\n\t)\n}\n"
  },
  {
    "path": "plugin/markdown/markdown.go",
    "content": "package markdown\n\nimport (\n\t\"bytes\"\n\t\"strings\"\n\n\t\"github.com/yuin/goldmark\"\n\tgast \"github.com/yuin/goldmark/ast\"\n\t\"github.com/yuin/goldmark/extension\"\n\teast \"github.com/yuin/goldmark/extension/ast\"\n\t\"github.com/yuin/goldmark/parser\"\n\t\"github.com/yuin/goldmark/text\"\n\n\tmast \"github.com/usememos/memos/plugin/markdown/ast\"\n\t\"github.com/usememos/memos/plugin/markdown/extensions\"\n\t\"github.com/usememos/memos/plugin/markdown/renderer\"\n\tstorepb \"github.com/usememos/memos/proto/gen/store\"\n)\n\n// ExtractedData contains all metadata extracted from markdown in a single pass.\ntype ExtractedData struct {\n\tTags     []string\n\tProperty *storepb.MemoPayload_Property\n}\n\n// Service handles markdown metadata extraction.\n// It uses goldmark to parse markdown and extract tags, properties, and snippets.\n// HTML rendering is primarily done on frontend using markdown-it, but backend provides\n// RenderHTML for RSS feeds and other server-side rendering needs.\ntype Service interface {\n\t// ExtractAll extracts tags, properties, and references in a single parse (most efficient)\n\tExtractAll(content []byte) (*ExtractedData, error)\n\n\t// ExtractTags returns all #tags found in content\n\tExtractTags(content []byte) ([]string, error)\n\n\t// ExtractProperties computes boolean properties\n\tExtractProperties(content []byte) (*storepb.MemoPayload_Property, error)\n\n\t// RenderMarkdown renders goldmark AST back to markdown text\n\tRenderMarkdown(content []byte) (string, error)\n\n\t// RenderHTML renders markdown content to HTML\n\tRenderHTML(content []byte) (string, error)\n\n\t// GenerateSnippet creates plain text summary\n\tGenerateSnippet(content []byte, maxLength int) (string, error)\n\n\t// ValidateContent checks for syntax errors\n\tValidateContent(content []byte) error\n\n\t// RenameTag renames all occurrences of oldTag to newTag in content\n\tRenameTag(content []byte, oldTag, newTag string) (string, error)\n}\n\n// service implements the Service interface.\ntype service struct {\n\tmd goldmark.Markdown\n}\n\n// Option configures the markdown service.\ntype Option func(*config)\n\ntype config struct {\n\tenableTags bool\n}\n\n// WithTagExtension enables #tag parsing.\nfunc WithTagExtension() Option {\n\treturn func(c *config) {\n\t\tc.enableTags = true\n\t}\n}\n\n// NewService creates a new markdown service with the given options.\nfunc NewService(opts ...Option) Service {\n\tcfg := &config{}\n\tfor _, opt := range opts {\n\t\topt(cfg)\n\t}\n\n\texts := []goldmark.Extender{\n\t\textension.GFM, // GitHub Flavored Markdown (tables, strikethrough, task lists, autolinks)\n\t}\n\n\t// Add custom extensions based on config\n\tif cfg.enableTags {\n\t\texts = append(exts, extensions.TagExtension)\n\t}\n\n\tmd := goldmark.New(\n\t\tgoldmark.WithExtensions(exts...),\n\t\tgoldmark.WithParserOptions(\n\t\t\tparser.WithAutoHeadingID(), // Generate heading IDs\n\t\t),\n\t)\n\n\treturn &service{\n\t\tmd: md,\n\t}\n}\n\n// parse is an internal helper to parse content into AST.\nfunc (s *service) parse(content []byte) (gast.Node, error) {\n\treader := text.NewReader(content)\n\tdoc := s.md.Parser().Parse(reader)\n\treturn doc, nil\n}\n\n// ExtractTags returns all #tags found in content.\nfunc (s *service) ExtractTags(content []byte) ([]string, error) {\n\troot, err := s.parse(content)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar tags []string\n\n\t// Walk the AST to find tag nodes\n\terr = gast.Walk(root, func(n gast.Node, entering bool) (gast.WalkStatus, error) {\n\t\tif !entering {\n\t\t\treturn gast.WalkContinue, nil\n\t\t}\n\n\t\t// Check for custom TagNode\n\t\tif tagNode, ok := n.(*mast.TagNode); ok {\n\t\t\ttags = append(tags, string(tagNode.Tag))\n\t\t}\n\n\t\treturn gast.WalkContinue, nil\n\t})\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Deduplicate tags while preserving original case\n\treturn uniquePreserveCase(tags), nil\n}\n\n// extractHeadingText extracts plain text content from a heading node.\nfunc extractHeadingText(n gast.Node, source []byte) string {\n\tvar buf strings.Builder\n\tfor child := n.FirstChild(); child != nil; child = child.NextSibling() {\n\t\textractTextFromNode(child, source, &buf)\n\t}\n\treturn buf.String()\n}\n\n// extractTextFromNode recursively extracts plain text from a node and its children.\nfunc extractTextFromNode(n gast.Node, source []byte, buf *strings.Builder) {\n\tif textNode, ok := n.(*gast.Text); ok {\n\t\tbuf.Write(textNode.Segment.Value(source))\n\t\treturn\n\t}\n\tfor child := n.FirstChild(); child != nil; child = child.NextSibling() {\n\t\textractTextFromNode(child, source, buf)\n\t}\n}\n\n// ExtractProperties computes boolean properties about the content.\nfunc (s *service) ExtractProperties(content []byte) (*storepb.MemoPayload_Property, error) {\n\troot, err := s.parse(content)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tprop := &storepb.MemoPayload_Property{}\n\tfirstBlockChecked := false\n\n\terr = gast.Walk(root, func(n gast.Node, entering bool) (gast.WalkStatus, error) {\n\t\tif !entering {\n\t\t\treturn gast.WalkContinue, nil\n\t\t}\n\n\t\t// Check if the first block-level child of the document is an H1 heading.\n\t\tif !firstBlockChecked && n.Parent() != nil && n.Parent().Kind() == gast.KindDocument {\n\t\t\tfirstBlockChecked = true\n\t\t\tif heading, ok := n.(*gast.Heading); ok && heading.Level == 1 {\n\t\t\t\tprop.Title = extractHeadingText(n, content)\n\t\t\t}\n\t\t}\n\n\t\tswitch n.Kind() {\n\t\tcase gast.KindLink:\n\t\t\tprop.HasLink = true\n\n\t\tcase gast.KindCodeBlock, gast.KindFencedCodeBlock, gast.KindCodeSpan:\n\t\t\tprop.HasCode = true\n\n\t\tcase east.KindTaskCheckBox:\n\t\t\tprop.HasTaskList = true\n\t\t\tif checkBox, ok := n.(*east.TaskCheckBox); ok {\n\t\t\t\tif !checkBox.IsChecked {\n\t\t\t\t\tprop.HasIncompleteTasks = true\n\t\t\t\t}\n\t\t\t}\n\t\tdefault:\n\t\t\t// No special handling for other node types\n\t\t}\n\n\t\treturn gast.WalkContinue, nil\n\t})\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn prop, nil\n}\n\n// RenderMarkdown renders goldmark AST back to markdown text.\nfunc (s *service) RenderMarkdown(content []byte) (string, error) {\n\troot, err := s.parse(content)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tmdRenderer := renderer.NewMarkdownRenderer()\n\treturn mdRenderer.Render(root, content), nil\n}\n\n// RenderHTML renders markdown content to HTML using goldmark's built-in HTML renderer.\nfunc (s *service) RenderHTML(content []byte) (string, error) {\n\tvar buf bytes.Buffer\n\tif err := s.md.Convert(content, &buf); err != nil {\n\t\treturn \"\", err\n\t}\n\treturn buf.String(), nil\n}\n\n// GenerateSnippet creates a plain text summary from markdown content.\nfunc (s *service) GenerateSnippet(content []byte, maxLength int) (string, error) {\n\troot, err := s.parse(content)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tvar buf strings.Builder\n\tvar lastNodeWasBlock bool\n\n\terr = gast.Walk(root, func(n gast.Node, entering bool) (gast.WalkStatus, error) {\n\t\tif entering {\n\t\t\t// Skip code blocks entirely (but keep inline code spans for snippet text)\n\t\t\tswitch n.Kind() {\n\t\t\tcase gast.KindCodeBlock, gast.KindFencedCodeBlock:\n\t\t\t\treturn gast.WalkSkipChildren, nil\n\t\t\tdefault:\n\t\t\t\t// Continue walking for other node types\n\t\t\t}\n\n\t\t\t// Add space before block elements (except first)\n\t\t\tswitch n.Kind() {\n\t\t\tcase gast.KindParagraph, gast.KindHeading, gast.KindListItem, east.KindTableCell, east.KindTableRow, east.KindTableHeader:\n\t\t\t\tif buf.Len() > 0 && lastNodeWasBlock {\n\t\t\t\t\tbuf.WriteByte(' ')\n\t\t\t\t}\n\t\t\tdefault:\n\t\t\t\t// No space needed for other node types\n\t\t\t}\n\t\t}\n\n\t\tif !entering {\n\t\t\t// Mark that we just exited a block element\n\t\t\tswitch n.Kind() {\n\t\t\tcase gast.KindParagraph, gast.KindHeading, gast.KindListItem, east.KindTableCell, east.KindTableRow, east.KindTableHeader:\n\t\t\t\tlastNodeWasBlock = true\n\t\t\tdefault:\n\t\t\t\t// Not a block element\n\t\t\t}\n\t\t\treturn gast.WalkContinue, nil\n\t\t}\n\n\t\tlastNodeWasBlock = false\n\n\t\t// Extract text from various node types\n\t\tswitch node := n.(type) {\n\t\tcase *gast.Text:\n\t\t\tsegment := node.Segment\n\t\t\tbuf.Write(segment.Value(content))\n\t\t\tif node.SoftLineBreak() {\n\t\t\t\tbuf.WriteByte(' ')\n\t\t\t}\n\t\tcase *gast.AutoLink:\n\t\t\tbuf.Write(node.URL(content))\n\t\t\treturn gast.WalkSkipChildren, nil\n\t\tcase *mast.TagNode:\n\t\t\tbuf.WriteByte('#')\n\t\t\tbuf.Write(node.Tag)\n\t\tdefault:\n\t\t\t// Ignore other node types.\n\t\t}\n\n\t\t// Stop walking if we've exceeded double the max length\n\t\t// (we'll truncate precisely later)\n\t\tif buf.Len() > maxLength*2 {\n\t\t\treturn gast.WalkStop, nil\n\t\t}\n\n\t\treturn gast.WalkContinue, nil\n\t})\n\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tsnippet := buf.String()\n\n\t// Truncate at word boundary if needed\n\tif len(snippet) > maxLength {\n\t\tsnippet = truncateAtWord(snippet, maxLength)\n\t}\n\n\treturn strings.TrimSpace(snippet), nil\n}\n\n// ValidateContent checks if the markdown content is valid.\nfunc (s *service) ValidateContent(content []byte) error {\n\t// Try to parse the content\n\t_, err := s.parse(content)\n\treturn err\n}\n\n// ExtractAll extracts tags, properties, and references in a single parse for efficiency.\nfunc (s *service) ExtractAll(content []byte) (*ExtractedData, error) {\n\troot, err := s.parse(content)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tdata := &ExtractedData{\n\t\tTags:     []string{},\n\t\tProperty: &storepb.MemoPayload_Property{},\n\t}\n\n\tfirstBlockChecked := false\n\n\t// Single walk to collect all data\n\terr = gast.Walk(root, func(n gast.Node, entering bool) (gast.WalkStatus, error) {\n\t\tif !entering {\n\t\t\treturn gast.WalkContinue, nil\n\t\t}\n\n\t\t// Extract tags\n\t\tif tagNode, ok := n.(*mast.TagNode); ok {\n\t\t\tdata.Tags = append(data.Tags, string(tagNode.Tag))\n\t\t}\n\n\t\t// Check if the first block-level child of the document is an H1 heading.\n\t\tif !firstBlockChecked && n.Parent() != nil && n.Parent().Kind() == gast.KindDocument {\n\t\t\tfirstBlockChecked = true\n\t\t\tif heading, ok := n.(*gast.Heading); ok && heading.Level == 1 {\n\t\t\t\tdata.Property.Title = extractHeadingText(n, content)\n\t\t\t}\n\t\t}\n\n\t\t// Extract properties based on node kind\n\t\tswitch n.Kind() {\n\t\tcase gast.KindLink:\n\t\t\tdata.Property.HasLink = true\n\n\t\tcase gast.KindCodeBlock, gast.KindFencedCodeBlock, gast.KindCodeSpan:\n\t\t\tdata.Property.HasCode = true\n\n\t\tcase east.KindTaskCheckBox:\n\t\t\tdata.Property.HasTaskList = true\n\t\t\tif checkBox, ok := n.(*east.TaskCheckBox); ok {\n\t\t\t\tif !checkBox.IsChecked {\n\t\t\t\t\tdata.Property.HasIncompleteTasks = true\n\t\t\t\t}\n\t\t\t}\n\t\tdefault:\n\t\t\t// No special handling for other node types\n\t\t}\n\n\t\treturn gast.WalkContinue, nil\n\t})\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Deduplicate tags while preserving original case\n\tdata.Tags = uniquePreserveCase(data.Tags)\n\n\treturn data, nil\n}\n\n// RenameTag renames all occurrences of oldTag to newTag in content.\nfunc (s *service) RenameTag(content []byte, oldTag, newTag string) (string, error) {\n\troot, err := s.parse(content)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\t// Walk the AST to find and rename tag nodes\n\terr = gast.Walk(root, func(n gast.Node, entering bool) (gast.WalkStatus, error) {\n\t\tif !entering {\n\t\t\treturn gast.WalkContinue, nil\n\t\t}\n\n\t\t// Check for custom TagNode and rename if it matches\n\t\tif tagNode, ok := n.(*mast.TagNode); ok {\n\t\t\tif string(tagNode.Tag) == oldTag {\n\t\t\t\ttagNode.Tag = []byte(newTag)\n\t\t\t}\n\t\t}\n\n\t\treturn gast.WalkContinue, nil\n\t})\n\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\t// Render back to markdown using the already-parsed AST\n\tmdRenderer := renderer.NewMarkdownRenderer()\n\treturn mdRenderer.Render(root, content), nil\n}\n\n// uniquePreserveCase returns unique strings from input while preserving case.\nfunc uniquePreserveCase(strs []string) []string {\n\tseen := make(map[string]struct{})\n\tvar result []string\n\n\tfor _, s := range strs {\n\t\tif _, exists := seen[s]; !exists {\n\t\t\tseen[s] = struct{}{}\n\t\t\tresult = append(result, s)\n\t\t}\n\t}\n\n\treturn result\n}\n\n// truncateAtWord truncates a string at the last word boundary before maxLength.\n// maxLength is treated as a rune (character) count to properly handle UTF-8 multi-byte characters.\nfunc truncateAtWord(s string, maxLength int) string {\n\t// Convert to runes to properly handle multi-byte UTF-8 characters\n\trunes := []rune(s)\n\tif len(runes) <= maxLength {\n\t\treturn s\n\t}\n\n\t// Truncate to max length (by character count, not byte count)\n\ttruncated := string(runes[:maxLength])\n\n\t// Find last space to avoid cutting in the middle of a word\n\tlastSpace := strings.LastIndexAny(truncated, \" \\t\\n\\r\")\n\tif lastSpace > 0 {\n\t\ttruncated = truncated[:lastSpace]\n\t}\n\n\treturn truncated + \" ...\"\n}\n"
  },
  {
    "path": "plugin/markdown/markdown_test.go",
    "content": "package markdown\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestNewService(t *testing.T) {\n\tsvc := NewService()\n\tassert.NotNil(t, svc)\n}\n\nfunc TestValidateContent(t *testing.T) {\n\tsvc := NewService()\n\n\ttests := []struct {\n\t\tname    string\n\t\tcontent string\n\t\twantErr bool\n\t}{\n\t\t{\n\t\t\tname:    \"valid markdown\",\n\t\t\tcontent: \"# Hello\\n\\nThis is **bold** text.\",\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"empty content\",\n\t\t\tcontent: \"\",\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"complex markdown\",\n\t\t\tcontent: \"# Title\\n\\n- List item 1\\n- List item 2\\n\\n```go\\ncode block\\n```\",\n\t\t\twantErr: false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\terr := svc.ValidateContent([]byte(tt.content))\n\t\t\tif tt.wantErr {\n\t\t\t\tassert.Error(t, err)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestGenerateSnippet(t *testing.T) {\n\tsvc := NewService()\n\n\ttests := []struct {\n\t\tname      string\n\t\tcontent   string\n\t\tmaxLength int\n\t\texpected  string\n\t}{\n\t\t{\n\t\t\tname:      \"simple text\",\n\t\t\tcontent:   \"Hello world\",\n\t\t\tmaxLength: 100,\n\t\t\texpected:  \"Hello world\",\n\t\t},\n\t\t{\n\t\t\tname:      \"text with formatting\",\n\t\t\tcontent:   \"This is **bold** and *italic* text.\",\n\t\t\tmaxLength: 100,\n\t\t\texpected:  \"This is bold and italic text.\",\n\t\t},\n\t\t{\n\t\t\tname:      \"truncate long text\",\n\t\t\tcontent:   \"This is a very long piece of text that should be truncated at a word boundary.\",\n\t\t\tmaxLength: 30,\n\t\t\texpected:  \"This is a very long piece of ...\",\n\t\t},\n\t\t{\n\t\t\tname:      \"heading and paragraph\",\n\t\t\tcontent:   \"# My Title\\n\\nThis is the first paragraph.\",\n\t\t\tmaxLength: 100,\n\t\t\texpected:  \"My Title This is the first paragraph.\",\n\t\t},\n\t\t{\n\t\t\tname:      \"code block removed\",\n\t\t\tcontent:   \"Text before\\n\\n```go\\ncode\\n```\\n\\nText after\",\n\t\t\tmaxLength: 100,\n\t\t\texpected:  \"Text before Text after\",\n\t\t},\n\t\t{\n\t\t\tname:      \"list items\",\n\t\t\tcontent:   \"- Item 1\\n- Item 2\\n- Item 3\",\n\t\t\tmaxLength: 100,\n\t\t\texpected:  \"Item 1 Item 2 Item 3\",\n\t\t},\n\t\t{\n\t\t\tname:      \"inline code preserved\",\n\t\t\tcontent:   \"`console.log('hello')`\",\n\t\t\tmaxLength: 100,\n\t\t\texpected:  \"console.log('hello')\",\n\t\t},\n\t\t{\n\t\t\tname:      \"text with inline code\",\n\t\t\tcontent:   \"Use `fmt.Println` to print output.\",\n\t\t\tmaxLength: 100,\n\t\t\texpected:  \"Use fmt.Println to print output.\",\n\t\t},\n\t\t{\n\t\t\tname:      \"image alt text\",\n\t\t\tcontent:   \"![alt text](https://example.com/img.png)\",\n\t\t\tmaxLength: 100,\n\t\t\texpected:  \"alt text\",\n\t\t},\n\t\t{\n\t\t\tname:      \"strikethrough text\",\n\t\t\tcontent:   \"~~deleted text~~\",\n\t\t\tmaxLength: 100,\n\t\t\texpected:  \"deleted text\",\n\t\t},\n\t\t{\n\t\t\tname:      \"blockquote\",\n\t\t\tcontent:   \"> quoted text\",\n\t\t\tmaxLength: 100,\n\t\t\texpected:  \"quoted text\",\n\t\t},\n\t\t{\n\t\t\tname:      \"table cells spaced\",\n\t\t\tcontent:   \"| a | b |\\n|---|---|\\n| 1 | 2 |\",\n\t\t\tmaxLength: 100,\n\t\t\texpected:  \"a b 1 2\",\n\t\t},\n\t\t{\n\t\t\tname:      \"plain URL autolink\",\n\t\t\tcontent:   \"https://usememos.com\",\n\t\t\tmaxLength: 100,\n\t\t\texpected:  \"https://usememos.com\",\n\t\t},\n\t\t{\n\t\t\tname:      \"text with plain URL\",\n\t\t\tcontent:   \"Check out https://usememos.com for more info.\",\n\t\t\tmaxLength: 100,\n\t\t\texpected:  \"Check out https://usememos.com for more info.\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tsnippet, err := svc.GenerateSnippet([]byte(tt.content), tt.maxLength)\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, tt.expected, snippet)\n\t\t})\n\t}\n\n\t// Test with tag extension enabled (matches production config).\n\tsvcWithTags := NewService(WithTagExtension())\n\ttagTests := []struct {\n\t\tname      string\n\t\tcontent   string\n\t\tmaxLength int\n\t\texpected  string\n\t}{\n\t\t{\n\t\t\tname:      \"tag only\",\n\t\t\tcontent:   \"#todo\",\n\t\t\tmaxLength: 100,\n\t\t\texpected:  \"#todo\",\n\t\t},\n\t\t{\n\t\t\tname:      \"text with tags\",\n\t\t\tcontent:   \"Remember to #review the #code\",\n\t\t\tmaxLength: 100,\n\t\t\texpected:  \"Remember to #review the #code\",\n\t\t},\n\t}\n\tfor _, tt := range tagTests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tsnippet, err := svcWithTags.GenerateSnippet([]byte(tt.content), tt.maxLength)\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, tt.expected, snippet)\n\t\t})\n\t}\n}\n\nfunc TestExtractProperties(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tcontent  string\n\t\thasLink  bool\n\t\thasCode  bool\n\t\thasTasks bool\n\t\thasInc   bool\n\t\ttitle    string\n\t}{\n\t\t{\n\t\t\tname:     \"plain text\",\n\t\t\tcontent:  \"Just plain text\",\n\t\t\thasLink:  false,\n\t\t\thasCode:  false,\n\t\t\thasTasks: false,\n\t\t\thasInc:   false,\n\t\t\ttitle:    \"\",\n\t\t},\n\t\t{\n\t\t\tname:     \"with link\",\n\t\t\tcontent:  \"Check out [this link](https://example.com)\",\n\t\t\thasLink:  true,\n\t\t\thasCode:  false,\n\t\t\thasTasks: false,\n\t\t\thasInc:   false,\n\t\t\ttitle:    \"\",\n\t\t},\n\t\t{\n\t\t\tname:     \"with inline code\",\n\t\t\tcontent:  \"Use `console.log()` to debug\",\n\t\t\thasLink:  false,\n\t\t\thasCode:  true,\n\t\t\thasTasks: false,\n\t\t\thasInc:   false,\n\t\t\ttitle:    \"\",\n\t\t},\n\t\t{\n\t\t\tname:     \"with code block\",\n\t\t\tcontent:  \"```go\\nfunc main() {}\\n```\",\n\t\t\thasLink:  false,\n\t\t\thasCode:  true,\n\t\t\thasTasks: false,\n\t\t\thasInc:   false,\n\t\t\ttitle:    \"\",\n\t\t},\n\t\t{\n\t\t\tname:     \"with completed task\",\n\t\t\tcontent:  \"- [x] Completed task\",\n\t\t\thasLink:  false,\n\t\t\thasCode:  false,\n\t\t\thasTasks: true,\n\t\t\thasInc:   false,\n\t\t\ttitle:    \"\",\n\t\t},\n\t\t{\n\t\t\tname:     \"with incomplete task\",\n\t\t\tcontent:  \"- [ ] Todo item\",\n\t\t\thasLink:  false,\n\t\t\thasCode:  false,\n\t\t\thasTasks: true,\n\t\t\thasInc:   true,\n\t\t\ttitle:    \"\",\n\t\t},\n\t\t{\n\t\t\tname:     \"mixed tasks\",\n\t\t\tcontent:  \"- [x] Done\\n- [ ] Not done\",\n\t\t\thasLink:  false,\n\t\t\thasCode:  false,\n\t\t\thasTasks: true,\n\t\t\thasInc:   true,\n\t\t\ttitle:    \"\",\n\t\t},\n\t\t{\n\t\t\tname:     \"everything\",\n\t\t\tcontent:  \"# Title\\n\\n[Link](url)\\n\\n`code`\\n\\n- [ ] Task\",\n\t\t\thasLink:  true,\n\t\t\thasCode:  true,\n\t\t\thasTasks: true,\n\t\t\thasInc:   true,\n\t\t\ttitle:    \"Title\",\n\t\t},\n\t\t{\n\t\t\tname:    \"h1 as first node extracts title\",\n\t\t\tcontent: \"# My Article Title\\n\\nBody text here.\",\n\t\t\ttitle:   \"My Article Title\",\n\t\t},\n\t\t{\n\t\t\tname:    \"h2 as first node does not extract title\",\n\t\t\tcontent: \"## Sub Heading\\n\\nBody text.\",\n\t\t\ttitle:   \"\",\n\t\t},\n\t\t{\n\t\t\tname:    \"h1 not first node does not extract title\",\n\t\t\tcontent: \"Some text\\n\\n# Heading Later\",\n\t\t\ttitle:   \"\",\n\t\t},\n\t\t{\n\t\t\tname:    \"h1 with inline formatting extracts plain text\",\n\t\t\tcontent: \"# Title with **bold** and *italic*\\n\\nBody.\",\n\t\t\ttitle:   \"Title with bold and italic\",\n\t\t},\n\t\t{\n\t\t\tname:    \"empty content has no title\",\n\t\t\tcontent: \"\",\n\t\t\ttitle:   \"\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tsvc := NewService()\n\n\t\t\tprops, err := svc.ExtractProperties([]byte(tt.content))\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, tt.hasLink, props.HasLink, \"HasLink\")\n\t\t\tassert.Equal(t, tt.hasCode, props.HasCode, \"HasCode\")\n\t\t\tassert.Equal(t, tt.hasTasks, props.HasTaskList, \"HasTaskList\")\n\t\t\tassert.Equal(t, tt.hasInc, props.HasIncompleteTasks, \"HasIncompleteTasks\")\n\t\t\tassert.Equal(t, tt.title, props.Title, \"Title\")\n\t\t})\n\t}\n}\n\nfunc TestExtractAllTitle(t *testing.T) {\n\tsvc := NewService(WithTagExtension())\n\n\ttests := []struct {\n\t\tname    string\n\t\tcontent string\n\t\ttitle   string\n\t}{\n\t\t{\n\t\t\tname:    \"h1 first node\",\n\t\t\tcontent: \"# Article Title\\n\\nContent with #tag\",\n\t\t\ttitle:   \"Article Title\",\n\t\t},\n\t\t{\n\t\t\tname:    \"no h1\",\n\t\t\tcontent: \"Just text with #tag\",\n\t\t\ttitle:   \"\",\n\t\t},\n\t\t{\n\t\t\tname:    \"h1 not first\",\n\t\t\tcontent: \"Intro\\n\\n# Late Heading\",\n\t\t\ttitle:   \"\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tdata, err := svc.ExtractAll([]byte(tt.content))\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, tt.title, data.Property.Title, \"Title\")\n\t\t})\n\t}\n}\n\nfunc TestExtractTags(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tcontent  string\n\t\twithExt  bool\n\t\texpected []string\n\t}{\n\t\t{\n\t\t\tname:     \"no tags\",\n\t\t\tcontent:  \"Just plain text\",\n\t\t\twithExt:  false,\n\t\t\texpected: []string{},\n\t\t},\n\t\t{\n\t\t\tname:     \"single tag\",\n\t\t\tcontent:  \"Text with #tag\",\n\t\t\twithExt:  true,\n\t\t\texpected: []string{\"tag\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"multiple tags\",\n\t\t\tcontent:  \"Text with #tag1 and #tag2\",\n\t\t\twithExt:  true,\n\t\t\texpected: []string{\"tag1\", \"tag2\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"duplicate tags\",\n\t\t\tcontent:  \"#work is important. #Work #WORK\",\n\t\t\twithExt:  true,\n\t\t\texpected: []string{\"work\", \"Work\", \"WORK\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"tags with hyphens and underscores\",\n\t\t\tcontent:  \"Tags: #work-notes #2024_plans\",\n\t\t\twithExt:  true,\n\t\t\texpected: []string{\"work-notes\", \"2024_plans\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"tags at end of sentence\",\n\t\t\tcontent:  \"This is important #urgent.\",\n\t\t\twithExt:  true,\n\t\t\texpected: []string{\"urgent\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"headings not tags\",\n\t\t\tcontent:  \"## Heading\\n\\n# Title\\n\\nText with #realtag\",\n\t\t\twithExt:  true,\n\t\t\texpected: []string{\"realtag\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"numeric tag\",\n\t\t\tcontent:  \"Issue #123\",\n\t\t\twithExt:  true,\n\t\t\texpected: []string{\"123\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"tag in list\",\n\t\t\tcontent:  \"- Item 1 #todo\\n- Item 2 #done\",\n\t\t\twithExt:  true,\n\t\t\texpected: []string{\"todo\", \"done\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"no extension enabled\",\n\t\t\tcontent:  \"Text with #tag\",\n\t\t\twithExt:  false,\n\t\t\texpected: []string{},\n\t\t},\n\t\t{\n\t\t\tname:     \"Chinese tag\",\n\t\t\tcontent:  \"Text with #测试\",\n\t\t\twithExt:  true,\n\t\t\texpected: []string{\"测试\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"Chinese tag followed by punctuation\",\n\t\t\tcontent:  \"Text #测试。 More text\",\n\t\t\twithExt:  true,\n\t\t\texpected: []string{\"测试\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"mixed Chinese and ASCII tag\",\n\t\t\tcontent:  \"#测试test123 content\",\n\t\t\twithExt:  true,\n\t\t\texpected: []string{\"测试test123\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"Japanese tag\",\n\t\t\tcontent:  \"#日本語 content\",\n\t\t\twithExt:  true,\n\t\t\texpected: []string{\"日本語\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"Korean tag\",\n\t\t\tcontent:  \"#한국어 content\",\n\t\t\twithExt:  true,\n\t\t\texpected: []string{\"한국어\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"hierarchical tag with Chinese\",\n\t\t\tcontent:  \"#work/测试/项目\",\n\t\t\twithExt:  true,\n\t\t\texpected: []string{\"work/测试/项目\"},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tvar svc Service\n\t\t\tif tt.withExt {\n\t\t\t\tsvc = NewService(WithTagExtension())\n\t\t\t} else {\n\t\t\t\tsvc = NewService()\n\t\t\t}\n\n\t\t\ttags, err := svc.ExtractTags([]byte(tt.content))\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.ElementsMatch(t, tt.expected, tags)\n\t\t})\n\t}\n}\n\nfunc TestUniquePreserveCase(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    []string\n\t\texpected []string\n\t}{\n\t\t{\n\t\t\tname:     \"empty\",\n\t\t\tinput:    []string{},\n\t\t\texpected: []string{},\n\t\t},\n\t\t{\n\t\t\tname:     \"unique items\",\n\t\t\tinput:    []string{\"tag1\", \"tag2\", \"tag3\"},\n\t\t\texpected: []string{\"tag1\", \"tag2\", \"tag3\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"duplicates\",\n\t\t\tinput:    []string{\"tag\", \"TAG\", \"Tag\"},\n\t\t\texpected: []string{\"tag\", \"TAG\", \"Tag\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"mixed\",\n\t\t\tinput:    []string{\"Work\", \"work\", \"Important\", \"work\"},\n\t\t\texpected: []string{\"Work\", \"work\", \"Important\"},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := uniquePreserveCase(tt.input)\n\t\t\tassert.ElementsMatch(t, tt.expected, result)\n\t\t})\n\t}\n}\n\nfunc TestTruncateAtWord(t *testing.T) {\n\ttests := []struct {\n\t\tname      string\n\t\tinput     string\n\t\tmaxLength int\n\t\texpected  string\n\t}{\n\t\t{\n\t\t\tname:      \"no truncation needed\",\n\t\t\tinput:     \"short\",\n\t\t\tmaxLength: 10,\n\t\t\texpected:  \"short\",\n\t\t},\n\t\t{\n\t\t\tname:      \"exact length\",\n\t\t\tinput:     \"exactly ten\",\n\t\t\tmaxLength: 11,\n\t\t\texpected:  \"exactly ten\",\n\t\t},\n\t\t{\n\t\t\tname:      \"truncate at word\",\n\t\t\tinput:     \"this is a long sentence\",\n\t\t\tmaxLength: 10,\n\t\t\texpected:  \"this is a ...\",\n\t\t},\n\t\t{\n\t\t\tname:      \"truncate very long word\",\n\t\t\tinput:     \"supercalifragilisticexpialidocious\",\n\t\t\tmaxLength: 10,\n\t\t\texpected:  \"supercalif ...\",\n\t\t},\n\t\t{\n\t\t\tname:      \"CJK characters without spaces\",\n\t\t\tinput:     \"这是一个很长的中文句子没有空格的情况下也要正确处理\",\n\t\t\tmaxLength: 15,\n\t\t\texpected:  \"这是一个很长的中文句子没有空格 ...\",\n\t\t},\n\t\t{\n\t\t\tname:      \"mixed CJK and Latin\",\n\t\t\tinput:     \"这是中文mixed with English文字\",\n\t\t\tmaxLength: 10,\n\t\t\texpected:  \"这是中文mixed ...\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := truncateAtWord(tt.input, tt.maxLength)\n\t\t\tassert.Equal(t, tt.expected, result)\n\t\t})\n\t}\n}\n\n// Benchmark tests.\nfunc BenchmarkGenerateSnippet(b *testing.B) {\n\tsvc := NewService()\n\tcontent := []byte(`# Large Document\n\nThis is a large document with multiple paragraphs and formatting.\n\n## Section 1\n\nHere is some **bold** text and *italic* text with [links](https://example.com).\n\n- List item 1\n- List item 2\n- List item 3\n\n## Section 2\n\nMore content here with ` + \"`inline code`\" + ` and other elements.\n\n` + \"```go\\nfunc example() {\\n    return true\\n}\\n```\")\n\n\tb.ResetTimer()\n\tfor i := 0; i < b.N; i++ {\n\t\t_, err := svc.GenerateSnippet(content, 200)\n\t\tif err != nil {\n\t\t\tb.Fatal(err)\n\t\t}\n\t}\n}\n\nfunc BenchmarkExtractProperties(b *testing.B) {\n\tsvc := NewService()\n\tcontent := []byte(\"# Title\\n\\n[Link](url)\\n\\n`code`\\n\\n- [ ] Task\\n- [x] Done\")\n\n\tb.ResetTimer()\n\tfor i := 0; i < b.N; i++ {\n\t\t_, err := svc.ExtractProperties(content)\n\t\tif err != nil {\n\t\t\tb.Fatal(err)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "plugin/markdown/parser/tag.go",
    "content": "package parser\n\nimport (\n\t\"unicode\"\n\t\"unicode/utf8\"\n\n\tgast \"github.com/yuin/goldmark/ast\"\n\t\"github.com/yuin/goldmark/parser\"\n\t\"github.com/yuin/goldmark/text\"\n\n\tmast \"github.com/usememos/memos/plugin/markdown/ast\"\n)\n\nconst (\n\t// MaxTagLength defines the maximum number of runes allowed in a tag.\n\tMaxTagLength = 100\n)\n\ntype tagParser struct{}\n\n// NewTagParser creates a new inline parser for #tag syntax.\nfunc NewTagParser() parser.InlineParser {\n\treturn &tagParser{}\n}\n\n// Trigger returns the characters that trigger this parser.\nfunc (*tagParser) Trigger() []byte {\n\treturn []byte{'#'}\n}\n\n// isValidTagRune checks if a Unicode rune is valid in a tag.\n// Uses Unicode categories for proper international character support.\nfunc isValidTagRune(r rune) bool {\n\t// Allow Unicode letters (any script: Latin, CJK, Arabic, Cyrillic, etc.)\n\tif unicode.IsLetter(r) {\n\t\treturn true\n\t}\n\n\t// Allow Unicode digits\n\tif unicode.IsNumber(r) {\n\t\treturn true\n\t}\n\n\t// Allow emoji and symbols (So category: Symbol, Other)\n\t// This includes emoji, which are essential for social media-style tagging\n\tif unicode.IsSymbol(r) {\n\t\treturn true\n\t}\n\n\t// Allow marks (non-spacing, spacing combining, enclosing)\n\t// This covers variation selectors (e.g. VS16 \\uFE0F) and combining marks (e.g. Keycap \\u20E3, accents)\n\tif unicode.IsMark(r) {\n\t\treturn true\n\t}\n\n\t// Allow Zero Width Joiner (ZWJ) for emoji sequences\n\tif r == '\\u200D' {\n\t\treturn true\n\t}\n\n\t// Allow specific ASCII symbols for tag structure\n\t// Underscore: word separation (snake_case)\n\t// Hyphen: word separation (kebab-case)\n\t// Forward slash: hierarchical tags (category/subcategory)\n\t// Ampersand: compound tags (science&tech)\n\tif r == '_' || r == '-' || r == '/' || r == '&' {\n\t\treturn true\n\t}\n\n\treturn false\n}\n\n// Parse parses #tag syntax using Unicode-aware validation.\n// Tags support international characters and follow these rules:\n//   - Must start with # followed by valid tag characters\n//   - Valid characters: Unicode letters, Unicode digits, underscore (_), hyphen (-), forward slash (/)\n//   - Maximum length: 100 runes (Unicode characters)\n//   - Stops at: whitespace, punctuation, or other invalid characters\nfunc (*tagParser) Parse(_ gast.Node, block text.Reader, _ parser.Context) gast.Node {\n\tline, _ := block.PeekLine()\n\n\t// Must start with #\n\tif len(line) == 0 || line[0] != '#' {\n\t\treturn nil\n\t}\n\n\t// Check if it's a heading (## or space after #)\n\tif len(line) > 1 {\n\t\tif line[1] == '#' {\n\t\t\t// It's a heading (##), not a tag\n\t\t\treturn nil\n\t\t}\n\t\tif line[1] == ' ' {\n\t\t\t// Space after # - heading or just a hash\n\t\t\treturn nil\n\t\t}\n\t} else {\n\t\t// Just a lone #\n\t\treturn nil\n\t}\n\n\t// Parse tag using UTF-8 aware rune iteration\n\ttagStart := 1\n\tpos := tagStart\n\truneCount := 0\n\n\tfor pos < len(line) {\n\t\tr, size := utf8.DecodeRune(line[pos:])\n\n\t\t// Stop at invalid UTF-8\n\t\tif r == utf8.RuneError && size == 1 {\n\t\t\tbreak\n\t\t}\n\n\t\t// Validate character using Unicode categories\n\t\tif !isValidTagRune(r) {\n\t\t\tbreak\n\t\t}\n\n\t\t// Enforce max length (by rune count, not byte count)\n\t\truneCount++\n\t\tif runeCount > MaxTagLength {\n\t\t\tbreak\n\t\t}\n\n\t\tpos += size\n\t}\n\n\t// Must have at least one character after #\n\tif pos <= tagStart {\n\t\treturn nil\n\t}\n\n\t// Extract tag (without #)\n\ttagName := line[tagStart:pos]\n\n\t// Make a copy of the tag name\n\ttagCopy := make([]byte, len(tagName))\n\tcopy(tagCopy, tagName)\n\n\t// Advance reader\n\tblock.Advance(pos)\n\n\t// Create node\n\tnode := &mast.TagNode{\n\t\tTag: tagCopy,\n\t}\n\n\treturn node\n}\n"
  },
  {
    "path": "plugin/markdown/parser/tag_test.go",
    "content": "package parser\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/yuin/goldmark/parser\"\n\t\"github.com/yuin/goldmark/text\"\n\n\tmast \"github.com/usememos/memos/plugin/markdown/ast\"\n)\n\nfunc TestTagParser(t *testing.T) {\n\ttests := []struct {\n\t\tname        string\n\t\tinput       string\n\t\texpectedTag string\n\t\tshouldParse bool\n\t}{\n\t\t{\n\t\t\tname:        \"basic tag\",\n\t\t\tinput:       \"#tag\",\n\t\t\texpectedTag: \"tag\",\n\t\t\tshouldParse: true,\n\t\t},\n\t\t{\n\t\t\tname:        \"tag with hyphen\",\n\t\t\tinput:       \"#work-notes\",\n\t\t\texpectedTag: \"work-notes\",\n\t\t\tshouldParse: true,\n\t\t},\n\t\t{\n\t\t\tname:        \"tag with ampersand\",\n\t\t\tinput:       \"#science&tech\",\n\t\t\texpectedTag: \"science&tech\",\n\t\t\tshouldParse: true,\n\t\t},\n\t\t{\n\t\t\tname:        \"tag with underscore\",\n\t\t\tinput:       \"#2024_plans\",\n\t\t\texpectedTag: \"2024_plans\",\n\t\t\tshouldParse: true,\n\t\t},\n\t\t{\n\t\t\tname:        \"numeric tag\",\n\t\t\tinput:       \"#123\",\n\t\t\texpectedTag: \"123\",\n\t\t\tshouldParse: true,\n\t\t},\n\t\t{\n\t\t\tname:        \"tag followed by space\",\n\t\t\tinput:       \"#tag \",\n\t\t\texpectedTag: \"tag\",\n\t\t\tshouldParse: true,\n\t\t},\n\t\t{\n\t\t\tname:        \"tag followed by punctuation\",\n\t\t\tinput:       \"#tag.\",\n\t\t\texpectedTag: \"tag\",\n\t\t\tshouldParse: true,\n\t\t},\n\t\t{\n\t\t\tname:        \"tag in sentence\",\n\t\t\tinput:       \"#important task\",\n\t\t\texpectedTag: \"important\",\n\t\t\tshouldParse: true,\n\t\t},\n\t\t{\n\t\t\tname:        \"heading (##)\",\n\t\t\tinput:       \"## Heading\",\n\t\t\texpectedTag: \"\",\n\t\t\tshouldParse: false,\n\t\t},\n\t\t{\n\t\t\tname:        \"space after hash\",\n\t\t\tinput:       \"# heading\",\n\t\t\texpectedTag: \"\",\n\t\t\tshouldParse: false,\n\t\t},\n\t\t{\n\t\t\tname:        \"lone hash\",\n\t\t\tinput:       \"#\",\n\t\t\texpectedTag: \"\",\n\t\t\tshouldParse: false,\n\t\t},\n\t\t{\n\t\t\tname:        \"hash with space\",\n\t\t\tinput:       \"# \",\n\t\t\texpectedTag: \"\",\n\t\t\tshouldParse: false,\n\t\t},\n\t\t{\n\t\t\tname:        \"special characters\",\n\t\t\tinput:       \"#tag@special\",\n\t\t\texpectedTag: \"tag\",\n\t\t\tshouldParse: true,\n\t\t},\n\t\t{\n\t\t\tname:        \"mixed case\",\n\t\t\tinput:       \"#WorkNotes\",\n\t\t\texpectedTag: \"WorkNotes\",\n\t\t\tshouldParse: true,\n\t\t},\n\t\t{\n\t\t\tname:        \"hierarchical tag with slash\",\n\t\t\tinput:       \"#tag1/subtag\",\n\t\t\texpectedTag: \"tag1/subtag\",\n\t\t\tshouldParse: true,\n\t\t},\n\t\t{\n\t\t\tname:        \"hierarchical tag with multiple levels\",\n\t\t\tinput:       \"#tag1/subtag/subtag2\",\n\t\t\texpectedTag: \"tag1/subtag/subtag2\",\n\t\t\tshouldParse: true,\n\t\t},\n\t\t{\n\t\t\tname:        \"hierarchical tag followed by space\",\n\t\t\tinput:       \"#work/notes \",\n\t\t\texpectedTag: \"work/notes\",\n\t\t\tshouldParse: true,\n\t\t},\n\t\t{\n\t\t\tname:        \"hierarchical tag followed by punctuation\",\n\t\t\tinput:       \"#project/2024.\",\n\t\t\texpectedTag: \"project/2024\",\n\t\t\tshouldParse: true,\n\t\t},\n\t\t{\n\t\t\tname:        \"hierarchical tag with numbers and dashes\",\n\t\t\tinput:       \"#work-log/2024/q1\",\n\t\t\texpectedTag: \"work-log/2024/q1\",\n\t\t\tshouldParse: true,\n\t\t},\n\t\t{\n\t\t\tname:        \"Chinese characters\",\n\t\t\tinput:       \"#测试\",\n\t\t\texpectedTag: \"测试\",\n\t\t\tshouldParse: true,\n\t\t},\n\t\t{\n\t\t\tname:        \"Chinese tag followed by space\",\n\t\t\tinput:       \"#测试 some text\",\n\t\t\texpectedTag: \"测试\",\n\t\t\tshouldParse: true,\n\t\t},\n\t\t{\n\t\t\tname:        \"Chinese tag followed by punctuation\",\n\t\t\tinput:       \"#测试。\",\n\t\t\texpectedTag: \"测试\",\n\t\t\tshouldParse: true,\n\t\t},\n\t\t{\n\t\t\tname:        \"mixed Chinese and ASCII\",\n\t\t\tinput:       \"#测试test123\",\n\t\t\texpectedTag: \"测试test123\",\n\t\t\tshouldParse: true,\n\t\t},\n\t\t{\n\t\t\tname:        \"Japanese characters\",\n\t\t\tinput:       \"#テスト\",\n\t\t\texpectedTag: \"テスト\",\n\t\t\tshouldParse: true,\n\t\t},\n\t\t{\n\t\t\tname:        \"Korean characters\",\n\t\t\tinput:       \"#테스트\",\n\t\t\texpectedTag: \"테스트\",\n\t\t\tshouldParse: true,\n\t\t},\n\t\t{\n\t\t\tname:        \"emoji\",\n\t\t\tinput:       \"#test🚀\",\n\t\t\texpectedTag: \"test🚀\",\n\t\t\tshouldParse: true,\n\t\t},\n\t\t{\n\t\t\tname:        \"emoji with VS16\",\n\t\t\tinput:       \"#test👁️\", // Eye + VS16\n\t\t\texpectedTag: \"test👁️\",\n\t\t\tshouldParse: true,\n\t\t},\n\t\t{\n\t\t\tname:        \"emoji with ZWJ sequence\",\n\t\t\tinput:       \"#family👨‍👩‍👧‍👦\", // Family ZWJ sequence\n\t\t\texpectedTag: \"family👨‍👩‍👧‍👦\",\n\t\t\tshouldParse: true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tp := NewTagParser()\n\t\t\treader := text.NewReader([]byte(tt.input))\n\t\t\tctx := parser.NewContext()\n\n\t\t\tnode := p.Parse(nil, reader, ctx)\n\n\t\t\tif tt.shouldParse {\n\t\t\t\trequire.NotNil(t, node, \"Expected tag to be parsed\")\n\t\t\t\trequire.IsType(t, &mast.TagNode{}, node)\n\n\t\t\t\ttagNode, ok := node.(*mast.TagNode)\n\t\t\t\trequire.True(t, ok, \"Expected node to be *mast.TagNode\")\n\t\t\t\tassert.Equal(t, tt.expectedTag, string(tagNode.Tag))\n\t\t\t} else {\n\t\t\t\tassert.Nil(t, node, \"Expected tag NOT to be parsed\")\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestTagParser_Trigger(t *testing.T) {\n\tp := NewTagParser()\n\ttriggers := p.Trigger()\n\n\tassert.Equal(t, []byte{'#'}, triggers)\n}\n\nfunc TestTagParser_MultipleTags(t *testing.T) {\n\t// Test that parser correctly handles multiple tags in sequence\n\tinput := \"#tag1 #tag2\"\n\n\tp := NewTagParser()\n\treader := text.NewReader([]byte(input))\n\tctx := parser.NewContext()\n\n\t// Parse first tag\n\tnode1 := p.Parse(nil, reader, ctx)\n\trequire.NotNil(t, node1)\n\ttagNode1, ok := node1.(*mast.TagNode)\n\trequire.True(t, ok, \"Expected node1 to be *mast.TagNode\")\n\tassert.Equal(t, \"tag1\", string(tagNode1.Tag))\n\n\t// Advance past the space\n\treader.Advance(1)\n\n\t// Parse second tag\n\tnode2 := p.Parse(nil, reader, ctx)\n\trequire.NotNil(t, node2)\n\ttagNode2, ok := node2.(*mast.TagNode)\n\trequire.True(t, ok, \"Expected node2 to be *mast.TagNode\")\n\tassert.Equal(t, \"tag2\", string(tagNode2.Tag))\n}\n\nfunc TestTagNode_Kind(t *testing.T) {\n\tnode := &mast.TagNode{\n\t\tTag: []byte(\"test\"),\n\t}\n\n\tassert.Equal(t, mast.KindTag, node.Kind())\n}\n\nfunc TestTagNode_Dump(t *testing.T) {\n\tnode := &mast.TagNode{\n\t\tTag: []byte(\"test\"),\n\t}\n\n\t// Should not panic\n\tassert.NotPanics(t, func() {\n\t\tnode.Dump([]byte(\"#test\"), 0)\n\t})\n}\n"
  },
  {
    "path": "plugin/markdown/renderer/markdown_renderer.go",
    "content": "package renderer\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"strings\"\n\n\tgast \"github.com/yuin/goldmark/ast\"\n\teast \"github.com/yuin/goldmark/extension/ast\"\n\n\tmast \"github.com/usememos/memos/plugin/markdown/ast\"\n)\n\n// MarkdownRenderer renders goldmark AST back to markdown text.\ntype MarkdownRenderer struct {\n\tbuf *bytes.Buffer\n}\n\n// NewMarkdownRenderer creates a new markdown renderer.\nfunc NewMarkdownRenderer() *MarkdownRenderer {\n\treturn &MarkdownRenderer{\n\t\tbuf: &bytes.Buffer{},\n\t}\n}\n\n// Render renders the AST node to markdown and returns the result.\nfunc (r *MarkdownRenderer) Render(node gast.Node, source []byte) string {\n\tr.buf.Reset()\n\tr.renderNode(node, source, 0)\n\treturn r.buf.String()\n}\n\n// renderNode renders a single node and its children.\nfunc (r *MarkdownRenderer) renderNode(node gast.Node, source []byte, depth int) {\n\tswitch n := node.(type) {\n\tcase *gast.Document:\n\t\tr.renderChildren(n, source, depth)\n\n\tcase *gast.Paragraph:\n\t\tr.renderChildren(n, source, depth)\n\t\tif node.NextSibling() != nil {\n\t\t\tr.buf.WriteString(\"\\n\\n\")\n\t\t}\n\n\tcase *gast.Text:\n\t\t// Text nodes store their content as segments in the source\n\t\tsegment := n.Segment\n\t\tr.buf.Write(segment.Value(source))\n\t\tif n.SoftLineBreak() {\n\t\t\tr.buf.WriteByte('\\n')\n\t\t} else if n.HardLineBreak() {\n\t\t\tr.buf.WriteString(\"  \\n\")\n\t\t}\n\n\tcase *gast.CodeSpan:\n\t\tr.buf.WriteByte('`')\n\t\tr.renderChildren(n, source, depth)\n\t\tr.buf.WriteByte('`')\n\n\tcase *gast.Emphasis:\n\t\tsymbol := \"*\"\n\t\tif n.Level == 2 {\n\t\t\tsymbol = \"**\"\n\t\t}\n\t\tr.buf.WriteString(symbol)\n\t\tr.renderChildren(n, source, depth)\n\t\tr.buf.WriteString(symbol)\n\n\tcase *gast.Link:\n\t\tr.buf.WriteString(\"[\")\n\t\tr.renderChildren(n, source, depth)\n\t\tr.buf.WriteString(\"](\")\n\t\tr.buf.Write(n.Destination)\n\t\tif len(n.Title) > 0 {\n\t\t\tr.buf.WriteString(` \"`)\n\t\t\tr.buf.Write(n.Title)\n\t\t\tr.buf.WriteString(`\"`)\n\t\t}\n\t\tr.buf.WriteString(\")\")\n\n\tcase *gast.AutoLink:\n\t\turl := n.URL(source)\n\t\tif n.AutoLinkType == gast.AutoLinkEmail {\n\t\t\tr.buf.WriteString(\"<\")\n\t\t\tr.buf.Write(url)\n\t\t\tr.buf.WriteString(\">\")\n\t\t} else {\n\t\t\tr.buf.Write(url)\n\t\t}\n\n\tcase *gast.Image:\n\t\tr.buf.WriteString(\"![\")\n\t\tr.renderChildren(n, source, depth)\n\t\tr.buf.WriteString(\"](\")\n\t\tr.buf.Write(n.Destination)\n\t\tif len(n.Title) > 0 {\n\t\t\tr.buf.WriteString(` \"`)\n\t\t\tr.buf.Write(n.Title)\n\t\t\tr.buf.WriteString(`\"`)\n\t\t}\n\t\tr.buf.WriteString(\")\")\n\n\tcase *gast.Heading:\n\t\tr.buf.WriteString(strings.Repeat(\"#\", n.Level))\n\t\tr.buf.WriteByte(' ')\n\t\tr.renderChildren(n, source, depth)\n\t\tif node.NextSibling() != nil {\n\t\t\tr.buf.WriteString(\"\\n\\n\")\n\t\t}\n\n\tcase *gast.CodeBlock, *gast.FencedCodeBlock:\n\t\tr.renderCodeBlock(n, source)\n\n\tcase *gast.Blockquote:\n\t\t// Render each child line with \"> \" prefix\n\t\tr.renderBlockquote(n, source, depth)\n\t\tif node.NextSibling() != nil {\n\t\t\tr.buf.WriteString(\"\\n\\n\")\n\t\t}\n\n\tcase *gast.List:\n\t\tr.renderChildren(n, source, depth)\n\t\tif node.NextSibling() != nil {\n\t\t\tr.buf.WriteString(\"\\n\\n\")\n\t\t}\n\n\tcase *gast.ListItem:\n\t\tr.renderListItem(n, source, depth)\n\n\tcase *gast.ThematicBreak:\n\t\tr.buf.WriteString(\"---\")\n\t\tif node.NextSibling() != nil {\n\t\t\tr.buf.WriteString(\"\\n\\n\")\n\t\t}\n\n\tcase *east.Strikethrough:\n\t\tr.buf.WriteString(\"~~\")\n\t\tr.renderChildren(n, source, depth)\n\t\tr.buf.WriteString(\"~~\")\n\n\tcase *east.TaskCheckBox:\n\t\tif n.IsChecked {\n\t\t\tr.buf.WriteString(\"[x] \")\n\t\t} else {\n\t\t\tr.buf.WriteString(\"[ ] \")\n\t\t}\n\n\tcase *east.Table:\n\t\tr.renderTable(n, source)\n\t\tif node.NextSibling() != nil {\n\t\t\tr.buf.WriteString(\"\\n\\n\")\n\t\t}\n\n\t// Custom Memos nodes\n\tcase *mast.TagNode:\n\t\tr.buf.WriteByte('#')\n\t\tr.buf.Write(n.Tag)\n\n\tdefault:\n\t\t// For unknown nodes, try to render children\n\t\tr.renderChildren(n, source, depth)\n\t}\n}\n\n// renderChildren renders all children of a node.\nfunc (r *MarkdownRenderer) renderChildren(node gast.Node, source []byte, depth int) {\n\tchild := node.FirstChild()\n\tfor child != nil {\n\t\tr.renderNode(child, source, depth+1)\n\t\tchild = child.NextSibling()\n\t}\n}\n\n// renderCodeBlock renders a code block.\nfunc (r *MarkdownRenderer) renderCodeBlock(node gast.Node, source []byte) {\n\tif fenced, ok := node.(*gast.FencedCodeBlock); ok {\n\t\t// Fenced code block with language\n\t\tr.buf.WriteString(\"```\")\n\t\tif lang := fenced.Language(source); len(lang) > 0 {\n\t\t\tr.buf.Write(lang)\n\t\t}\n\t\tr.buf.WriteByte('\\n')\n\n\t\t// Write all lines\n\t\tlines := fenced.Lines()\n\t\tfor i := 0; i < lines.Len(); i++ {\n\t\t\tline := lines.At(i)\n\t\t\tr.buf.Write(line.Value(source))\n\t\t}\n\n\t\tr.buf.WriteString(\"```\")\n\t\tif node.NextSibling() != nil {\n\t\t\tr.buf.WriteString(\"\\n\\n\")\n\t\t}\n\t} else if codeBlock, ok := node.(*gast.CodeBlock); ok {\n\t\t// Indented code block\n\t\tlines := codeBlock.Lines()\n\t\tfor i := 0; i < lines.Len(); i++ {\n\t\t\tline := lines.At(i)\n\t\t\tr.buf.WriteString(\"    \")\n\t\t\tr.buf.Write(line.Value(source))\n\t\t}\n\t\tif node.NextSibling() != nil {\n\t\t\tr.buf.WriteString(\"\\n\\n\")\n\t\t}\n\t}\n}\n\n// renderBlockquote renders a blockquote with \"> \" prefix.\nfunc (r *MarkdownRenderer) renderBlockquote(node *gast.Blockquote, source []byte, depth int) {\n\t// Create a temporary buffer for the blockquote content\n\ttempBuf := &bytes.Buffer{}\n\ttempRenderer := &MarkdownRenderer{buf: tempBuf}\n\ttempRenderer.renderChildren(node, source, depth)\n\n\t// Add \"> \" prefix to each line\n\tcontent := tempBuf.String()\n\tlines := strings.Split(strings.TrimRight(content, \"\\n\"), \"\\n\")\n\tfor i, line := range lines {\n\t\tr.buf.WriteString(\"> \")\n\t\tr.buf.WriteString(line)\n\t\tif i < len(lines)-1 {\n\t\t\tr.buf.WriteByte('\\n')\n\t\t}\n\t}\n}\n\n// renderListItem renders a list item with proper indentation and markers.\nfunc (r *MarkdownRenderer) renderListItem(node *gast.ListItem, source []byte, depth int) {\n\tparent := node.Parent()\n\tlist, ok := parent.(*gast.List)\n\tif !ok {\n\t\tr.renderChildren(node, source, depth)\n\t\treturn\n\t}\n\n\t// Add indentation only for nested lists\n\t// Document=0, List=1, ListItem=2 (no indent), nested ListItem=3+ (indent)\n\tif depth > 2 {\n\t\tindent := strings.Repeat(\"  \", depth-2)\n\t\tr.buf.WriteString(indent)\n\t}\n\n\t// Add list marker\n\tif list.IsOrdered() {\n\t\tfmt.Fprintf(r.buf, \"%d. \", list.Start)\n\t\tlist.Start++ // Increment for next item\n\t} else {\n\t\tr.buf.WriteString(\"- \")\n\t}\n\n\t// Render content\n\tr.renderChildren(node, source, depth)\n\n\t// Add newline if there's a next sibling\n\tif node.NextSibling() != nil {\n\t\tr.buf.WriteByte('\\n')\n\t}\n}\n\n// renderTable renders a table in markdown format.\nfunc (r *MarkdownRenderer) renderTable(table *east.Table, source []byte) {\n\t// This is a simplified table renderer\n\t// A full implementation would need to handle alignment, etc.\n\tr.renderChildren(table, source, 0)\n}\n"
  },
  {
    "path": "plugin/markdown/renderer/markdown_renderer_test.go",
    "content": "package renderer\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/yuin/goldmark\"\n\t\"github.com/yuin/goldmark/extension\"\n\t\"github.com/yuin/goldmark/parser\"\n\t\"github.com/yuin/goldmark/text\"\n\n\t\"github.com/usememos/memos/plugin/markdown/extensions\"\n)\n\nfunc TestMarkdownRenderer(t *testing.T) {\n\t// Create goldmark instance with all extensions\n\tmd := goldmark.New(\n\t\tgoldmark.WithExtensions(\n\t\t\textension.GFM,\n\t\t\textensions.TagExtension,\n\t\t),\n\t\tgoldmark.WithParserOptions(\n\t\t\tparser.WithAutoHeadingID(),\n\t\t),\n\t)\n\n\ttests := []struct {\n\t\tname     string\n\t\tinput    string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname:     \"simple text\",\n\t\t\tinput:    \"Hello world\",\n\t\t\texpected: \"Hello world\",\n\t\t},\n\t\t{\n\t\t\tname:     \"paragraph with newlines\",\n\t\t\tinput:    \"First paragraph\\n\\nSecond paragraph\",\n\t\t\texpected: \"First paragraph\\n\\nSecond paragraph\",\n\t\t},\n\t\t{\n\t\t\tname:     \"emphasis\",\n\t\t\tinput:    \"This is *italic* and **bold** text\",\n\t\t\texpected: \"This is *italic* and **bold** text\",\n\t\t},\n\t\t{\n\t\t\tname:     \"headings\",\n\t\t\tinput:    \"# Heading 1\\n\\n## Heading 2\\n\\n### Heading 3\",\n\t\t\texpected: \"# Heading 1\\n\\n## Heading 2\\n\\n### Heading 3\",\n\t\t},\n\t\t{\n\t\t\tname:     \"link\",\n\t\t\tinput:    \"Check [this link](https://example.com)\",\n\t\t\texpected: \"Check [this link](https://example.com)\",\n\t\t},\n\t\t{\n\t\t\tname:     \"image\",\n\t\t\tinput:    \"![alt text](image.png)\",\n\t\t\texpected: \"![alt text](image.png)\",\n\t\t},\n\t\t{\n\t\t\tname:     \"code inline\",\n\t\t\tinput:    \"This is `inline code` here\",\n\t\t\texpected: \"This is `inline code` here\",\n\t\t},\n\t\t{\n\t\t\tname:     \"code block fenced\",\n\t\t\tinput:    \"```go\\nfunc main() {\\n}\\n```\",\n\t\t\texpected: \"```go\\nfunc main() {\\n}\\n```\",\n\t\t},\n\t\t{\n\t\t\tname:     \"unordered list\",\n\t\t\tinput:    \"- Item 1\\n- Item 2\\n- Item 3\",\n\t\t\texpected: \"- Item 1\\n- Item 2\\n- Item 3\",\n\t\t},\n\t\t{\n\t\t\tname:     \"ordered list\",\n\t\t\tinput:    \"1. First\\n2. Second\\n3. Third\",\n\t\t\texpected: \"1. First\\n2. Second\\n3. Third\",\n\t\t},\n\t\t{\n\t\t\tname:     \"blockquote\",\n\t\t\tinput:    \"> This is a quote\\n> Second line\",\n\t\t\texpected: \"> This is a quote\\n> Second line\",\n\t\t},\n\t\t{\n\t\t\tname:     \"horizontal rule\",\n\t\t\tinput:    \"Text before\\n\\n---\\n\\nText after\",\n\t\t\texpected: \"Text before\\n\\n---\\n\\nText after\",\n\t\t},\n\t\t{\n\t\t\tname:     \"strikethrough\",\n\t\t\tinput:    \"This is ~~deleted~~ text\",\n\t\t\texpected: \"This is ~~deleted~~ text\",\n\t\t},\n\t\t{\n\t\t\tname:     \"task list\",\n\t\t\tinput:    \"- [x] Completed task\\n- [ ] Incomplete task\",\n\t\t\texpected: \"- [x] Completed task\\n- [ ] Incomplete task\",\n\t\t},\n\t\t{\n\t\t\tname:     \"tag\",\n\t\t\tinput:    \"This has #tag in it\",\n\t\t\texpected: \"This has #tag in it\",\n\t\t},\n\t\t{\n\t\t\tname:     \"multiple tags\",\n\t\t\tinput:    \"#work #important meeting notes\",\n\t\t\texpected: \"#work #important meeting notes\",\n\t\t},\n\t\t{\n\t\t\tname:     \"complex mixed content\",\n\t\t\tinput:    \"# Meeting Notes\\n\\n**Date**: 2024-01-01\\n\\n## Attendees\\n- Alice\\n- Bob\\n\\n## Discussion\\n\\nWe discussed #project status.\\n\\n```python\\nprint('hello')\\n```\",\n\t\t\texpected: \"# Meeting Notes\\n\\n**Date**: 2024-01-01\\n\\n## Attendees\\n\\n- Alice\\n- Bob\\n\\n## Discussion\\n\\nWe discussed #project status.\\n\\n```python\\nprint('hello')\\n```\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\t// Parse the input\n\t\t\tsource := []byte(tt.input)\n\t\t\treader := text.NewReader(source)\n\t\t\tdoc := md.Parser().Parse(reader)\n\t\t\trequire.NotNil(t, doc)\n\n\t\t\t// Render back to markdown\n\t\t\trenderer := NewMarkdownRenderer()\n\t\t\tresult := renderer.Render(doc, source)\n\n\t\t\t// For debugging\n\t\t\tif result != tt.expected {\n\t\t\t\tt.Logf(\"Input:    %q\", tt.input)\n\t\t\t\tt.Logf(\"Expected: %q\", tt.expected)\n\t\t\t\tt.Logf(\"Got:      %q\", result)\n\t\t\t}\n\n\t\t\tassert.Equal(t, tt.expected, result)\n\t\t})\n\t}\n}\n\nfunc TestMarkdownRendererPreservesStructure(t *testing.T) {\n\t// Test that parsing and rendering preserves structure\n\tmd := goldmark.New(\n\t\tgoldmark.WithExtensions(\n\t\t\textension.GFM,\n\t\t\textensions.TagExtension,\n\t\t),\n\t)\n\n\tinputs := []string{\n\t\t\"# Title\\n\\nParagraph\",\n\t\t\"**Bold** and *italic*\",\n\t\t\"- List\\n- Items\",\n\t\t\"#tag #another\",\n\t\t\"> Quote\",\n\t}\n\n\trenderer := NewMarkdownRenderer()\n\n\tfor _, input := range inputs {\n\t\tt.Run(input, func(t *testing.T) {\n\t\t\tsource := []byte(input)\n\t\t\treader := text.NewReader(source)\n\t\t\tdoc := md.Parser().Parse(reader)\n\n\t\t\tresult := renderer.Render(doc, source)\n\n\t\t\t// The result should be structurally similar\n\t\t\t// (may have minor formatting differences)\n\t\t\tassert.NotEmpty(t, result)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "plugin/scheduler/README.md",
    "content": "# Scheduler Plugin\n\nA production-ready, GitHub Actions-inspired cron job scheduler for Go.\n\n## Features\n\n- **Standard Cron Syntax**: Supports both 5-field and 6-field (with seconds) cron expressions\n- **Timezone-Aware**: Explicit timezone handling to avoid DST surprises\n- **Middleware Pattern**: Composable job wrappers for logging, metrics, panic recovery, timeouts\n- **Graceful Shutdown**: Jobs complete cleanly or cancel when context expires\n- **Zero Dependencies**: Core functionality uses only the standard library\n- **Type-Safe**: Strong typing with clear error messages\n- **Well-Tested**: Comprehensive test coverage\n\n## Installation\n\nThis package is included with Memos. No separate installation required.\n\n## Quick Start\n\n```go\npackage main\n\nimport (\n    \"context\"\n    \"fmt\"\n    \"github.com/usememos/memos/plugin/scheduler\"\n)\n\nfunc main() {\n    s := scheduler.New()\n\n    s.Register(&scheduler.Job{\n        Name:     \"daily-cleanup\",\n        Schedule: \"0 2 * * *\", // 2 AM daily\n        Handler: func(ctx context.Context) error {\n            fmt.Println(\"Running cleanup...\")\n            return nil\n        },\n    })\n\n    s.Start()\n    defer s.Stop(context.Background())\n\n    // Keep running...\n    select {}\n}\n```\n\n## Cron Expression Format\n\n### 5-Field Format (Standard)\n```\n┌───────────── minute (0 - 59)\n│ ┌───────────── hour (0 - 23)\n│ │ ┌───────────── day of month (1 - 31)\n│ │ │ ┌───────────── month (1 - 12)\n│ │ │ │ ┌───────────── day of week (0 - 7) (Sunday = 0 or 7)\n│ │ │ │ │\n* * * * *\n```\n\n### 6-Field Format (With Seconds)\n```\n┌───────────── second (0 - 59)\n│ ┌───────────── minute (0 - 59)\n│ │ ┌───────────── hour (0 - 23)\n│ │ │ ┌───────────── day of month (1 - 31)\n│ │ │ │ ┌───────────── month (1 - 12)\n│ │ │ │ │ ┌───────────── day of week (0 - 7)\n│ │ │ │ │ │\n* * * * * *\n```\n\n### Special Characters\n\n- `*` - Any value (every minute, every hour, etc.)\n- `,` - List of values: `1,15,30` (1st, 15th, and 30th)\n- `-` - Range: `9-17` (9 AM through 5 PM)\n- `/` - Step: `*/15` (every 15 units)\n\n### Common Examples\n\n| Schedule | Description |\n|----------|-------------|\n| `* * * * *` | Every minute |\n| `0 * * * *` | Every hour |\n| `0 0 * * *` | Daily at midnight |\n| `0 9 * * 1-5` | Weekdays at 9 AM |\n| `*/15 * * * *` | Every 15 minutes |\n| `0 0 1 * *` | First day of every month |\n| `0 0 * * 0` | Every Sunday at midnight |\n| `30 14 * * *` | Every day at 2:30 PM |\n\n## Timezone Support\n\n```go\n// Global timezone for all jobs\ns := scheduler.New(\n    scheduler.WithTimezone(\"America/New_York\"),\n)\n\n// Per-job timezone (overrides global)\ns.Register(&scheduler.Job{\n    Name:     \"tokyo-report\",\n    Schedule: \"0 9 * * *\", // 9 AM Tokyo time\n    Timezone: \"Asia/Tokyo\",\n    Handler: func(ctx context.Context) error {\n        // Runs at 9 AM in Tokyo\n        return nil\n    },\n})\n```\n\n**Important**: Always use IANA timezone names (`America/New_York`, not `EST`).\n\n## Middleware\n\nMiddleware wraps job handlers to add cross-cutting behavior. Multiple middleware can be chained together.\n\n### Built-in Middleware\n\n#### Recovery (Panic Handling)\n\n```go\ns := scheduler.New(\n    scheduler.WithMiddleware(\n        scheduler.Recovery(func(jobName string, r interface{}) {\n            log.Printf(\"Job %s panicked: %v\", jobName, r)\n        }),\n    ),\n)\n```\n\n#### Logging\n\n```go\ntype Logger interface {\n    Info(msg string, args ...interface{})\n    Error(msg string, args ...interface{})\n}\n\ns := scheduler.New(\n    scheduler.WithMiddleware(\n        scheduler.Logging(myLogger),\n    ),\n)\n```\n\n#### Timeout\n\n```go\ns := scheduler.New(\n    scheduler.WithMiddleware(\n        scheduler.Timeout(5 * time.Minute),\n    ),\n)\n```\n\n### Combining Middleware\n\n```go\ns := scheduler.New(\n    scheduler.WithMiddleware(\n        scheduler.Recovery(panicHandler),\n        scheduler.Logging(logger),\n        scheduler.Timeout(10 * time.Minute),\n    ),\n)\n```\n\n**Order matters**: Middleware are applied left-to-right. In the example above:\n1. Recovery (outermost) catches panics from everything\n2. Logging logs the execution\n3. Timeout (innermost) wraps the actual handler\n\n### Custom Middleware\n\n```go\nfunc Metrics(recorder MetricsRecorder) scheduler.Middleware {\n    return func(next scheduler.JobHandler) scheduler.JobHandler {\n        return func(ctx context.Context) error {\n            start := time.Now()\n            err := next(ctx)\n            duration := time.Since(start)\n\n            jobName := scheduler.GetJobName(ctx)\n            recorder.Record(jobName, duration, err)\n\n            return err\n        }\n    }\n}\n```\n\n## Graceful Shutdown\n\nAlways use `Stop()` with a context to allow jobs to finish cleanly:\n\n```go\n// Give jobs up to 30 seconds to complete\nctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)\ndefer cancel()\n\nif err := s.Stop(ctx); err != nil {\n    log.Printf(\"Shutdown error: %v\", err)\n}\n```\n\nJobs should respect context cancellation:\n\n```go\nHandler: func(ctx context.Context) error {\n    for i := 0; i < 100; i++ {\n        select {\n        case <-ctx.Done():\n            return ctx.Err() // Canceled\n        default:\n            // Do work\n        }\n    }\n    return nil\n}\n```\n\n## Best Practices\n\n### 1. Always Name Your Jobs\n\nNames are used for logging, metrics, and debugging:\n\n```go\nName: \"user-cleanup-job\" // Good\nName: \"job1\"             // Bad\n```\n\n### 2. Add Descriptions and Tags\n\n```go\ns.Register(&scheduler.Job{\n    Name:        \"stale-session-cleanup\",\n    Description: \"Removes user sessions older than 30 days\",\n    Tags:        []string{\"maintenance\", \"security\"},\n    Schedule:    \"0 3 * * *\",\n    Handler:     cleanupSessions,\n})\n```\n\n### 3. Use Appropriate Middleware\n\nAlways include Recovery and Logging in production:\n\n```go\nscheduler.New(\n    scheduler.WithMiddleware(\n        scheduler.Recovery(logPanic),\n        scheduler.Logging(logger),\n    ),\n)\n```\n\n### 4. Avoid Scheduling Exactly on the Hour\n\nMany systems schedule jobs at `:00`, causing load spikes. Stagger your jobs:\n\n```go\n\"5 2 * * *\"  // 2:05 AM (good)\n\"0 2 * * *\"  // 2:00 AM (often overloaded)\n```\n\n### 5. Make Jobs Idempotent\n\nJobs may run multiple times (crash recovery, etc.). Design them to be safely re-runnable:\n\n```go\nHandler: func(ctx context.Context) error {\n    // Use unique constraint or check-before-insert\n    db.Exec(\"INSERT IGNORE INTO processed_items ...\")\n    return nil\n}\n```\n\n### 6. Handle Timezones Explicitly\n\nAlways specify timezone for business-hour jobs:\n\n```go\nTimezone: \"America/New_York\" // Good\n// Timezone: \"\"              // Bad (defaults to UTC)\n```\n\n### 7. Test Your Cron Expressions\n\nUse a cron expression calculator before deploying:\n- [crontab.guru](https://crontab.guru/)\n- Write unit tests with the parser\n\n## Testing Jobs\n\nTest job handlers independently of the scheduler:\n\n```go\nfunc TestCleanupJob(t *testing.T) {\n    ctx := context.Background()\n\n    err := cleanupHandler(ctx)\n    if err != nil {\n        t.Fatalf(\"cleanup failed: %v\", err)\n    }\n\n    // Verify cleanup occurred\n}\n```\n\nTest schedule parsing:\n\n```go\nfunc TestScheduleParsing(t *testing.T) {\n    job := &scheduler.Job{\n        Name:     \"test\",\n        Schedule: \"0 2 * * *\",\n        Handler:  func(ctx context.Context) error { return nil },\n    }\n\n    if err := job.Validate(); err != nil {\n        t.Fatalf(\"invalid job: %v\", err)\n    }\n}\n```\n\n## Comparison to Other Solutions\n\n| Feature | scheduler | robfig/cron | github.com/go-co-op/gocron |\n|---------|-----------|-------------|----------------------------|\n| Standard cron syntax | ✅ | ✅ | ✅ |\n| Seconds support | ✅ | ✅ | ✅ |\n| Timezone support | ✅ | ✅ | ✅ |\n| Middleware pattern | ✅ | ⚠️ (basic) | ❌ |\n| Graceful shutdown | ✅ | ⚠️ (basic) | ✅ |\n| Zero dependencies | ✅ | ❌ | ❌ |\n| Job metadata | ✅ | ❌ | ⚠️ (limited) |\n\n## API Reference\n\nSee [example_test.go](./example_test.go) for comprehensive examples.\n\n### Core Types\n\n- `Scheduler` - Manages scheduled jobs\n- `Job` - Job definition with schedule and handler\n- `Middleware` - Function that wraps job handlers\n\n### Functions\n\n- `New(opts ...Option) *Scheduler` - Create new scheduler\n- `WithTimezone(tz string) Option` - Set default timezone\n- `WithMiddleware(mw ...Middleware) Option` - Add middleware\n\n### Methods\n\n- `Register(job *Job) error` - Add job to scheduler\n- `Start() error` - Begin executing jobs\n- `Stop(ctx context.Context) error` - Graceful shutdown\n\n## License\n\nThis package is part of the Memos project and shares its license.\n"
  },
  {
    "path": "plugin/scheduler/doc.go",
    "content": "// Package scheduler provides a GitHub Actions-inspired cron job scheduler.\n//\n// Features:\n//   - Standard cron expression syntax (5-field and 6-field formats)\n//   - Timezone-aware scheduling\n//   - Middleware pattern for cross-cutting concerns (logging, metrics, recovery)\n//   - Graceful shutdown with context cancellation\n//   - Zero external dependencies\n//\n// Basic usage:\n//\n//\ts := scheduler.New()\n//\n//\ts.Register(&scheduler.Job{\n//\t\tName:     \"daily-cleanup\",\n//\t\tSchedule: \"0 2 * * *\", // 2 AM daily\n//\t\tHandler: func(ctx context.Context) error {\n//\t\t\t// Your cleanup logic here\n//\t\t\treturn nil\n//\t\t},\n//\t})\n//\n//\ts.Start()\n//\tdefer s.Stop(context.Background())\n//\n// With middleware:\n//\n//\ts := scheduler.New(\n//\t\tscheduler.WithTimezone(\"America/New_York\"),\n//\t\tscheduler.WithMiddleware(\n//\t\t\tscheduler.Recovery(),\n//\t\t\tscheduler.Logging(),\n//\t\t),\n//\t)\npackage scheduler\n"
  },
  {
    "path": "plugin/scheduler/example_test.go",
    "content": "package scheduler_test\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"os\"\n\t\"time\"\n\n\t\"github.com/usememos/memos/plugin/scheduler\"\n)\n\n// Example demonstrates basic scheduler usage.\nfunc Example_basic() {\n\ts := scheduler.New()\n\n\ts.Register(&scheduler.Job{\n\t\tName:        \"hello\",\n\t\tSchedule:    \"*/5 * * * *\", // Every 5 minutes\n\t\tDescription: \"Say hello\",\n\t\tHandler: func(_ context.Context) error {\n\t\t\tfmt.Println(\"Hello from scheduler!\")\n\t\t\treturn nil\n\t\t},\n\t})\n\n\ts.Start()\n\tdefer s.Stop(context.Background())\n\n\t// Scheduler runs in background\n\ttime.Sleep(100 * time.Millisecond)\n}\n\n// Example demonstrates timezone-aware scheduling.\nfunc Example_timezone() {\n\ts := scheduler.New(\n\t\tscheduler.WithTimezone(\"America/New_York\"),\n\t)\n\n\ts.Register(&scheduler.Job{\n\t\tName:     \"daily-report\",\n\t\tSchedule: \"0 9 * * *\", // 9 AM in New York\n\t\tHandler: func(_ context.Context) error {\n\t\t\tfmt.Println(\"Generating daily report...\")\n\t\t\treturn nil\n\t\t},\n\t})\n\n\ts.Start()\n\tdefer s.Stop(context.Background())\n}\n\n// Example demonstrates middleware usage.\nfunc Example_middleware() {\n\tlogger := slog.New(slog.NewTextHandler(os.Stdout, nil))\n\n\ts := scheduler.New(\n\t\tscheduler.WithMiddleware(\n\t\t\tscheduler.Recovery(func(jobName string, r interface{}) {\n\t\t\t\tlogger.Error(\"Job panicked\", \"job\", jobName, \"panic\", r)\n\t\t\t}),\n\t\t\tscheduler.Logging(&slogAdapter{logger}),\n\t\t\tscheduler.Timeout(5*time.Minute),\n\t\t),\n\t)\n\n\ts.Register(&scheduler.Job{\n\t\tName:     \"data-sync\",\n\t\tSchedule: \"0 */2 * * *\", // Every 2 hours\n\t\tHandler: func(_ context.Context) error {\n\t\t\t// Your sync logic here\n\t\t\treturn nil\n\t\t},\n\t})\n\n\ts.Start()\n\tdefer s.Stop(context.Background())\n}\n\n// slogAdapter adapts slog.Logger to scheduler.Logger interface.\ntype slogAdapter struct {\n\tlogger *slog.Logger\n}\n\nfunc (a *slogAdapter) Info(msg string, args ...interface{}) {\n\ta.logger.Info(msg, args...)\n}\n\nfunc (a *slogAdapter) Error(msg string, args ...interface{}) {\n\ta.logger.Error(msg, args...)\n}\n\n// Example demonstrates multiple jobs with different schedules.\nfunc Example_multipleJobs() {\n\ts := scheduler.New()\n\n\t// Cleanup old data every night at 2 AM\n\ts.Register(&scheduler.Job{\n\t\tName:     \"cleanup\",\n\t\tSchedule: \"0 2 * * *\",\n\t\tTags:     []string{\"maintenance\"},\n\t\tHandler: func(_ context.Context) error {\n\t\t\tfmt.Println(\"Cleaning up old data...\")\n\t\t\treturn nil\n\t\t},\n\t})\n\n\t// Health check every 5 minutes\n\ts.Register(&scheduler.Job{\n\t\tName:     \"health-check\",\n\t\tSchedule: \"*/5 * * * *\",\n\t\tTags:     []string{\"monitoring\"},\n\t\tHandler: func(_ context.Context) error {\n\t\t\tfmt.Println(\"Running health check...\")\n\t\t\treturn nil\n\t\t},\n\t})\n\n\t// Weekly backup on Sundays at 1 AM\n\ts.Register(&scheduler.Job{\n\t\tName:     \"weekly-backup\",\n\t\tSchedule: \"0 1 * * 0\",\n\t\tTags:     []string{\"backup\"},\n\t\tHandler: func(_ context.Context) error {\n\t\t\tfmt.Println(\"Creating weekly backup...\")\n\t\t\treturn nil\n\t\t},\n\t})\n\n\ts.Start()\n\tdefer s.Stop(context.Background())\n}\n\n// Example demonstrates graceful shutdown with timeout.\nfunc Example_gracefulShutdown() {\n\ts := scheduler.New()\n\n\ts.Register(&scheduler.Job{\n\t\tName:     \"long-running\",\n\t\tSchedule: \"* * * * *\",\n\t\tHandler: func(ctx context.Context) error {\n\t\t\tselect {\n\t\t\tcase <-time.After(30 * time.Second):\n\t\t\t\tfmt.Println(\"Job completed\")\n\t\t\tcase <-ctx.Done():\n\t\t\t\tfmt.Println(\"Job canceled, cleaning up...\")\n\t\t\t\treturn ctx.Err()\n\t\t\t}\n\t\t\treturn nil\n\t\t},\n\t})\n\n\ts.Start()\n\n\t// Simulate shutdown signal\n\ttime.Sleep(5 * time.Second)\n\n\t// Give jobs 10 seconds to finish\n\tshutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)\n\tdefer cancel()\n\n\tif err := s.Stop(shutdownCtx); err != nil {\n\t\tfmt.Printf(\"Shutdown error: %v\\n\", err)\n\t}\n}\n"
  },
  {
    "path": "plugin/scheduler/integration_test.go",
    "content": "package scheduler_test\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"strings\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/usememos/memos/plugin/scheduler\"\n)\n\n// TestRealWorldScenario tests a realistic multi-job scenario.\nfunc TestRealWorldScenario(t *testing.T) {\n\tvar (\n\t\tquickJobCount  atomic.Int32\n\t\thourlyJobCount atomic.Int32\n\t\tlogEntries     []string\n\t\tlogMu          sync.Mutex\n\t)\n\n\tlogger := &testLogger{\n\t\tonInfo: func(msg string, _ ...interface{}) {\n\t\t\tlogMu.Lock()\n\t\t\tlogEntries = append(logEntries, fmt.Sprintf(\"INFO: %s\", msg))\n\t\t\tlogMu.Unlock()\n\t\t},\n\t\tonError: func(msg string, _ ...interface{}) {\n\t\t\tlogMu.Lock()\n\t\t\tlogEntries = append(logEntries, fmt.Sprintf(\"ERROR: %s\", msg))\n\t\t\tlogMu.Unlock()\n\t\t},\n\t}\n\n\ts := scheduler.New(\n\t\tscheduler.WithTimezone(\"UTC\"),\n\t\tscheduler.WithMiddleware(\n\t\t\tscheduler.Recovery(func(jobName string, r interface{}) {\n\t\t\t\tt.Logf(\"Job %s panicked: %v\", jobName, r)\n\t\t\t}),\n\t\t\tscheduler.Logging(logger),\n\t\t\tscheduler.Timeout(5*time.Second),\n\t\t),\n\t)\n\n\t// Quick job (every second)\n\ts.Register(&scheduler.Job{\n\t\tName:     \"quick-check\",\n\t\tSchedule: \"* * * * * *\",\n\t\tHandler: func(_ context.Context) error {\n\t\t\tquickJobCount.Add(1)\n\t\t\ttime.Sleep(100 * time.Millisecond)\n\t\t\treturn nil\n\t\t},\n\t})\n\n\t// Slower job (every 2 seconds)\n\ts.Register(&scheduler.Job{\n\t\tName:     \"slow-process\",\n\t\tSchedule: \"*/2 * * * * *\",\n\t\tHandler: func(_ context.Context) error {\n\t\t\thourlyJobCount.Add(1)\n\t\t\ttime.Sleep(500 * time.Millisecond)\n\t\t\treturn nil\n\t\t},\n\t})\n\n\t// Start scheduler\n\tif err := s.Start(); err != nil {\n\t\tt.Fatalf(\"failed to start scheduler: %v\", err)\n\t}\n\n\t// Let it run for 5 seconds\n\ttime.Sleep(5 * time.Second)\n\n\t// Graceful shutdown\n\tctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)\n\tdefer cancel()\n\n\tif err := s.Stop(ctx); err != nil {\n\t\tt.Fatalf(\"failed to stop scheduler: %v\", err)\n\t}\n\n\t// Verify execution counts\n\tquick := quickJobCount.Load()\n\tslow := hourlyJobCount.Load()\n\n\tt.Logf(\"Quick job ran %d times\", quick)\n\tt.Logf(\"Slow job ran %d times\", slow)\n\n\tif quick < 4 {\n\t\tt.Errorf(\"expected quick job to run at least 4 times, ran %d\", quick)\n\t}\n\n\tif slow < 2 {\n\t\tt.Errorf(\"expected slow job to run at least 2 times, ran %d\", slow)\n\t}\n\n\t// Verify logging\n\tlogMu.Lock()\n\tdefer logMu.Unlock()\n\n\thasStartLog := false\n\thasCompleteLog := false\n\tfor _, entry := range logEntries {\n\t\tif contains(entry, \"Job started\") {\n\t\t\thasStartLog = true\n\t\t}\n\t\tif contains(entry, \"Job completed\") {\n\t\t\thasCompleteLog = true\n\t\t}\n\t}\n\n\tif !hasStartLog {\n\t\tt.Error(\"expected job start logs\")\n\t}\n\tif !hasCompleteLog {\n\t\tt.Error(\"expected job completion logs\")\n\t}\n}\n\n// TestCancellationDuringExecution verifies jobs can be canceled mid-execution.\nfunc TestCancellationDuringExecution(t *testing.T) {\n\tvar canceled atomic.Bool\n\tvar started atomic.Bool\n\n\ts := scheduler.New()\n\n\ts.Register(&scheduler.Job{\n\t\tName:     \"long-job\",\n\t\tSchedule: \"* * * * * *\",\n\t\tHandler: func(ctx context.Context) error {\n\t\t\tstarted.Store(true)\n\t\t\t// Simulate long-running work\n\t\t\tfor i := 0; i < 100; i++ {\n\t\t\t\tselect {\n\t\t\t\tcase <-ctx.Done():\n\t\t\t\t\tcanceled.Store(true)\n\t\t\t\t\treturn ctx.Err()\n\t\t\t\tcase <-time.After(100 * time.Millisecond):\n\t\t\t\t\t// Keep working\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn nil\n\t\t},\n\t})\n\n\tif err := s.Start(); err != nil {\n\t\tt.Fatalf(\"failed to start: %v\", err)\n\t}\n\n\t// Wait until job starts\n\tfor i := 0; i < 30; i++ {\n\t\tif started.Load() {\n\t\t\tbreak\n\t\t}\n\t\ttime.Sleep(100 * time.Millisecond)\n\t}\n\n\tif !started.Load() {\n\t\tt.Fatal(\"job did not start within timeout\")\n\t}\n\n\t// Stop with reasonable timeout\n\tctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)\n\tdefer cancel()\n\n\tif err := s.Stop(ctx); err != nil {\n\t\tt.Logf(\"stop returned error (may be expected): %v\", err)\n\t}\n\n\tif !canceled.Load() {\n\t\tt.Error(\"expected job to detect cancellation\")\n\t}\n}\n\n// TestTimezoneHandling verifies timezone-aware scheduling.\nfunc TestTimezoneHandling(t *testing.T) {\n\t// Parse a schedule in a specific timezone\n\tschedule, err := scheduler.ParseCronExpression(\"0 9 * * *\") // 9 AM\n\tif err != nil {\n\t\tt.Fatalf(\"failed to parse schedule: %v\", err)\n\t}\n\n\t// Test in New York timezone\n\tnyc, err := time.LoadLocation(\"America/New_York\")\n\tif err != nil {\n\t\tt.Fatalf(\"failed to load timezone: %v\", err)\n\t}\n\n\t// Current time: 8:30 AM in New York\n\tnow := time.Date(2025, 1, 15, 8, 30, 0, 0, nyc)\n\n\t// Next run should be 9:00 AM same day\n\tnext := schedule.Next(now)\n\texpected := time.Date(2025, 1, 15, 9, 0, 0, 0, nyc)\n\n\tif !next.Equal(expected) {\n\t\tt.Errorf(\"next = %v, expected %v\", next, expected)\n\t}\n\n\t// If it's already past 9 AM\n\tnow = time.Date(2025, 1, 15, 9, 30, 0, 0, nyc)\n\tnext = schedule.Next(now)\n\texpected = time.Date(2025, 1, 16, 9, 0, 0, 0, nyc)\n\n\tif !next.Equal(expected) {\n\t\tt.Errorf(\"next = %v, expected %v\", next, expected)\n\t}\n}\n\n// TestErrorPropagation verifies error handling.\nfunc TestErrorPropagation(t *testing.T) {\n\tvar errorLogged atomic.Bool\n\n\tlogger := &testLogger{\n\t\tonError: func(msg string, _ ...interface{}) {\n\t\t\tif msg == \"Job failed\" {\n\t\t\t\terrorLogged.Store(true)\n\t\t\t}\n\t\t},\n\t}\n\n\ts := scheduler.New(\n\t\tscheduler.WithMiddleware(\n\t\t\tscheduler.Logging(logger),\n\t\t),\n\t)\n\n\ts.Register(&scheduler.Job{\n\t\tName:     \"failing-job\",\n\t\tSchedule: \"* * * * * *\",\n\t\tHandler: func(_ context.Context) error {\n\t\t\treturn errors.New(\"intentional error\")\n\t\t},\n\t})\n\n\tif err := s.Start(); err != nil {\n\t\tt.Fatalf(\"failed to start: %v\", err)\n\t}\n\n\t// Let it run once\n\ttime.Sleep(1500 * time.Millisecond)\n\n\tctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)\n\tdefer cancel()\n\n\tif err := s.Stop(ctx); err != nil {\n\t\tt.Fatalf(\"failed to stop: %v\", err)\n\t}\n\n\tif !errorLogged.Load() {\n\t\tt.Error(\"expected error to be logged\")\n\t}\n}\n\n// TestPanicRecovery verifies panic recovery middleware.\nfunc TestPanicRecovery(t *testing.T) {\n\tvar panicRecovered atomic.Bool\n\n\ts := scheduler.New(\n\t\tscheduler.WithMiddleware(\n\t\t\tscheduler.Recovery(func(jobName string, r interface{}) {\n\t\t\t\tpanicRecovered.Store(true)\n\t\t\t\tt.Logf(\"Recovered from panic in job %s: %v\", jobName, r)\n\t\t\t}),\n\t\t),\n\t)\n\n\ts.Register(&scheduler.Job{\n\t\tName:     \"panicking-job\",\n\t\tSchedule: \"* * * * * *\",\n\t\tHandler: func(_ context.Context) error {\n\t\t\tpanic(\"intentional panic for testing\")\n\t\t},\n\t})\n\n\tif err := s.Start(); err != nil {\n\t\tt.Fatalf(\"failed to start: %v\", err)\n\t}\n\n\t// Let it run once\n\ttime.Sleep(1500 * time.Millisecond)\n\n\tctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)\n\tdefer cancel()\n\n\tif err := s.Stop(ctx); err != nil {\n\t\tt.Fatalf(\"failed to stop: %v\", err)\n\t}\n\n\tif !panicRecovered.Load() {\n\t\tt.Error(\"expected panic to be recovered\")\n\t}\n}\n\n// TestMultipleJobsWithDifferentSchedules verifies concurrent job execution.\nfunc TestMultipleJobsWithDifferentSchedules(t *testing.T) {\n\tvar (\n\t\tjob1Count atomic.Int32\n\t\tjob2Count atomic.Int32\n\t\tjob3Count atomic.Int32\n\t)\n\n\ts := scheduler.New()\n\n\t// Job 1: Every second\n\ts.Register(&scheduler.Job{\n\t\tName:     \"job-1sec\",\n\t\tSchedule: \"* * * * * *\",\n\t\tHandler: func(_ context.Context) error {\n\t\t\tjob1Count.Add(1)\n\t\t\treturn nil\n\t\t},\n\t})\n\n\t// Job 2: Every 2 seconds\n\ts.Register(&scheduler.Job{\n\t\tName:     \"job-2sec\",\n\t\tSchedule: \"*/2 * * * * *\",\n\t\tHandler: func(_ context.Context) error {\n\t\t\tjob2Count.Add(1)\n\t\t\treturn nil\n\t\t},\n\t})\n\n\t// Job 3: Every 3 seconds\n\ts.Register(&scheduler.Job{\n\t\tName:     \"job-3sec\",\n\t\tSchedule: \"*/3 * * * * *\",\n\t\tHandler: func(_ context.Context) error {\n\t\t\tjob3Count.Add(1)\n\t\t\treturn nil\n\t\t},\n\t})\n\n\tif err := s.Start(); err != nil {\n\t\tt.Fatalf(\"failed to start: %v\", err)\n\t}\n\n\t// Let them run for 6 seconds\n\ttime.Sleep(6 * time.Second)\n\n\tctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)\n\tdefer cancel()\n\n\tif err := s.Stop(ctx); err != nil {\n\t\tt.Fatalf(\"failed to stop: %v\", err)\n\t}\n\n\t// Verify counts (allowing for timing variance)\n\tc1 := job1Count.Load()\n\tc2 := job2Count.Load()\n\tc3 := job3Count.Load()\n\n\tt.Logf(\"Job 1 ran %d times, Job 2 ran %d times, Job 3 ran %d times\", c1, c2, c3)\n\n\tif c1 < 5 {\n\t\tt.Errorf(\"expected job1 to run at least 5 times, ran %d\", c1)\n\t}\n\tif c2 < 2 {\n\t\tt.Errorf(\"expected job2 to run at least 2 times, ran %d\", c2)\n\t}\n\tif c3 < 1 {\n\t\tt.Errorf(\"expected job3 to run at least 1 time, ran %d\", c3)\n\t}\n}\n\n// Helpers\n\ntype testLogger struct {\n\tonInfo  func(msg string, args ...interface{})\n\tonError func(msg string, args ...interface{})\n}\n\nfunc (l *testLogger) Info(msg string, args ...interface{}) {\n\tif l.onInfo != nil {\n\t\tl.onInfo(msg, args...)\n\t}\n}\n\nfunc (l *testLogger) Error(msg string, args ...interface{}) {\n\tif l.onError != nil {\n\t\tl.onError(msg, args...)\n\t}\n}\n\nfunc contains(s, substr string) bool {\n\treturn strings.Contains(s, substr)\n}\n"
  },
  {
    "path": "plugin/scheduler/job.go",
    "content": "package scheduler\n\nimport (\n\t\"context\"\n\n\t\"github.com/pkg/errors\"\n)\n\n// JobHandler is the function signature for scheduled job handlers.\n// The context passed to the handler will be canceled if the scheduler is shutting down.\ntype JobHandler func(ctx context.Context) error\n\n// Job represents a scheduled task.\ntype Job struct {\n\t// Name is a unique identifier for this job (required).\n\t// Used for logging and metrics.\n\tName string\n\n\t// Schedule is a cron expression defining when this job runs (required).\n\t// Supports standard 5-field format: \"minute hour day month weekday\"\n\t// Examples: \"0 * * * *\" (hourly), \"0 0 * * *\" (daily at midnight)\n\tSchedule string\n\n\t// Timezone for schedule evaluation (optional, defaults to UTC).\n\t// Use IANA timezone names: \"America/New_York\", \"Europe/London\", etc.\n\tTimezone string\n\n\t// Handler is the function to execute when the job triggers (required).\n\tHandler JobHandler\n\n\t// Description provides human-readable context about what this job does (optional).\n\tDescription string\n\n\t// Tags allow categorizing jobs for filtering/monitoring (optional).\n\tTags []string\n}\n\n// Validate checks if the job definition is valid.\nfunc (j *Job) Validate() error {\n\tif j.Name == \"\" {\n\t\treturn errors.New(\"job name is required\")\n\t}\n\n\tif j.Schedule == \"\" {\n\t\treturn errors.New(\"job schedule is required\")\n\t}\n\n\t// Validate cron expression using parser\n\tif _, err := ParseCronExpression(j.Schedule); err != nil {\n\t\treturn errors.Wrap(err, \"invalid cron expression\")\n\t}\n\n\tif j.Handler == nil {\n\t\treturn errors.New(\"job handler is required\")\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "plugin/scheduler/job_test.go",
    "content": "package scheduler\n\nimport (\n\t\"context\"\n\t\"testing\"\n)\n\nfunc TestJobDefinition(t *testing.T) {\n\tcallCount := 0\n\tjob := &Job{\n\t\tName: \"test-job\",\n\t\tHandler: func(_ context.Context) error {\n\t\t\tcallCount++\n\t\t\treturn nil\n\t\t},\n\t}\n\n\tif job.Name != \"test-job\" {\n\t\tt.Errorf(\"expected name 'test-job', got %s\", job.Name)\n\t}\n\n\t// Test handler execution\n\tif err := job.Handler(context.Background()); err != nil {\n\t\tt.Fatalf(\"handler failed: %v\", err)\n\t}\n\n\tif callCount != 1 {\n\t\tt.Errorf(\"expected handler to be called once, called %d times\", callCount)\n\t}\n}\n\nfunc TestJobValidation(t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\tjob     *Job\n\t\twantErr bool\n\t}{\n\t\t{\n\t\t\tname: \"valid job\",\n\t\t\tjob: &Job{\n\t\t\t\tName:     \"valid\",\n\t\t\t\tSchedule: \"0 * * * *\",\n\t\t\t\tHandler:  func(_ context.Context) error { return nil },\n\t\t\t},\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"missing name\",\n\t\t\tjob: &Job{\n\t\t\t\tSchedule: \"0 * * * *\",\n\t\t\t\tHandler:  func(_ context.Context) error { return nil },\n\t\t\t},\n\t\t\twantErr: true,\n\t\t},\n\t\t{\n\t\t\tname: \"missing schedule\",\n\t\t\tjob: &Job{\n\t\t\t\tName:    \"test\",\n\t\t\t\tHandler: func(_ context.Context) error { return nil },\n\t\t\t},\n\t\t\twantErr: true,\n\t\t},\n\t\t{\n\t\t\tname: \"invalid cron expression\",\n\t\t\tjob: &Job{\n\t\t\t\tName:     \"test\",\n\t\t\t\tSchedule: \"invalid cron\",\n\t\t\t\tHandler:  func(_ context.Context) error { return nil },\n\t\t\t},\n\t\t\twantErr: true,\n\t\t},\n\t\t{\n\t\t\tname: \"missing handler\",\n\t\t\tjob: &Job{\n\t\t\t\tName:     \"test\",\n\t\t\t\tSchedule: \"0 * * * *\",\n\t\t\t},\n\t\t\twantErr: true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\terr := tt.job.Validate()\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"Validate() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "plugin/scheduler/middleware.go",
    "content": "package scheduler\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t\"github.com/pkg/errors\"\n)\n\n// Middleware wraps a JobHandler to add cross-cutting behavior.\ntype Middleware func(JobHandler) JobHandler\n\n// Chain combines multiple middleware into a single middleware.\n// Middleware are applied in the order they're provided (left to right).\nfunc Chain(middlewares ...Middleware) Middleware {\n\treturn func(handler JobHandler) JobHandler {\n\t\t// Apply middleware in reverse order so first middleware wraps outermost\n\t\tfor i := len(middlewares) - 1; i >= 0; i-- {\n\t\t\thandler = middlewares[i](handler)\n\t\t}\n\t\treturn handler\n\t}\n}\n\n// Recovery recovers from panics in job handlers and converts them to errors.\nfunc Recovery(onPanic func(jobName string, recovered interface{})) Middleware {\n\treturn func(next JobHandler) JobHandler {\n\t\treturn func(ctx context.Context) (err error) {\n\t\t\tdefer func() {\n\t\t\t\tif r := recover(); r != nil {\n\t\t\t\t\tjobName := getJobName(ctx)\n\t\t\t\t\tif onPanic != nil {\n\t\t\t\t\t\tonPanic(jobName, r)\n\t\t\t\t\t}\n\t\t\t\t\terr = errors.Errorf(\"job %q panicked: %v\", jobName, r)\n\t\t\t\t}\n\t\t\t}()\n\t\t\treturn next(ctx)\n\t\t}\n\t}\n}\n\n// Logger is a minimal logging interface.\ntype Logger interface {\n\tInfo(msg string, args ...interface{})\n\tError(msg string, args ...interface{})\n}\n\n// Logging adds execution logging to jobs.\nfunc Logging(logger Logger) Middleware {\n\treturn func(next JobHandler) JobHandler {\n\t\treturn func(ctx context.Context) error {\n\t\t\tjobName := getJobName(ctx)\n\t\t\tstart := time.Now()\n\n\t\t\tlogger.Info(\"Job started\", \"job\", jobName)\n\n\t\t\terr := next(ctx)\n\t\t\tduration := time.Since(start)\n\n\t\t\tif err != nil {\n\t\t\t\tlogger.Error(\"Job failed\", \"job\", jobName, \"duration\", duration, \"error\", err)\n\t\t\t} else {\n\t\t\t\tlogger.Info(\"Job completed\", \"job\", jobName, \"duration\", duration)\n\t\t\t}\n\n\t\t\treturn err\n\t\t}\n\t}\n}\n\n// Timeout wraps a job handler with a timeout.\nfunc Timeout(duration time.Duration) Middleware {\n\treturn func(next JobHandler) JobHandler {\n\t\treturn func(ctx context.Context) error {\n\t\t\tctx, cancel := context.WithTimeout(ctx, duration)\n\t\t\tdefer cancel()\n\n\t\t\tdone := make(chan error, 1)\n\t\t\tgo func() {\n\t\t\t\tdone <- next(ctx)\n\t\t\t}()\n\n\t\t\tselect {\n\t\t\tcase err := <-done:\n\t\t\t\treturn err\n\t\t\tcase <-ctx.Done():\n\t\t\t\treturn errors.Errorf(\"job %q timed out after %v\", getJobName(ctx), duration)\n\t\t\t}\n\t\t}\n\t}\n}\n\n// Context keys for job metadata.\ntype contextKey int\n\nconst (\n\tjobNameKey contextKey = iota\n)\n\n// withJobName adds the job name to the context.\nfunc withJobName(ctx context.Context, name string) context.Context {\n\treturn context.WithValue(ctx, jobNameKey, name)\n}\n\n// getJobName retrieves the job name from the context.\nfunc getJobName(ctx context.Context) string {\n\tif name, ok := ctx.Value(jobNameKey).(string); ok {\n\t\treturn name\n\t}\n\treturn \"unknown\"\n}\n\n// GetJobName retrieves the job name from the context (public API).\n// Returns empty string if not found.\n//\n//nolint:revive // GetJobName is the public API, getJobName is internal\nfunc GetJobName(ctx context.Context) string {\n\treturn getJobName(ctx)\n}\n"
  },
  {
    "path": "plugin/scheduler/middleware_test.go",
    "content": "package scheduler\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"sync/atomic\"\n\t\"testing\"\n)\n\nfunc TestMiddlewareChaining(t *testing.T) {\n\tvar order []string\n\n\tmw1 := func(next JobHandler) JobHandler {\n\t\treturn func(ctx context.Context) error {\n\t\t\torder = append(order, \"before-1\")\n\t\t\terr := next(ctx)\n\t\t\torder = append(order, \"after-1\")\n\t\t\treturn err\n\t\t}\n\t}\n\n\tmw2 := func(next JobHandler) JobHandler {\n\t\treturn func(ctx context.Context) error {\n\t\t\torder = append(order, \"before-2\")\n\t\t\terr := next(ctx)\n\t\t\torder = append(order, \"after-2\")\n\t\t\treturn err\n\t\t}\n\t}\n\n\thandler := func(_ context.Context) error {\n\t\torder = append(order, \"handler\")\n\t\treturn nil\n\t}\n\n\tchain := Chain(mw1, mw2)\n\twrapped := chain(handler)\n\n\tif err := wrapped(context.Background()); err != nil {\n\t\tt.Fatalf(\"wrapped handler failed: %v\", err)\n\t}\n\n\texpected := []string{\"before-1\", \"before-2\", \"handler\", \"after-2\", \"after-1\"}\n\tif len(order) != len(expected) {\n\t\tt.Fatalf(\"expected %d calls, got %d\", len(expected), len(order))\n\t}\n\n\tfor i, want := range expected {\n\t\tif order[i] != want {\n\t\t\tt.Errorf(\"order[%d] = %q, want %q\", i, order[i], want)\n\t\t}\n\t}\n}\n\nfunc TestRecoveryMiddleware(t *testing.T) {\n\tvar panicRecovered atomic.Bool\n\n\tonPanic := func(_ string, _ interface{}) {\n\t\tpanicRecovered.Store(true)\n\t}\n\n\thandler := func(_ context.Context) error {\n\t\tpanic(\"simulated panic\")\n\t}\n\n\trecovery := Recovery(onPanic)\n\twrapped := recovery(handler)\n\n\t// Should not panic, error should be returned\n\terr := wrapped(withJobName(context.Background(), \"test-job\"))\n\tif err == nil {\n\t\tt.Error(\"expected error from recovered panic\")\n\t}\n\n\tif !panicRecovered.Load() {\n\t\tt.Error(\"panic handler was not called\")\n\t}\n}\n\nfunc TestLoggingMiddleware(t *testing.T) {\n\tvar loggedStart, loggedEnd atomic.Bool\n\tvar loggedError atomic.Bool\n\n\tlogger := &testLogger{\n\t\tonInfo: func(msg string, _ ...interface{}) {\n\t\t\tif msg == \"Job started\" {\n\t\t\t\tloggedStart.Store(true)\n\t\t\t} else if msg == \"Job completed\" {\n\t\t\t\tloggedEnd.Store(true)\n\t\t\t}\n\t\t},\n\t\tonError: func(msg string, _ ...interface{}) {\n\t\t\tif msg == \"Job failed\" {\n\t\t\t\tloggedError.Store(true)\n\t\t\t}\n\t\t},\n\t}\n\n\t// Test successful execution\n\thandler := func(_ context.Context) error {\n\t\treturn nil\n\t}\n\n\tlogging := Logging(logger)\n\twrapped := logging(handler)\n\n\tif err := wrapped(withJobName(context.Background(), \"test-job\")); err != nil {\n\t\tt.Fatalf(\"handler failed: %v\", err)\n\t}\n\n\tif !loggedStart.Load() {\n\t\tt.Error(\"start was not logged\")\n\t}\n\tif !loggedEnd.Load() {\n\t\tt.Error(\"end was not logged\")\n\t}\n\n\t// Test error handling\n\thandlerErr := func(_ context.Context) error {\n\t\treturn errors.New(\"job error\")\n\t}\n\n\twrappedErr := logging(handlerErr)\n\t_ = wrappedErr(withJobName(context.Background(), \"test-job-error\"))\n\n\tif !loggedError.Load() {\n\t\tt.Error(\"error was not logged\")\n\t}\n}\n\ntype testLogger struct {\n\tonInfo  func(msg string, args ...interface{})\n\tonError func(msg string, args ...interface{})\n}\n\nfunc (l *testLogger) Info(msg string, args ...interface{}) {\n\tif l.onInfo != nil {\n\t\tl.onInfo(msg, args...)\n\t}\n}\n\nfunc (l *testLogger) Error(msg string, args ...interface{}) {\n\tif l.onError != nil {\n\t\tl.onError(msg, args...)\n\t}\n}\n"
  },
  {
    "path": "plugin/scheduler/parser.go",
    "content": "package scheduler\n\nimport (\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/pkg/errors\"\n)\n\n// Schedule represents a parsed cron expression.\ntype Schedule struct {\n\tseconds  fieldMatcher // 0-59 (optional, for 6-field format)\n\tminutes  fieldMatcher // 0-59\n\thours    fieldMatcher // 0-23\n\tdays     fieldMatcher // 1-31\n\tmonths   fieldMatcher // 1-12\n\tweekdays fieldMatcher // 0-7 (0 and 7 are Sunday)\n\thasSecs  bool\n}\n\n// fieldMatcher determines if a field value matches.\ntype fieldMatcher interface {\n\tmatches(value int) bool\n}\n\n// ParseCronExpression parses a cron expression and returns a Schedule.\n// Supports both 5-field (minute hour day month weekday) and 6-field (second minute hour day month weekday) formats.\nfunc ParseCronExpression(expr string) (*Schedule, error) {\n\tif expr == \"\" {\n\t\treturn nil, errors.New(\"empty cron expression\")\n\t}\n\n\tfields := strings.Fields(expr)\n\tif len(fields) != 5 && len(fields) != 6 {\n\t\treturn nil, errors.Errorf(\"invalid cron expression: expected 5 or 6 fields, got %d\", len(fields))\n\t}\n\n\ts := &Schedule{\n\t\thasSecs: len(fields) == 6,\n\t}\n\n\tvar err error\n\toffset := 0\n\n\t// Parse seconds (if 6-field format)\n\tif s.hasSecs {\n\t\ts.seconds, err = parseField(fields[0], 0, 59)\n\t\tif err != nil {\n\t\t\treturn nil, errors.Wrap(err, \"invalid seconds field\")\n\t\t}\n\t\toffset = 1\n\t} else {\n\t\ts.seconds = &exactMatcher{value: 0} // Default to 0 seconds\n\t}\n\n\t// Parse minutes\n\ts.minutes, err = parseField(fields[offset], 0, 59)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"invalid minutes field\")\n\t}\n\n\t// Parse hours\n\ts.hours, err = parseField(fields[offset+1], 0, 23)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"invalid hours field\")\n\t}\n\n\t// Parse days\n\ts.days, err = parseField(fields[offset+2], 1, 31)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"invalid days field\")\n\t}\n\n\t// Parse months\n\ts.months, err = parseField(fields[offset+3], 1, 12)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"invalid months field\")\n\t}\n\n\t// Parse weekdays (0-7, where both 0 and 7 represent Sunday)\n\ts.weekdays, err = parseField(fields[offset+4], 0, 7)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"invalid weekdays field\")\n\t}\n\n\treturn s, nil\n}\n\n// Next returns the next time the schedule should run after the given time.\nfunc (s *Schedule) Next(from time.Time) time.Time {\n\t// Start from the next second/minute\n\tif s.hasSecs {\n\t\tfrom = from.Add(1 * time.Second).Truncate(time.Second)\n\t} else {\n\t\tfrom = from.Add(1 * time.Minute).Truncate(time.Minute)\n\t}\n\n\t// Cap search at 4 years to prevent infinite loops\n\tmaxTime := from.AddDate(4, 0, 0)\n\n\tfor from.Before(maxTime) {\n\t\tif s.matches(from) {\n\t\t\treturn from\n\t\t}\n\n\t\t// Advance to next potential match\n\t\tif s.hasSecs {\n\t\t\tfrom = from.Add(1 * time.Second)\n\t\t} else {\n\t\t\tfrom = from.Add(1 * time.Minute)\n\t\t}\n\t}\n\n\t// Should never reach here with valid cron expressions\n\treturn time.Time{}\n}\n\n// matches checks if the given time matches the schedule.\nfunc (s *Schedule) matches(t time.Time) bool {\n\treturn s.seconds.matches(t.Second()) &&\n\t\ts.minutes.matches(t.Minute()) &&\n\t\ts.hours.matches(t.Hour()) &&\n\t\ts.months.matches(int(t.Month())) &&\n\t\t(s.days.matches(t.Day()) || s.weekdays.matches(int(t.Weekday())))\n}\n\n// parseField parses a single cron field (supports *, ranges, lists, steps).\nfunc parseField(field string, min, max int) (fieldMatcher, error) {\n\t// Wildcard\n\tif field == \"*\" {\n\t\treturn &wildcardMatcher{}, nil\n\t}\n\n\t// Step values (*/N)\n\tif strings.HasPrefix(field, \"*/\") {\n\t\tstep, err := strconv.Atoi(field[2:])\n\t\tif err != nil || step < 1 || step > max {\n\t\t\treturn nil, errors.Errorf(\"invalid step value: %s\", field)\n\t\t}\n\t\treturn &stepMatcher{step: step, min: min, max: max}, nil\n\t}\n\n\t// List (1,2,3)\n\tif strings.Contains(field, \",\") {\n\t\tparts := strings.Split(field, \",\")\n\t\tvalues := make([]int, 0, len(parts))\n\t\tfor _, p := range parts {\n\t\t\tval, err := strconv.Atoi(strings.TrimSpace(p))\n\t\t\tif err != nil || val < min || val > max {\n\t\t\t\treturn nil, errors.Errorf(\"invalid list value: %s\", p)\n\t\t\t}\n\t\t\tvalues = append(values, val)\n\t\t}\n\t\treturn &listMatcher{values: values}, nil\n\t}\n\n\t// Range (1-5)\n\tif strings.Contains(field, \"-\") {\n\t\tparts := strings.Split(field, \"-\")\n\t\tif len(parts) != 2 {\n\t\t\treturn nil, errors.Errorf(\"invalid range: %s\", field)\n\t\t}\n\t\tstart, err1 := strconv.Atoi(strings.TrimSpace(parts[0]))\n\t\tend, err2 := strconv.Atoi(strings.TrimSpace(parts[1]))\n\t\tif err1 != nil || err2 != nil || start < min || end > max || start > end {\n\t\t\treturn nil, errors.Errorf(\"invalid range: %s\", field)\n\t\t}\n\t\treturn &rangeMatcher{start: start, end: end}, nil\n\t}\n\n\t// Exact value\n\tval, err := strconv.Atoi(field)\n\tif err != nil || val < min || val > max {\n\t\treturn nil, errors.Errorf(\"invalid value: %s (must be between %d and %d)\", field, min, max)\n\t}\n\treturn &exactMatcher{value: val}, nil\n}\n\n// wildcardMatcher matches any value.\ntype wildcardMatcher struct{}\n\nfunc (*wildcardMatcher) matches(_ int) bool {\n\treturn true\n}\n\n// exactMatcher matches a specific value.\ntype exactMatcher struct {\n\tvalue int\n}\n\nfunc (m *exactMatcher) matches(value int) bool {\n\treturn value == m.value\n}\n\n// rangeMatcher matches values in a range.\ntype rangeMatcher struct {\n\tstart, end int\n}\n\nfunc (m *rangeMatcher) matches(value int) bool {\n\treturn value >= m.start && value <= m.end\n}\n\n// listMatcher matches any value in a list.\ntype listMatcher struct {\n\tvalues []int\n}\n\nfunc (m *listMatcher) matches(value int) bool {\n\tfor _, v := range m.values {\n\t\tif v == value {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// stepMatcher matches values at regular intervals.\ntype stepMatcher struct {\n\tstep, min, max int\n}\n\nfunc (m *stepMatcher) matches(value int) bool {\n\tif value < m.min || value > m.max {\n\t\treturn false\n\t}\n\treturn (value-m.min)%m.step == 0\n}\n"
  },
  {
    "path": "plugin/scheduler/parser_test.go",
    "content": "package scheduler\n\nimport (\n\t\"testing\"\n\t\"time\"\n)\n\nfunc TestParseCronExpression(t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\texpr    string\n\t\twantErr bool\n\t}{\n\t\t// Standard 5-field format\n\t\t{\"every minute\", \"* * * * *\", false},\n\t\t{\"hourly\", \"0 * * * *\", false},\n\t\t{\"daily midnight\", \"0 0 * * *\", false},\n\t\t{\"weekly sunday\", \"0 0 * * 0\", false},\n\t\t{\"monthly\", \"0 0 1 * *\", false},\n\t\t{\"specific time\", \"30 14 * * *\", false}, // 2:30 PM daily\n\t\t{\"range\", \"0 9-17 * * *\", false},        // Every hour 9 AM - 5 PM\n\t\t{\"step\", \"*/15 * * * *\", false},         // Every 15 minutes\n\t\t{\"list\", \"0 8,12,18 * * *\", false},      // 8 AM, 12 PM, 6 PM\n\n\t\t// 6-field format with seconds\n\t\t{\"with seconds\", \"0 * * * * *\", false},\n\t\t{\"every 30 seconds\", \"*/30 * * * * *\", false},\n\n\t\t// Invalid expressions\n\t\t{\"empty\", \"\", true},\n\t\t{\"too few fields\", \"* * *\", true},\n\t\t{\"too many fields\", \"* * * * * * *\", true},\n\t\t{\"invalid minute\", \"60 * * * *\", true},\n\t\t{\"invalid hour\", \"0 24 * * *\", true},\n\t\t{\"invalid day\", \"0 0 32 * *\", true},\n\t\t{\"invalid month\", \"0 0 1 13 *\", true},\n\t\t{\"invalid weekday\", \"0 0 * * 8\", true},\n\t\t{\"garbage\", \"not a cron expression\", true},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tschedule, err := ParseCronExpression(tt.expr)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"ParseCronExpression(%q) error = %v, wantErr %v\", tt.expr, err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !tt.wantErr && schedule == nil {\n\t\t\t\tt.Errorf(\"ParseCronExpression(%q) returned nil schedule without error\", tt.expr)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestScheduleNext(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\texpr     string\n\t\tfrom     time.Time\n\t\texpected time.Time\n\t}{\n\t\t{\n\t\t\tname:     \"every minute from start of hour\",\n\t\t\texpr:     \"* * * * *\",\n\t\t\tfrom:     time.Date(2025, 1, 1, 10, 0, 0, 0, time.UTC),\n\t\t\texpected: time.Date(2025, 1, 1, 10, 1, 0, 0, time.UTC),\n\t\t},\n\t\t{\n\t\t\tname:     \"hourly at minute 30\",\n\t\t\texpr:     \"30 * * * *\",\n\t\t\tfrom:     time.Date(2025, 1, 1, 10, 0, 0, 0, time.UTC),\n\t\t\texpected: time.Date(2025, 1, 1, 10, 30, 0, 0, time.UTC),\n\t\t},\n\t\t{\n\t\t\tname:     \"hourly at minute 30 (already past)\",\n\t\t\texpr:     \"30 * * * *\",\n\t\t\tfrom:     time.Date(2025, 1, 1, 10, 45, 0, 0, time.UTC),\n\t\t\texpected: time.Date(2025, 1, 1, 11, 30, 0, 0, time.UTC),\n\t\t},\n\t\t{\n\t\t\tname:     \"daily at 2 AM\",\n\t\t\texpr:     \"0 2 * * *\",\n\t\t\tfrom:     time.Date(2025, 1, 1, 10, 0, 0, 0, time.UTC),\n\t\t\texpected: time.Date(2025, 1, 2, 2, 0, 0, 0, time.UTC),\n\t\t},\n\t\t{\n\t\t\tname:     \"every 15 minutes\",\n\t\t\texpr:     \"*/15 * * * *\",\n\t\t\tfrom:     time.Date(2025, 1, 1, 10, 7, 0, 0, time.UTC),\n\t\t\texpected: time.Date(2025, 1, 1, 10, 15, 0, 0, time.UTC),\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tschedule, err := ParseCronExpression(tt.expr)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"failed to parse expression: %v\", err)\n\t\t\t}\n\n\t\t\tnext := schedule.Next(tt.from)\n\t\t\tif !next.Equal(tt.expected) {\n\t\t\t\tt.Errorf(\"Next(%v) = %v, expected %v\", tt.from, next, tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestScheduleNextWithTimezone(t *testing.T) {\n\tnyc, _ := time.LoadLocation(\"America/New_York\")\n\n\t// Schedule for 9 AM in New York\n\tschedule, err := ParseCronExpression(\"0 9 * * *\")\n\tif err != nil {\n\t\tt.Fatalf(\"failed to parse expression: %v\", err)\n\t}\n\n\t// Current time: 8 AM in New York\n\tfrom := time.Date(2025, 1, 1, 8, 0, 0, 0, nyc)\n\tnext := schedule.Next(from)\n\n\t// Should be 9 AM same day in New York\n\texpected := time.Date(2025, 1, 1, 9, 0, 0, 0, nyc)\n\tif !next.Equal(expected) {\n\t\tt.Errorf(\"Next(%v) = %v, expected %v\", from, next, expected)\n\t}\n}\n"
  },
  {
    "path": "plugin/scheduler/scheduler.go",
    "content": "package scheduler\n\nimport (\n\t\"context\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/pkg/errors\"\n)\n\n// Scheduler manages scheduled jobs.\ntype Scheduler struct {\n\tjobs       map[string]*registeredJob\n\tjobsMu     sync.RWMutex\n\ttimezone   *time.Location\n\tmiddleware Middleware\n\trunning    bool\n\trunningMu  sync.RWMutex\n\tstopCh     chan struct{}\n\twg         sync.WaitGroup\n}\n\n// registeredJob wraps a Job with runtime state.\ntype registeredJob struct {\n\tjob      *Job\n\tcancelFn context.CancelFunc\n}\n\n// Option configures a Scheduler.\ntype Option func(*Scheduler)\n\n// WithTimezone sets the default timezone for all jobs.\nfunc WithTimezone(tz string) Option {\n\treturn func(s *Scheduler) {\n\t\tloc, err := time.LoadLocation(tz)\n\t\tif err != nil {\n\t\t\t// Default to UTC on invalid timezone\n\t\t\tloc = time.UTC\n\t\t}\n\t\ts.timezone = loc\n\t}\n}\n\n// WithMiddleware sets middleware to wrap all job handlers.\nfunc WithMiddleware(mw ...Middleware) Option {\n\treturn func(s *Scheduler) {\n\t\tif len(mw) > 0 {\n\t\t\ts.middleware = Chain(mw...)\n\t\t}\n\t}\n}\n\n// New creates a new Scheduler with optional configuration.\nfunc New(opts ...Option) *Scheduler {\n\ts := &Scheduler{\n\t\tjobs:     make(map[string]*registeredJob),\n\t\ttimezone: time.UTC,\n\t\tstopCh:   make(chan struct{}),\n\t}\n\n\tfor _, opt := range opts {\n\t\topt(s)\n\t}\n\n\treturn s\n}\n\n// Register adds a job to the scheduler.\n// Jobs must be registered before calling Start().\nfunc (s *Scheduler) Register(job *Job) error {\n\tif job == nil {\n\t\treturn errors.New(\"job cannot be nil\")\n\t}\n\n\tif err := job.Validate(); err != nil {\n\t\treturn errors.Wrap(err, \"invalid job\")\n\t}\n\n\ts.jobsMu.Lock()\n\tdefer s.jobsMu.Unlock()\n\n\tif _, exists := s.jobs[job.Name]; exists {\n\t\treturn errors.Errorf(\"job with name %q already registered\", job.Name)\n\t}\n\n\ts.jobs[job.Name] = &registeredJob{job: job}\n\treturn nil\n}\n\n// Start begins executing scheduled jobs.\nfunc (s *Scheduler) Start() error {\n\ts.runningMu.Lock()\n\tdefer s.runningMu.Unlock()\n\n\tif s.running {\n\t\treturn errors.New(\"scheduler already running\")\n\t}\n\n\ts.jobsMu.RLock()\n\tdefer s.jobsMu.RUnlock()\n\n\t// Parse and schedule all jobs\n\tfor _, rj := range s.jobs {\n\t\tschedule, err := ParseCronExpression(rj.job.Schedule)\n\t\tif err != nil {\n\t\t\treturn errors.Wrapf(err, \"failed to parse schedule for job %q\", rj.job.Name)\n\t\t}\n\n\t\tctx, cancel := context.WithCancel(context.Background())\n\t\trj.cancelFn = cancel\n\n\t\ts.wg.Add(1)\n\t\tgo s.runJobWithSchedule(ctx, rj, schedule)\n\t}\n\n\ts.running = true\n\treturn nil\n}\n\n// runJobWithSchedule executes a job according to its cron schedule.\nfunc (s *Scheduler) runJobWithSchedule(ctx context.Context, rj *registeredJob, schedule *Schedule) {\n\tdefer s.wg.Done()\n\n\t// Apply middleware to handler\n\thandler := rj.job.Handler\n\tif s.middleware != nil {\n\t\thandler = s.middleware(handler)\n\t}\n\n\tfor {\n\t\t// Calculate next run time\n\t\tnow := time.Now()\n\t\tif rj.job.Timezone != \"\" {\n\t\t\tloc, err := time.LoadLocation(rj.job.Timezone)\n\t\t\tif err == nil {\n\t\t\t\tnow = now.In(loc)\n\t\t\t}\n\t\t} else if s.timezone != nil {\n\t\t\tnow = now.In(s.timezone)\n\t\t}\n\n\t\tnext := schedule.Next(now)\n\t\tduration := time.Until(next)\n\n\t\ttimer := time.NewTimer(duration)\n\n\t\tselect {\n\t\tcase <-timer.C:\n\t\t\t// Add job name to context and execute\n\t\t\tjobCtx := withJobName(ctx, rj.job.Name)\n\t\t\tif err := handler(jobCtx); err != nil {\n\t\t\t\t// Error already handled by middleware (if any)\n\t\t\t\t_ = err\n\t\t\t}\n\t\tcase <-ctx.Done():\n\t\t\t// Stop the timer to prevent it from firing. The timer will be garbage collected.\n\t\t\ttimer.Stop()\n\t\t\treturn\n\t\tcase <-s.stopCh:\n\t\t\t// Stop the timer to prevent it from firing. The timer will be garbage collected.\n\t\t\ttimer.Stop()\n\t\t\treturn\n\t\t}\n\t}\n}\n\n// Stop gracefully shuts down the scheduler.\n// It waits for all running jobs to complete or until the context is canceled.\nfunc (s *Scheduler) Stop(ctx context.Context) error {\n\ts.runningMu.Lock()\n\tif !s.running {\n\t\ts.runningMu.Unlock()\n\t\treturn errors.New(\"scheduler not running\")\n\t}\n\ts.running = false\n\ts.runningMu.Unlock()\n\n\t// Cancel all job contexts\n\ts.jobsMu.RLock()\n\tfor _, rj := range s.jobs {\n\t\tif rj.cancelFn != nil {\n\t\t\trj.cancelFn()\n\t\t}\n\t}\n\ts.jobsMu.RUnlock()\n\n\t// Signal stop and wait for jobs to finish\n\tclose(s.stopCh)\n\n\tdone := make(chan struct{})\n\tgo func() {\n\t\ts.wg.Wait()\n\t\tclose(done)\n\t}()\n\n\tselect {\n\tcase <-done:\n\t\treturn nil\n\tcase <-ctx.Done():\n\t\treturn ctx.Err()\n\t}\n}\n"
  },
  {
    "path": "plugin/scheduler/scheduler_test.go",
    "content": "package scheduler\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strings\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"time\"\n)\n\nfunc TestSchedulerCreation(t *testing.T) {\n\ts := New()\n\tif s == nil {\n\t\tt.Fatal(\"New() returned nil\")\n\t}\n}\n\nfunc TestSchedulerWithTimezone(t *testing.T) {\n\ts := New(WithTimezone(\"America/New_York\"))\n\tif s == nil {\n\t\tt.Fatal(\"New() with timezone returned nil\")\n\t}\n}\n\nfunc TestJobRegistration(t *testing.T) {\n\ts := New()\n\n\tjob := &Job{\n\t\tName:     \"test-registration\",\n\t\tSchedule: \"0 * * * *\",\n\t\tHandler:  func(_ context.Context) error { return nil },\n\t}\n\n\tif err := s.Register(job); err != nil {\n\t\tt.Fatalf(\"failed to register valid job: %v\", err)\n\t}\n\n\t// Registering duplicate name should fail\n\tif err := s.Register(job); err == nil {\n\t\tt.Error(\"expected error when registering duplicate job name\")\n\t}\n}\n\nfunc TestSchedulerStartStop(t *testing.T) {\n\ts := New()\n\n\tvar runCount atomic.Int32\n\tjob := &Job{\n\t\tName:     \"test-start-stop\",\n\t\tSchedule: \"* * * * * *\", // Every second (6-field format)\n\t\tHandler: func(_ context.Context) error {\n\t\t\trunCount.Add(1)\n\t\t\treturn nil\n\t\t},\n\t}\n\n\tif err := s.Register(job); err != nil {\n\t\tt.Fatalf(\"failed to register job: %v\", err)\n\t}\n\n\t// Start scheduler\n\tif err := s.Start(); err != nil {\n\t\tt.Fatalf(\"failed to start scheduler: %v\", err)\n\t}\n\n\t// Let it run for 2.5 seconds\n\ttime.Sleep(2500 * time.Millisecond)\n\n\t// Stop scheduler\n\tctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)\n\tdefer cancel()\n\n\tif err := s.Stop(ctx); err != nil {\n\t\tt.Fatalf(\"failed to stop scheduler: %v\", err)\n\t}\n\n\tcount := runCount.Load()\n\t// Should have run at least twice (at 0s and 1s, maybe 2s)\n\tif count < 2 {\n\t\tt.Errorf(\"expected job to run at least 2 times, ran %d times\", count)\n\t}\n\n\t// Verify it stopped (count shouldn't increase)\n\tfinalCount := count\n\ttime.Sleep(1500 * time.Millisecond)\n\tif runCount.Load() != finalCount {\n\t\tt.Error(\"scheduler did not stop - job continued running\")\n\t}\n}\n\nfunc TestSchedulerWithMiddleware(t *testing.T) {\n\tvar executionLog []string\n\tvar logMu sync.Mutex\n\n\tlogger := &testLogger{\n\t\tonInfo: func(msg string, _ ...interface{}) {\n\t\t\tlogMu.Lock()\n\t\t\texecutionLog = append(executionLog, fmt.Sprintf(\"INFO: %s\", msg))\n\t\t\tlogMu.Unlock()\n\t\t},\n\t\tonError: func(msg string, _ ...interface{}) {\n\t\t\tlogMu.Lock()\n\t\t\texecutionLog = append(executionLog, fmt.Sprintf(\"ERROR: %s\", msg))\n\t\t\tlogMu.Unlock()\n\t\t},\n\t}\n\n\ts := New(WithMiddleware(\n\t\tRecovery(func(jobName string, r interface{}) {\n\t\t\tlogMu.Lock()\n\t\t\texecutionLog = append(executionLog, fmt.Sprintf(\"PANIC: %s - %v\", jobName, r))\n\t\t\tlogMu.Unlock()\n\t\t}),\n\t\tLogging(logger),\n\t))\n\n\tjob := &Job{\n\t\tName:     \"test-middleware\",\n\t\tSchedule: \"* * * * * *\", // Every second\n\t\tHandler: func(_ context.Context) error {\n\t\t\treturn nil\n\t\t},\n\t}\n\n\tif err := s.Register(job); err != nil {\n\t\tt.Fatalf(\"failed to register job: %v\", err)\n\t}\n\n\tif err := s.Start(); err != nil {\n\t\tt.Fatalf(\"failed to start: %v\", err)\n\t}\n\n\ttime.Sleep(1500 * time.Millisecond)\n\n\tctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)\n\tdefer cancel()\n\n\tif err := s.Stop(ctx); err != nil {\n\t\tt.Fatalf(\"failed to stop: %v\", err)\n\t}\n\n\tlogMu.Lock()\n\tdefer logMu.Unlock()\n\n\t// Should have at least one start and one completion log\n\thasStart := false\n\thasCompletion := false\n\tfor _, log := range executionLog {\n\t\tif strings.Contains(log, \"Job started\") {\n\t\t\thasStart = true\n\t\t}\n\t\tif strings.Contains(log, \"Job completed\") {\n\t\t\thasCompletion = true\n\t\t}\n\t}\n\n\tif !hasStart {\n\t\tt.Error(\"expected job start log\")\n\t}\n\tif !hasCompletion {\n\t\tt.Error(\"expected job completion log\")\n\t}\n}\n"
  },
  {
    "path": "plugin/storage/s3/s3.go",
    "content": "package s3\n\nimport (\n\t\"context\"\n\t\"io\"\n\t\"time\"\n\n\t\"github.com/aws/aws-sdk-go-v2/aws\"\n\t\"github.com/aws/aws-sdk-go-v2/config\"\n\t\"github.com/aws/aws-sdk-go-v2/credentials\"\n\t\"github.com/aws/aws-sdk-go-v2/service/s3\"\n\t\"github.com/pkg/errors\"\n\n\tstorepb \"github.com/usememos/memos/proto/gen/store\"\n)\n\ntype Client struct {\n\tClient *s3.Client\n\tBucket *string\n}\n\nfunc NewClient(ctx context.Context, s3Config *storepb.StorageS3Config) (*Client, error) {\n\tcfg, err := config.LoadDefaultConfig(ctx,\n\t\tconfig.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(s3Config.AccessKeyId, s3Config.AccessKeySecret, \"\")),\n\t\tconfig.WithRegion(s3Config.Region),\n\t)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"failed to load s3 config\")\n\t}\n\n\tclient := s3.NewFromConfig(cfg, func(o *s3.Options) {\n\t\to.BaseEndpoint = aws.String(s3Config.Endpoint)\n\t\to.UsePathStyle = s3Config.UsePathStyle\n\t\to.RequestChecksumCalculation = aws.RequestChecksumCalculationWhenRequired\n\t\to.ResponseChecksumValidation = aws.ResponseChecksumValidationWhenRequired\n\t})\n\treturn &Client{\n\t\tClient: client,\n\t\tBucket: aws.String(s3Config.Bucket),\n\t}, nil\n}\n\n// UploadObject uploads an object to S3.\nfunc (c *Client) UploadObject(ctx context.Context, key string, fileType string, content io.Reader) (string, error) {\n\tputInput := s3.PutObjectInput{\n\t\tBucket:      c.Bucket,\n\t\tKey:         aws.String(key),\n\t\tContentType: aws.String(fileType),\n\t\tBody:        content,\n\t}\n\tif _, err := c.Client.PutObject(ctx, &putInput); err != nil {\n\t\treturn \"\", err\n\t}\n\treturn key, nil\n}\n\n// PresignGetObject presigns an object in S3.\nfunc (c *Client) PresignGetObject(ctx context.Context, key string) (string, error) {\n\tpresignClient := s3.NewPresignClient(c.Client)\n\tpresignResult, err := presignClient.PresignGetObject(ctx, &s3.GetObjectInput{\n\t\tBucket: aws.String(*c.Bucket),\n\t\tKey:    aws.String(key),\n\t}, func(opts *s3.PresignOptions) {\n\t\t// Set the expiration time of the presigned URL to 5 days.\n\t\t// Reference: https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-query-string-auth.html\n\t\topts.Expires = time.Duration(5 * 24 * time.Hour)\n\t})\n\tif err != nil {\n\t\treturn \"\", errors.Wrap(err, \"failed to presign get object\")\n\t}\n\treturn presignResult.URL, nil\n}\n\n// GetObject retrieves an object from S3.\nfunc (c *Client) GetObject(ctx context.Context, key string) ([]byte, error) {\n\toutput, err := c.Client.GetObject(ctx, &s3.GetObjectInput{\n\t\tBucket: c.Bucket,\n\t\tKey:    aws.String(key),\n\t})\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"failed to download object\")\n\t}\n\tdefer output.Body.Close()\n\tdata, err := io.ReadAll(output.Body)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"failed to read object body\")\n\t}\n\treturn data, nil\n}\n\n// GetObjectStream retrieves an object from S3 as a stream.\nfunc (c *Client) GetObjectStream(ctx context.Context, key string) (io.ReadCloser, error) {\n\toutput, err := c.Client.GetObject(ctx, &s3.GetObjectInput{\n\t\tBucket: c.Bucket,\n\t\tKey:    aws.String(key),\n\t})\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"failed to get object\")\n\t}\n\treturn output.Body, nil\n}\n\n// DeleteObject deletes an object in S3.\nfunc (c *Client) DeleteObject(ctx context.Context, key string) error {\n\t_, err := c.Client.DeleteObject(ctx, &s3.DeleteObjectInput{\n\t\tBucket: c.Bucket,\n\t\tKey:    aws.String(key),\n\t})\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"failed to delete object\")\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "plugin/webhook/validate.go",
    "content": "package webhook\n\nimport (\n\t\"net\"\n\t\"net/url\"\n\n\t\"google.golang.org/grpc/codes\"\n\t\"google.golang.org/grpc/status\"\n)\n\n// reservedCIDRs lists IP ranges that must never be targeted by outbound webhook requests.\n// Covers loopback, RFC-1918 private, link-local (including cloud IMDS at 169.254.169.254),\n// and their IPv6 equivalents.\nvar reservedCIDRs = []string{\n\t\"127.0.0.0/8\",    // IPv4 loopback\n\t\"10.0.0.0/8\",     // RFC-1918 class A\n\t\"172.16.0.0/12\",  // RFC-1918 class B\n\t\"192.168.0.0/16\", // RFC-1918 class C\n\t\"169.254.0.0/16\", // Link-local / cloud IMDS\n\t\"::1/128\",        // IPv6 loopback\n\t\"fc00::/7\",       // IPv6 unique local\n\t\"fe80::/10\",      // IPv6 link-local\n}\n\n// reservedNetworks is the parsed form of reservedCIDRs, built once at startup.\nvar reservedNetworks []*net.IPNet\n\nfunc init() {\n\tfor _, cidr := range reservedCIDRs {\n\t\t_, network, err := net.ParseCIDR(cidr)\n\t\tif err != nil {\n\t\t\tpanic(\"webhook: invalid reserved CIDR \" + cidr + \": \" + err.Error())\n\t\t}\n\t\treservedNetworks = append(reservedNetworks, network)\n\t}\n}\n\n// AllowPrivateIPs controls whether webhook URLs may resolve to reserved/private\n// IP addresses. When true, the SSRF protection is disabled. This is useful for\n// self-hosted deployments where webhooks target services on the local network.\nvar AllowPrivateIPs bool\n\n// isReservedIP reports whether ip falls within any reserved/private range.\nfunc isReservedIP(ip net.IP) bool {\n\tif AllowPrivateIPs {\n\t\treturn false\n\t}\n\tfor _, network := range reservedNetworks {\n\t\tif network.Contains(ip) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// ValidateURL checks that rawURL:\n//  1. Parses as a valid absolute URL.\n//  2. Uses the http or https scheme.\n//  3. Does not resolve to a reserved/private IP address.\n//\n// It returns a gRPC InvalidArgument status error so callers can return it directly.\nfunc ValidateURL(rawURL string) error {\n\tu, err := url.ParseRequestURI(rawURL)\n\tif err != nil {\n\t\treturn status.Errorf(codes.InvalidArgument, \"invalid webhook URL: %v\", err)\n\t}\n\tif u.Scheme != \"http\" && u.Scheme != \"https\" {\n\t\treturn status.Errorf(codes.InvalidArgument, \"webhook URL must use http or https scheme, got %q\", u.Scheme)\n\t}\n\n\tips, err := net.LookupHost(u.Hostname())\n\tif err != nil {\n\t\treturn status.Errorf(codes.InvalidArgument, \"webhook URL hostname could not be resolved: %v\", err)\n\t}\n\n\tfor _, ipStr := range ips {\n\t\tip := net.ParseIP(ipStr)\n\t\tif ip != nil && isReservedIP(ip) {\n\t\t\treturn status.Errorf(codes.InvalidArgument, \"webhook URL must not resolve to a reserved or private IP address\")\n\t\t}\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "plugin/webhook/webhook.go",
    "content": "package webhook\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"io\"\n\t\"log/slog\"\n\t\"net\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/pkg/errors\"\n\n\tv1pb \"github.com/usememos/memos/proto/gen/api/v1\"\n)\n\nvar (\n\t// timeout is the timeout for webhook request. Default to 30 seconds.\n\ttimeout = 30 * time.Second\n\n\t// safeClient is the shared HTTP client used for all webhook dispatches.\n\t// Its Transport guards against SSRF by blocking connections to reserved/private\n\t// IP addresses at dial time, which also defeats DNS rebinding attacks.\n\tsafeClient = &http.Client{\n\t\tTimeout: timeout,\n\t\tTransport: &http.Transport{\n\t\t\tDialContext: safeDialContext,\n\t\t},\n\t}\n)\n\n// safeDialContext is a net.Dialer.DialContext replacement that resolves the target\n// hostname and rejects any address that falls within a reserved/private IP range.\nfunc safeDialContext(ctx context.Context, network, addr string) (net.Conn, error) {\n\thost, port, err := net.SplitHostPort(addr)\n\tif err != nil {\n\t\treturn nil, errors.Errorf(\"webhook: invalid address %q\", addr)\n\t}\n\n\tips, err := net.DefaultResolver.LookupHost(ctx, host)\n\tif err != nil {\n\t\treturn nil, errors.Wrapf(err, \"webhook: failed to resolve host %q\", host)\n\t}\n\n\tfor _, ipStr := range ips {\n\t\tif ip := net.ParseIP(ipStr); ip != nil && isReservedIP(ip) {\n\t\t\treturn nil, errors.Errorf(\"webhook: connection to reserved/private IP address is not allowed\")\n\t\t}\n\t}\n\n\treturn (&net.Dialer{}).DialContext(ctx, network, net.JoinHostPort(host, port))\n}\n\ntype WebhookRequestPayload struct {\n\t// The target URL for the webhook request.\n\tURL string `json:\"url\"`\n\t// The type of activity that triggered this webhook.\n\tActivityType string `json:\"activityType\"`\n\t// The resource name of the creator. Format: users/{user}\n\tCreator string `json:\"creator\"`\n\t// The memo that triggered this webhook (if applicable).\n\tMemo *v1pb.Memo `json:\"memo\"`\n}\n\n// Post posts the message to webhook endpoint.\nfunc Post(requestPayload *WebhookRequestPayload) error {\n\tbody, err := json.Marshal(requestPayload)\n\tif err != nil {\n\t\treturn errors.Wrapf(err, \"failed to marshal webhook request to %s\", requestPayload.URL)\n\t}\n\n\treq, err := http.NewRequest(\"POST\", requestPayload.URL, bytes.NewBuffer(body))\n\tif err != nil {\n\t\treturn errors.Wrapf(err, \"failed to construct webhook request to %s\", requestPayload.URL)\n\t}\n\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\tresp, err := safeClient.Do(req)\n\tif err != nil {\n\t\treturn errors.Wrapf(err, \"failed to post webhook to %s\", requestPayload.URL)\n\t}\n\tdefer resp.Body.Close()\n\n\tb, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errors.Wrapf(err, \"failed to read webhook response from %s\", requestPayload.URL)\n\t}\n\n\tif resp.StatusCode < 200 || resp.StatusCode > 299 {\n\t\treturn errors.Errorf(\"failed to post webhook %s, status code: %d\", requestPayload.URL, resp.StatusCode)\n\t}\n\n\tresponse := &struct {\n\t\tCode    int    `json:\"code\"`\n\t\tMessage string `json:\"message\"`\n\t}{}\n\tif err := json.Unmarshal(b, response); err != nil {\n\t\treturn errors.Wrapf(err, \"failed to unmarshal webhook response from %s\", requestPayload.URL)\n\t}\n\n\tif response.Code != 0 {\n\t\treturn errors.Errorf(\"receive error code sent by webhook server, code %d, msg: %s\", response.Code, response.Message)\n\t}\n\n\treturn nil\n}\n\n// PostAsync posts the message to webhook endpoint asynchronously.\n// It spawns a new goroutine to handle the request and does not wait for the response.\nfunc PostAsync(requestPayload *WebhookRequestPayload) {\n\tgo func() {\n\t\tif err := Post(requestPayload); err != nil {\n\t\t\tslog.Warn(\"Failed to dispatch webhook asynchronously\",\n\t\t\t\tslog.String(\"url\", requestPayload.URL),\n\t\t\t\tslog.String(\"activityType\", requestPayload.ActivityType),\n\t\t\t\tslog.Any(\"err\", err))\n\t\t}\n\t}()\n}\n"
  },
  {
    "path": "plugin/webhook/webhook_test.go",
    "content": "package webhook\n"
  },
  {
    "path": "proto/README.md",
    "content": "# Guide\n\n## Prerequisites\n\n- [buf](https://docs.buf.build/installation)\n\n## Generate\n\n```sh\nbuf generate\n```\n\n## Format\n\n```sh\nbuf format -w\n```\n"
  },
  {
    "path": "proto/api/v1/README.md",
    "content": "# Memos API Design\n\nThis API design should follow the guidelines and best practices outlined in the [Google API Improvement Proposals (AIPs)](https://google.aip.dev/).\n"
  },
  {
    "path": "proto/api/v1/attachment_service.proto",
    "content": "syntax = \"proto3\";\n\npackage memos.api.v1;\n\nimport \"google/api/annotations.proto\";\nimport \"google/api/client.proto\";\nimport \"google/api/field_behavior.proto\";\nimport \"google/api/resource.proto\";\nimport \"google/protobuf/empty.proto\";\nimport \"google/protobuf/field_mask.proto\";\nimport \"google/protobuf/timestamp.proto\";\n\noption go_package = \"gen/api/v1\";\n\nservice AttachmentService {\n  // CreateAttachment creates a new attachment.\n  rpc CreateAttachment(CreateAttachmentRequest) returns (Attachment) {\n    option (google.api.http) = {\n      post: \"/api/v1/attachments\"\n      body: \"attachment\"\n    };\n    option (google.api.method_signature) = \"attachment\";\n  }\n  // ListAttachments lists all attachments.\n  rpc ListAttachments(ListAttachmentsRequest) returns (ListAttachmentsResponse) {\n    option (google.api.http) = {get: \"/api/v1/attachments\"};\n  }\n  // GetAttachment returns an attachment by name.\n  rpc GetAttachment(GetAttachmentRequest) returns (Attachment) {\n    option (google.api.http) = {get: \"/api/v1/{name=attachments/*}\"};\n    option (google.api.method_signature) = \"name\";\n  }\n  // UpdateAttachment updates an attachment.\n  rpc UpdateAttachment(UpdateAttachmentRequest) returns (Attachment) {\n    option (google.api.http) = {\n      patch: \"/api/v1/{attachment.name=attachments/*}\"\n      body: \"attachment\"\n    };\n    option (google.api.method_signature) = \"attachment,update_mask\";\n  }\n  // DeleteAttachment deletes an attachment by name.\n  rpc DeleteAttachment(DeleteAttachmentRequest) returns (google.protobuf.Empty) {\n    option (google.api.http) = {delete: \"/api/v1/{name=attachments/*}\"};\n    option (google.api.method_signature) = \"name\";\n  }\n}\n\nmessage Attachment {\n  option (google.api.resource) = {\n    type: \"memos.api.v1/Attachment\"\n    pattern: \"attachments/{attachment}\"\n    singular: \"attachment\"\n    plural: \"attachments\"\n  };\n\n  // The name of the attachment.\n  // Format: attachments/{attachment}\n  string name = 1 [(google.api.field_behavior) = IDENTIFIER];\n\n  // Output only. The creation timestamp.\n  google.protobuf.Timestamp create_time = 2 [(google.api.field_behavior) = OUTPUT_ONLY];\n\n  // The filename of the attachment.\n  string filename = 3 [(google.api.field_behavior) = REQUIRED];\n\n  // Input only. The content of the attachment.\n  bytes content = 4 [(google.api.field_behavior) = INPUT_ONLY];\n\n  // Optional. The external link of the attachment.\n  string external_link = 5 [(google.api.field_behavior) = OPTIONAL];\n\n  // The MIME type of the attachment.\n  string type = 6 [(google.api.field_behavior) = REQUIRED];\n\n  // Output only. The size of the attachment in bytes.\n  int64 size = 7 [(google.api.field_behavior) = OUTPUT_ONLY];\n\n  // Optional. The related memo. Refer to `Memo.name`.\n  // Format: memos/{memo}\n  optional string memo = 8 [(google.api.field_behavior) = OPTIONAL];\n}\n\nmessage CreateAttachmentRequest {\n  // Required. The attachment to create.\n  Attachment attachment = 1 [(google.api.field_behavior) = REQUIRED];\n\n  // Optional. The attachment ID to use for this attachment.\n  // If empty, a unique ID will be generated.\n  string attachment_id = 2 [(google.api.field_behavior) = OPTIONAL];\n}\n\nmessage ListAttachmentsRequest {\n  // Optional. The maximum number of attachments to return.\n  // The service may return fewer than this value.\n  // If unspecified, at most 50 attachments will be returned.\n  // The maximum value is 1000; values above 1000 will be coerced to 1000.\n  int32 page_size = 1 [(google.api.field_behavior) = OPTIONAL];\n\n  // Optional. A page token, received from a previous `ListAttachments` call.\n  // Provide this to retrieve the subsequent page.\n  string page_token = 2 [(google.api.field_behavior) = OPTIONAL];\n\n  // Optional. Filter to apply to the list results.\n  // Example: \"mime_type==\\\"image/png\\\"\" or \"filename.contains(\\\"test\\\")\"\n  // Supported operators: =, !=, <, <=, >, >=, : (contains), in\n  // Supported fields: filename, mime_type, create_time, memo\n  string filter = 3 [(google.api.field_behavior) = OPTIONAL];\n\n  // Optional. The order to sort results by.\n  // Example: \"create_time desc\" or \"filename asc\"\n  string order_by = 4 [(google.api.field_behavior) = OPTIONAL];\n}\n\nmessage ListAttachmentsResponse {\n  // The list of attachments.\n  repeated Attachment attachments = 1;\n\n  // A token that can be sent as `page_token` to retrieve the next page.\n  // If this field is omitted, there are no subsequent pages.\n  string next_page_token = 2;\n\n  // The total count of attachments (may be approximate).\n  int32 total_size = 3;\n}\n\nmessage GetAttachmentRequest {\n  // Required. The attachment name of the attachment to retrieve.\n  // Format: attachments/{attachment}\n  string name = 1 [\n    (google.api.field_behavior) = REQUIRED,\n    (google.api.resource_reference) = {type: \"memos.api.v1/Attachment\"}\n  ];\n}\n\nmessage UpdateAttachmentRequest {\n  // Required. The attachment which replaces the attachment on the server.\n  Attachment attachment = 1 [(google.api.field_behavior) = REQUIRED];\n\n  // Required. The list of fields to update.\n  google.protobuf.FieldMask update_mask = 2 [(google.api.field_behavior) = REQUIRED];\n}\n\nmessage DeleteAttachmentRequest {\n  // Required. The attachment name of the attachment to delete.\n  // Format: attachments/{attachment}\n  string name = 1 [\n    (google.api.field_behavior) = REQUIRED,\n    (google.api.resource_reference) = {type: \"memos.api.v1/Attachment\"}\n  ];\n}\n"
  },
  {
    "path": "proto/api/v1/auth_service.proto",
    "content": "syntax = \"proto3\";\n\npackage memos.api.v1;\n\nimport \"api/v1/user_service.proto\";\nimport \"google/api/annotations.proto\";\nimport \"google/api/field_behavior.proto\";\nimport \"google/protobuf/empty.proto\";\nimport \"google/protobuf/timestamp.proto\";\n\noption go_package = \"gen/api/v1\";\n\nservice AuthService {\n  // GetCurrentUser returns the authenticated user's information.\n  // Validates the access token and returns user details.\n  // Similar to OIDC's /userinfo endpoint.\n  rpc GetCurrentUser(GetCurrentUserRequest) returns (GetCurrentUserResponse) {\n    option (google.api.http) = {get: \"/api/v1/auth/me\"};\n  }\n\n  // SignIn authenticates a user with credentials and returns tokens.\n  // On success, returns an access token and sets a refresh token cookie.\n  // Supports password-based and SSO authentication methods.\n  rpc SignIn(SignInRequest) returns (SignInResponse) {\n    option (google.api.http) = {\n      post: \"/api/v1/auth/signin\"\n      body: \"*\"\n    };\n  }\n\n  // SignOut terminates the user's authentication.\n  // Revokes the refresh token and clears the authentication cookie.\n  rpc SignOut(SignOutRequest) returns (google.protobuf.Empty) {\n    option (google.api.http) = {post: \"/api/v1/auth/signout\"};\n  }\n\n  // RefreshToken exchanges a valid refresh token for a new access token.\n  // The refresh token is read from the HttpOnly cookie.\n  // Returns a new short-lived access token.\n  rpc RefreshToken(RefreshTokenRequest) returns (RefreshTokenResponse) {\n    option (google.api.http) = {\n      post: \"/api/v1/auth/refresh\"\n      body: \"*\"\n    };\n  }\n}\n\nmessage GetCurrentUserRequest {}\n\nmessage GetCurrentUserResponse {\n  // The authenticated user's information.\n  User user = 1;\n}\n\nmessage SignInRequest {\n  // Nested message for password-based authentication credentials.\n  message PasswordCredentials {\n    // The username to sign in with.\n    string username = 1 [(google.api.field_behavior) = REQUIRED];\n\n    // The password to sign in with.\n    string password = 2 [(google.api.field_behavior) = REQUIRED];\n  }\n\n  // Nested message for SSO authentication credentials.\n  message SSOCredentials {\n    // The resource name of the SSO provider.\n    // Format: identity-providers/{uid}\n    string idp_name = 1 [(google.api.field_behavior) = REQUIRED];\n\n    // The authorization code from the SSO provider.\n    string code = 2 [(google.api.field_behavior) = REQUIRED];\n\n    // The redirect URI used in the SSO flow.\n    string redirect_uri = 3 [(google.api.field_behavior) = REQUIRED];\n\n    // The PKCE code verifier for enhanced security (RFC 7636).\n    // Optional - enables PKCE flow protection against authorization code interception.\n    string code_verifier = 4 [(google.api.field_behavior) = OPTIONAL];\n  }\n\n  // Authentication credentials. Provide one method.\n  oneof credentials {\n    // Username and password authentication.\n    PasswordCredentials password_credentials = 1;\n\n    // SSO provider authentication.\n    SSOCredentials sso_credentials = 2;\n  }\n}\n\nmessage SignInResponse {\n  // The authenticated user's information.\n  User user = 1;\n\n  // The short-lived access token for API requests.\n  // Store in memory only, not in localStorage.\n  string access_token = 2;\n\n  // When the access token expires.\n  // Client should call RefreshToken before this time.\n  google.protobuf.Timestamp access_token_expires_at = 3;\n}\n\nmessage SignOutRequest {}\n\nmessage RefreshTokenRequest {}\n\nmessage RefreshTokenResponse {\n  // The new short-lived access token.\n  string access_token = 1;\n\n  // When the access token expires.\n  google.protobuf.Timestamp expires_at = 2;\n}\n"
  },
  {
    "path": "proto/api/v1/common.proto",
    "content": "syntax = \"proto3\";\n\npackage memos.api.v1;\n\noption go_package = \"gen/api/v1\";\n\nenum State {\n  STATE_UNSPECIFIED = 0;\n  NORMAL = 1;\n  ARCHIVED = 2;\n}\n\n// Used internally for obfuscating the page token.\nmessage PageToken {\n  int32 limit = 1;\n  int32 offset = 2;\n}\n\nenum Direction {\n  DIRECTION_UNSPECIFIED = 0;\n  ASC = 1;\n  DESC = 2;\n}\n"
  },
  {
    "path": "proto/api/v1/idp_service.proto",
    "content": "syntax = \"proto3\";\n\npackage memos.api.v1;\n\nimport \"google/api/annotations.proto\";\nimport \"google/api/client.proto\";\nimport \"google/api/field_behavior.proto\";\nimport \"google/api/resource.proto\";\nimport \"google/protobuf/empty.proto\";\nimport \"google/protobuf/field_mask.proto\";\n\noption go_package = \"gen/api/v1\";\n\nservice IdentityProviderService {\n  // ListIdentityProviders lists identity providers.\n  rpc ListIdentityProviders(ListIdentityProvidersRequest) returns (ListIdentityProvidersResponse) {\n    option (google.api.http) = {get: \"/api/v1/identity-providers\"};\n  }\n\n  // GetIdentityProvider gets an identity provider.\n  rpc GetIdentityProvider(GetIdentityProviderRequest) returns (IdentityProvider) {\n    option (google.api.http) = {get: \"/api/v1/{name=identity-providers/*}\"};\n    option (google.api.method_signature) = \"name\";\n  }\n\n  // CreateIdentityProvider creates an identity provider.\n  rpc CreateIdentityProvider(CreateIdentityProviderRequest) returns (IdentityProvider) {\n    option (google.api.http) = {\n      post: \"/api/v1/identity-providers\"\n      body: \"identity_provider\"\n    };\n    option (google.api.method_signature) = \"identity_provider\";\n  }\n\n  // UpdateIdentityProvider updates an identity provider.\n  rpc UpdateIdentityProvider(UpdateIdentityProviderRequest) returns (IdentityProvider) {\n    option (google.api.http) = {\n      patch: \"/api/v1/{identity_provider.name=identity-providers/*}\"\n      body: \"identity_provider\"\n    };\n    option (google.api.method_signature) = \"identity_provider,update_mask\";\n  }\n\n  // DeleteIdentityProvider deletes an identity provider.\n  rpc DeleteIdentityProvider(DeleteIdentityProviderRequest) returns (google.protobuf.Empty) {\n    option (google.api.http) = {delete: \"/api/v1/{name=identity-providers/*}\"};\n    option (google.api.method_signature) = \"name\";\n  }\n}\n\nmessage IdentityProvider {\n  option (google.api.resource) = {\n    type: \"memos.api.v1/IdentityProvider\"\n    pattern: \"identity-providers/{idp}\"\n    name_field: \"name\"\n    singular: \"identityProvider\"\n    plural: \"identityProviders\"\n  };\n\n  // The resource name of the identity provider.\n  // Format: identity-providers/{idp}\n  string name = 1 [(google.api.field_behavior) = IDENTIFIER];\n\n  // Required. The type of the identity provider.\n  Type type = 2 [(google.api.field_behavior) = REQUIRED];\n\n  // Required. The display title of the identity provider.\n  string title = 3 [(google.api.field_behavior) = REQUIRED];\n\n  // Optional. Filter applied to user identifiers.\n  string identifier_filter = 4 [(google.api.field_behavior) = OPTIONAL];\n\n  // Required. Configuration for the identity provider.\n  IdentityProviderConfig config = 5 [(google.api.field_behavior) = REQUIRED];\n\n  enum Type {\n    TYPE_UNSPECIFIED = 0;\n    // OAuth2 identity provider.\n    OAUTH2 = 1;\n  }\n}\n\nmessage IdentityProviderConfig {\n  oneof config {\n    OAuth2Config oauth2_config = 1;\n  }\n}\n\nmessage FieldMapping {\n  string identifier = 1;\n  string display_name = 2;\n  string email = 3;\n  string avatar_url = 4;\n}\n\nmessage OAuth2Config {\n  string client_id = 1;\n  string client_secret = 2;\n  string auth_url = 3;\n  string token_url = 4;\n  string user_info_url = 5;\n  repeated string scopes = 6;\n  FieldMapping field_mapping = 7;\n}\n\nmessage ListIdentityProvidersRequest {}\n\nmessage ListIdentityProvidersResponse {\n  // The list of identity providers.\n  repeated IdentityProvider identity_providers = 1;\n}\n\nmessage GetIdentityProviderRequest {\n  // Required. The resource name of the identity provider to get.\n  // Format: identity-providers/{idp}\n  string name = 1 [\n    (google.api.field_behavior) = REQUIRED,\n    (google.api.resource_reference) = {type: \"memos.api.v1/IdentityProvider\"}\n  ];\n}\n\nmessage CreateIdentityProviderRequest {\n  // Required. The identity provider to create.\n  IdentityProvider identity_provider = 1 [(google.api.field_behavior) = REQUIRED];\n\n  // Optional. The ID to use for the identity provider, which will become the final component of the resource name.\n  // If not provided, the system will generate one.\n  string identity_provider_id = 2 [(google.api.field_behavior) = OPTIONAL];\n}\n\nmessage UpdateIdentityProviderRequest {\n  // Required. The identity provider to update.\n  IdentityProvider identity_provider = 1 [(google.api.field_behavior) = REQUIRED];\n\n  // Required. The update mask applies to the resource. Only the top level fields of\n  // IdentityProvider are supported.\n  google.protobuf.FieldMask update_mask = 2 [(google.api.field_behavior) = REQUIRED];\n}\n\nmessage DeleteIdentityProviderRequest {\n  // Required. The resource name of the identity provider to delete.\n  // Format: identity-providers/{idp}\n  string name = 1 [\n    (google.api.field_behavior) = REQUIRED,\n    (google.api.resource_reference) = {type: \"memos.api.v1/IdentityProvider\"}\n  ];\n}\n"
  },
  {
    "path": "proto/api/v1/instance_service.proto",
    "content": "syntax = \"proto3\";\n\npackage memos.api.v1;\n\nimport \"api/v1/user_service.proto\";\nimport \"google/api/annotations.proto\";\nimport \"google/api/client.proto\";\nimport \"google/api/field_behavior.proto\";\nimport \"google/api/resource.proto\";\nimport \"google/protobuf/field_mask.proto\";\nimport \"google/type/color.proto\";\n\noption go_package = \"gen/api/v1\";\n\nservice InstanceService {\n  // Gets the instance profile.\n  rpc GetInstanceProfile(GetInstanceProfileRequest) returns (InstanceProfile) {\n    option (google.api.http) = {get: \"/api/v1/instance/profile\"};\n  }\n\n  // Gets an instance setting.\n  rpc GetInstanceSetting(GetInstanceSettingRequest) returns (InstanceSetting) {\n    option (google.api.http) = {get: \"/api/v1/{name=instance/settings/*}\"};\n    option (google.api.method_signature) = \"name\";\n  }\n\n  // Updates an instance setting.\n  rpc UpdateInstanceSetting(UpdateInstanceSettingRequest) returns (InstanceSetting) {\n    option (google.api.http) = {\n      patch: \"/api/v1/{setting.name=instance/settings/*}\"\n      body: \"setting\"\n    };\n    option (google.api.method_signature) = \"setting,update_mask\";\n  }\n}\n\n// Instance profile message containing basic instance information.\nmessage InstanceProfile {\n  // Version is the current version of instance.\n  string version = 2;\n\n  // Demo indicates if the instance is in demo mode.\n  bool demo = 3;\n\n  // Instance URL is the URL of the instance.\n  string instance_url = 6;\n\n  // The first administrator who set up this instance.\n  // When null, instance requires initial setup (creating the first admin account).\n  User admin = 7;\n}\n\n// Request for instance profile.\nmessage GetInstanceProfileRequest {}\n\n// An instance setting resource.\nmessage InstanceSetting {\n  option (google.api.resource) = {\n    type: \"memos.api.v1/InstanceSetting\"\n    pattern: \"instance/settings/{setting}\"\n    singular: \"instanceSetting\"\n    plural: \"instanceSettings\"\n  };\n\n  // The name of the instance setting.\n  // Format: instance/settings/{setting}\n  string name = 1 [(google.api.field_behavior) = IDENTIFIER];\n\n  oneof value {\n    GeneralSetting general_setting = 2;\n    StorageSetting storage_setting = 3;\n    MemoRelatedSetting memo_related_setting = 4;\n    TagsSetting tags_setting = 5;\n    NotificationSetting notification_setting = 6;\n  }\n\n  // Enumeration of instance setting keys.\n  enum Key {\n    KEY_UNSPECIFIED = 0;\n    // GENERAL is the key for general settings.\n    GENERAL = 1;\n    // STORAGE is the key for storage settings.\n    STORAGE = 2;\n    // MEMO_RELATED is the key for memo related settings.\n    MEMO_RELATED = 3;\n    // TAGS is the key for tag metadata.\n    TAGS = 4;\n    // NOTIFICATION is the key for notification transport settings.\n    NOTIFICATION = 5;\n  }\n\n  // General instance settings configuration.\n  message GeneralSetting {\n    // disallow_user_registration disallows user registration.\n    bool disallow_user_registration = 2;\n    // disallow_password_auth disallows password authentication.\n    bool disallow_password_auth = 3;\n    // additional_script is the additional script.\n    string additional_script = 4;\n    // additional_style is the additional style.\n    string additional_style = 5;\n    // custom_profile is the custom profile.\n    CustomProfile custom_profile = 6;\n    // week_start_day_offset is the week start day offset from Sunday.\n    // 0: Sunday, 1: Monday, 2: Tuesday, 3: Wednesday, 4: Thursday, 5: Friday, 6: Saturday\n    // Default is Sunday.\n    int32 week_start_day_offset = 7;\n\n    // disallow_change_username disallows changing username.\n    bool disallow_change_username = 8;\n    // disallow_change_nickname disallows changing nickname.\n    bool disallow_change_nickname = 9;\n\n    // Custom profile configuration for instance branding.\n    message CustomProfile {\n      string title = 1;\n      string description = 2;\n      string logo_url = 3;\n    }\n  }\n\n  // Storage configuration settings for instance attachments.\n  message StorageSetting {\n    // Storage type enumeration for different storage backends.\n    enum StorageType {\n      STORAGE_TYPE_UNSPECIFIED = 0;\n      // DATABASE is the database storage type.\n      DATABASE = 1;\n      // LOCAL is the local storage type.\n      LOCAL = 2;\n      // S3 is the S3 storage type.\n      S3 = 3;\n    }\n    // storage_type is the storage type.\n    StorageType storage_type = 1;\n    // The template of file path.\n    // e.g. assets/{timestamp}_{filename}\n    string filepath_template = 2;\n    // The max upload size in megabytes.\n    int64 upload_size_limit_mb = 3;\n\n    // S3 configuration for cloud storage backend.\n    // Reference: https://developers.cloudflare.com/r2/examples/aws/aws-sdk-go/\n    message S3Config {\n      string access_key_id = 1;\n      string access_key_secret = 2;\n      string endpoint = 3;\n      string region = 4;\n      string bucket = 5;\n      bool use_path_style = 6;\n    }\n    // The S3 config.\n    S3Config s3_config = 4;\n  }\n\n  // Memo-related instance settings and policies.\n  message MemoRelatedSetting {\n    // display_with_update_time orders and displays memo with update time.\n    bool display_with_update_time = 2;\n    // content_length_limit is the limit of content length. Unit is byte.\n    int32 content_length_limit = 3;\n    // enable_double_click_edit enables editing on double click.\n    bool enable_double_click_edit = 4;\n    // reactions is the list of reactions.\n    repeated string reactions = 7;\n  }\n\n  // Metadata for a tag.\n  message TagMetadata {\n    // Background color for the tag label.\n    google.type.Color background_color = 1;\n  }\n\n  // Tag metadata configuration.\n  message TagsSetting {\n    map<string, TagMetadata> tags = 1;\n  }\n\n  // Notification transport configuration.\n  message NotificationSetting {\n    EmailSetting email = 1;\n\n    // Email delivery configuration for notifications.\n    message EmailSetting {\n      bool enabled = 1;\n      string smtp_host = 2;\n      int32 smtp_port = 3;\n      string smtp_username = 4;\n      string smtp_password = 5;\n      string from_email = 6;\n      string from_name = 7;\n      string reply_to = 8;\n      bool use_tls = 9;\n      bool use_ssl = 10;\n    }\n  }\n}\n\n// Request message for GetInstanceSetting method.\nmessage GetInstanceSettingRequest {\n  // The resource name of the instance setting.\n  // Format: instance/settings/{setting}\n  string name = 1 [\n    (google.api.field_behavior) = REQUIRED,\n    (google.api.resource_reference) = {type: \"memos.api.v1/InstanceSetting\"}\n  ];\n}\n\n// Request message for UpdateInstanceSetting method.\nmessage UpdateInstanceSettingRequest {\n  // The instance setting resource which replaces the resource on the server.\n  InstanceSetting setting = 1 [(google.api.field_behavior) = REQUIRED];\n\n  // The list of fields to update.\n  google.protobuf.FieldMask update_mask = 2 [(google.api.field_behavior) = OPTIONAL];\n}\n"
  },
  {
    "path": "proto/api/v1/memo_service.proto",
    "content": "syntax = \"proto3\";\n\npackage memos.api.v1;\n\nimport \"api/v1/attachment_service.proto\";\nimport \"api/v1/common.proto\";\nimport \"google/api/annotations.proto\";\nimport \"google/api/client.proto\";\nimport \"google/api/field_behavior.proto\";\nimport \"google/api/resource.proto\";\nimport \"google/protobuf/empty.proto\";\nimport \"google/protobuf/field_mask.proto\";\nimport \"google/protobuf/timestamp.proto\";\n\noption go_package = \"gen/api/v1\";\n\nservice MemoService {\n  // CreateMemo creates a memo.\n  rpc CreateMemo(CreateMemoRequest) returns (Memo) {\n    option (google.api.http) = {\n      post: \"/api/v1/memos\"\n      body: \"memo\"\n    };\n    option (google.api.method_signature) = \"memo\";\n  }\n  // ListMemos lists memos with pagination and filter.\n  rpc ListMemos(ListMemosRequest) returns (ListMemosResponse) {\n    option (google.api.http) = {get: \"/api/v1/memos\"};\n    option (google.api.method_signature) = \"\";\n  }\n  // GetMemo gets a memo.\n  rpc GetMemo(GetMemoRequest) returns (Memo) {\n    option (google.api.http) = {get: \"/api/v1/{name=memos/*}\"};\n    option (google.api.method_signature) = \"name\";\n  }\n  // UpdateMemo updates a memo.\n  rpc UpdateMemo(UpdateMemoRequest) returns (Memo) {\n    option (google.api.http) = {\n      patch: \"/api/v1/{memo.name=memos/*}\"\n      body: \"memo\"\n    };\n    option (google.api.method_signature) = \"memo,update_mask\";\n  }\n  // DeleteMemo deletes a memo.\n  rpc DeleteMemo(DeleteMemoRequest) returns (google.protobuf.Empty) {\n    option (google.api.http) = {delete: \"/api/v1/{name=memos/*}\"};\n    option (google.api.method_signature) = \"name\";\n  }\n  // SetMemoAttachments sets attachments for a memo.\n  rpc SetMemoAttachments(SetMemoAttachmentsRequest) returns (google.protobuf.Empty) {\n    option (google.api.http) = {\n      patch: \"/api/v1/{name=memos/*}/attachments\"\n      body: \"*\"\n    };\n    option (google.api.method_signature) = \"name\";\n  }\n  // ListMemoAttachments lists attachments for a memo.\n  rpc ListMemoAttachments(ListMemoAttachmentsRequest) returns (ListMemoAttachmentsResponse) {\n    option (google.api.http) = {get: \"/api/v1/{name=memos/*}/attachments\"};\n    option (google.api.method_signature) = \"name\";\n  }\n  // SetMemoRelations sets relations for a memo.\n  rpc SetMemoRelations(SetMemoRelationsRequest) returns (google.protobuf.Empty) {\n    option (google.api.http) = {\n      patch: \"/api/v1/{name=memos/*}/relations\"\n      body: \"*\"\n    };\n    option (google.api.method_signature) = \"name\";\n  }\n  // ListMemoRelations lists relations for a memo.\n  rpc ListMemoRelations(ListMemoRelationsRequest) returns (ListMemoRelationsResponse) {\n    option (google.api.http) = {get: \"/api/v1/{name=memos/*}/relations\"};\n    option (google.api.method_signature) = \"name\";\n  }\n  // CreateMemoComment creates a comment for a memo.\n  rpc CreateMemoComment(CreateMemoCommentRequest) returns (Memo) {\n    option (google.api.http) = {\n      post: \"/api/v1/{name=memos/*}/comments\"\n      body: \"comment\"\n    };\n    option (google.api.method_signature) = \"name,comment\";\n  }\n  // ListMemoComments lists comments for a memo.\n  rpc ListMemoComments(ListMemoCommentsRequest) returns (ListMemoCommentsResponse) {\n    option (google.api.http) = {get: \"/api/v1/{name=memos/*}/comments\"};\n    option (google.api.method_signature) = \"name\";\n  }\n  // ListMemoReactions lists reactions for a memo.\n  rpc ListMemoReactions(ListMemoReactionsRequest) returns (ListMemoReactionsResponse) {\n    option (google.api.http) = {get: \"/api/v1/{name=memos/*}/reactions\"};\n    option (google.api.method_signature) = \"name\";\n  }\n  // UpsertMemoReaction upserts a reaction for a memo.\n  rpc UpsertMemoReaction(UpsertMemoReactionRequest) returns (Reaction) {\n    option (google.api.http) = {\n      post: \"/api/v1/{name=memos/*}/reactions\"\n      body: \"*\"\n    };\n    option (google.api.method_signature) = \"name\";\n  }\n  // DeleteMemoReaction deletes a reaction for a memo.\n  rpc DeleteMemoReaction(DeleteMemoReactionRequest) returns (google.protobuf.Empty) {\n    option (google.api.http) = {delete: \"/api/v1/{name=memos/*/reactions/*}\"};\n    option (google.api.method_signature) = \"name\";\n  }\n  // CreateMemoShare creates a share link for a memo. Requires authentication as the memo creator.\n  rpc CreateMemoShare(CreateMemoShareRequest) returns (MemoShare) {\n    option (google.api.http) = {\n      post: \"/api/v1/{parent=memos/*}/shares\"\n      body: \"memo_share\"\n    };\n    option (google.api.method_signature) = \"parent,memo_share\";\n  }\n  // ListMemoShares lists all share links for a memo. Requires authentication as the memo creator.\n  rpc ListMemoShares(ListMemoSharesRequest) returns (ListMemoSharesResponse) {\n    option (google.api.http) = {get: \"/api/v1/{parent=memos/*}/shares\"};\n    option (google.api.method_signature) = \"parent\";\n  }\n  // DeleteMemoShare revokes a share link. Requires authentication as the memo creator.\n  rpc DeleteMemoShare(DeleteMemoShareRequest) returns (google.protobuf.Empty) {\n    option (google.api.http) = {delete: \"/api/v1/{name=memos/*/shares/*}\"};\n    option (google.api.method_signature) = \"name\";\n  }\n  // GetMemoByShare resolves a share token to its memo. No authentication required.\n  // Returns NOT_FOUND if the token is invalid or expired.\n  rpc GetMemoByShare(GetMemoByShareRequest) returns (Memo) {\n    option (google.api.http) = {get: \"/api/v1/shares/{share_id}\"};\n  }\n}\n\nenum Visibility {\n  VISIBILITY_UNSPECIFIED = 0;\n  PRIVATE = 1;\n  PROTECTED = 2;\n  PUBLIC = 3;\n}\n\nmessage Reaction {\n  option (google.api.resource) = {\n    type: \"memos.api.v1/Reaction\"\n    pattern: \"memos/{memo}/reactions/{reaction}\"\n    name_field: \"name\"\n    singular: \"reaction\"\n    plural: \"reactions\"\n  };\n\n  // The resource name of the reaction.\n  // Format: memos/{memo}/reactions/{reaction}\n  string name = 1 [\n    (google.api.field_behavior) = OUTPUT_ONLY,\n    (google.api.field_behavior) = IDENTIFIER\n  ];\n\n  // The resource name of the creator.\n  // Format: users/{user}\n  string creator = 2 [\n    (google.api.field_behavior) = OUTPUT_ONLY,\n    (google.api.resource_reference) = {type: \"memos.api.v1/User\"}\n  ];\n\n  // The resource name of the content.\n  // For memo reactions, this should be the memo's resource name.\n  // Format: memos/{memo}\n  string content_id = 3 [\n    (google.api.field_behavior) = REQUIRED,\n    (google.api.resource_reference) = {type: \"memos.api.v1/Memo\"}\n  ];\n\n  // Required. The type of reaction (e.g., \"👍\", \"❤️\", \"😄\").\n  string reaction_type = 4 [(google.api.field_behavior) = REQUIRED];\n\n  // Output only. The creation timestamp.\n  google.protobuf.Timestamp create_time = 5 [(google.api.field_behavior) = OUTPUT_ONLY];\n}\n\nmessage Memo {\n  option (google.api.resource) = {\n    type: \"memos.api.v1/Memo\"\n    pattern: \"memos/{memo}\"\n    name_field: \"name\"\n    singular: \"memo\"\n    plural: \"memos\"\n  };\n\n  // The resource name of the memo.\n  // Format: memos/{memo}, memo is the user defined id or uuid.\n  string name = 1 [(google.api.field_behavior) = IDENTIFIER];\n\n  // The state of the memo.\n  State state = 2 [(google.api.field_behavior) = REQUIRED];\n\n  // The name of the creator.\n  // Format: users/{user}\n  string creator = 3 [\n    (google.api.field_behavior) = OUTPUT_ONLY,\n    (google.api.resource_reference) = {type: \"memos.api.v1/User\"}\n  ];\n\n  // The creation timestamp.\n  // If not set on creation, the server will set it to the current time.\n  google.protobuf.Timestamp create_time = 4 [(google.api.field_behavior) = OPTIONAL];\n\n  // The last update timestamp.\n  // If not set on creation, the server will set it to the current time.\n  google.protobuf.Timestamp update_time = 5 [(google.api.field_behavior) = OPTIONAL];\n\n  // The display timestamp of the memo.\n  google.protobuf.Timestamp display_time = 6 [(google.api.field_behavior) = OPTIONAL];\n\n  // Required. The content of the memo in Markdown format.\n  string content = 7 [(google.api.field_behavior) = REQUIRED];\n\n  // The visibility of the memo.\n  Visibility visibility = 9 [(google.api.field_behavior) = REQUIRED];\n\n  // Output only. The tags extracted from the content.\n  repeated string tags = 10 [(google.api.field_behavior) = OUTPUT_ONLY];\n\n  // Whether the memo is pinned.\n  bool pinned = 11 [(google.api.field_behavior) = OPTIONAL];\n\n  // Optional. The attachments of the memo.\n  repeated Attachment attachments = 12 [(google.api.field_behavior) = OPTIONAL];\n\n  // Optional. The relations of the memo.\n  repeated MemoRelation relations = 13 [(google.api.field_behavior) = OPTIONAL];\n\n  // Output only. The reactions to the memo.\n  repeated Reaction reactions = 14 [(google.api.field_behavior) = OUTPUT_ONLY];\n\n  // Output only. The computed properties of the memo.\n  Property property = 15 [(google.api.field_behavior) = OUTPUT_ONLY];\n\n  // Output only. The name of the parent memo.\n  // Format: memos/{memo}\n  optional string parent = 16 [\n    (google.api.field_behavior) = OUTPUT_ONLY,\n    (google.api.resource_reference) = {type: \"memos.api.v1/Memo\"}\n  ];\n\n  // Output only. The snippet of the memo content. Plain text only.\n  string snippet = 17 [(google.api.field_behavior) = OUTPUT_ONLY];\n\n  // Optional. The location of the memo.\n  optional Location location = 18 [(google.api.field_behavior) = OPTIONAL];\n\n  // Computed properties of a memo.\n  message Property {\n    bool has_link = 1;\n    bool has_task_list = 2;\n    bool has_code = 3;\n    bool has_incomplete_tasks = 4;\n    // The title extracted from the first H1 heading, if present.\n    string title = 5;\n  }\n}\n\nmessage Location {\n  // A placeholder text for the location.\n  string placeholder = 1 [(google.api.field_behavior) = OPTIONAL];\n\n  // The latitude of the location.\n  double latitude = 2 [(google.api.field_behavior) = OPTIONAL];\n\n  // The longitude of the location.\n  double longitude = 3 [(google.api.field_behavior) = OPTIONAL];\n}\n\nmessage CreateMemoRequest {\n  // Required. The memo to create.\n  Memo memo = 1 [(google.api.field_behavior) = REQUIRED];\n\n  // Optional. The memo ID to use for this memo.\n  // If empty, a unique ID will be generated.\n  string memo_id = 2 [(google.api.field_behavior) = OPTIONAL];\n}\n\nmessage ListMemosRequest {\n  // Optional. The maximum number of memos to return.\n  // The service may return fewer than this value.\n  // If unspecified, at most 50 memos will be returned.\n  // The maximum value is 1000; values above 1000 will be coerced to 1000.\n  int32 page_size = 1 [(google.api.field_behavior) = OPTIONAL];\n\n  // Optional. A page token, received from a previous `ListMemos` call.\n  // Provide this to retrieve the subsequent page.\n  string page_token = 2 [(google.api.field_behavior) = OPTIONAL];\n\n  // Optional. The state of the memos to list.\n  // Default to `NORMAL`. Set to `ARCHIVED` to list archived memos.\n  State state = 3 [(google.api.field_behavior) = OPTIONAL];\n\n  // Optional. The order to sort results by.\n  // Default to \"display_time desc\".\n  // Supports comma-separated list of fields following AIP-132.\n  // Example: \"pinned desc, display_time desc\" or \"create_time asc\"\n  // Supported fields: pinned, display_time, create_time, update_time, name\n  string order_by = 4 [(google.api.field_behavior) = OPTIONAL];\n\n  // Optional. Filter to apply to the list results.\n  // Filter is a CEL expression to filter memos.\n  // Refer to `Shortcut.filter`.\n  string filter = 5 [(google.api.field_behavior) = OPTIONAL];\n\n  // Optional. If true, show deleted memos in the response.\n  bool show_deleted = 6 [(google.api.field_behavior) = OPTIONAL];\n}\n\nmessage ListMemosResponse {\n  // The list of memos.\n  repeated Memo memos = 1;\n\n  // A token that can be sent as `page_token` to retrieve the next page.\n  // If this field is omitted, there are no subsequent pages.\n  string next_page_token = 2;\n}\n\nmessage GetMemoRequest {\n  // Required. The resource name of the memo.\n  // Format: memos/{memo}\n  string name = 1 [\n    (google.api.field_behavior) = REQUIRED,\n    (google.api.resource_reference) = {type: \"memos.api.v1/Memo\"}\n  ];\n}\n\nmessage UpdateMemoRequest {\n  // Required. The memo to update.\n  // The `name` field is required.\n  Memo memo = 1 [(google.api.field_behavior) = REQUIRED];\n\n  // Required. The list of fields to update.\n  google.protobuf.FieldMask update_mask = 2 [(google.api.field_behavior) = REQUIRED];\n}\n\nmessage DeleteMemoRequest {\n  // Required. The resource name of the memo to delete.\n  // Format: memos/{memo}\n  string name = 1 [\n    (google.api.field_behavior) = REQUIRED,\n    (google.api.resource_reference) = {type: \"memos.api.v1/Memo\"}\n  ];\n\n  // Optional. If set to true, the memo will be deleted even if it has associated data.\n  bool force = 2 [(google.api.field_behavior) = OPTIONAL];\n}\n\nmessage SetMemoAttachmentsRequest {\n  // Required. The resource name of the memo.\n  // Format: memos/{memo}\n  string name = 1 [\n    (google.api.field_behavior) = REQUIRED,\n    (google.api.resource_reference) = {type: \"memos.api.v1/Memo\"}\n  ];\n\n  // Required. The attachments to set for the memo.\n  repeated Attachment attachments = 2 [(google.api.field_behavior) = REQUIRED];\n}\n\nmessage ListMemoAttachmentsRequest {\n  // Required. The resource name of the memo.\n  // Format: memos/{memo}\n  string name = 1 [\n    (google.api.field_behavior) = REQUIRED,\n    (google.api.resource_reference) = {type: \"memos.api.v1/Memo\"}\n  ];\n\n  // Optional. The maximum number of attachments to return.\n  int32 page_size = 2 [(google.api.field_behavior) = OPTIONAL];\n\n  // Optional. A page token for pagination.\n  string page_token = 3 [(google.api.field_behavior) = OPTIONAL];\n}\n\nmessage ListMemoAttachmentsResponse {\n  // The list of attachments.\n  repeated Attachment attachments = 1;\n\n  // A token for the next page of results.\n  string next_page_token = 2;\n}\n\nmessage MemoRelation {\n  // The memo in the relation.\n  Memo memo = 1 [(google.api.field_behavior) = REQUIRED];\n\n  // The related memo.\n  Memo related_memo = 2 [(google.api.field_behavior) = REQUIRED];\n\n  // The type of the relation.\n  enum Type {\n    TYPE_UNSPECIFIED = 0;\n    REFERENCE = 1;\n    COMMENT = 2;\n  }\n  Type type = 3 [(google.api.field_behavior) = REQUIRED];\n\n  // Memo reference in relations.\n  message Memo {\n    // The resource name of the memo.\n    // Format: memos/{memo}\n    string name = 1 [\n      (google.api.field_behavior) = REQUIRED,\n      (google.api.resource_reference) = {type: \"memos.api.v1/Memo\"}\n    ];\n\n    // Output only. The snippet of the memo content. Plain text only.\n    string snippet = 2 [(google.api.field_behavior) = OUTPUT_ONLY];\n  }\n}\n\nmessage SetMemoRelationsRequest {\n  // Required. The resource name of the memo.\n  // Format: memos/{memo}\n  string name = 1 [\n    (google.api.field_behavior) = REQUIRED,\n    (google.api.resource_reference) = {type: \"memos.api.v1/Memo\"}\n  ];\n\n  // Required. The relations to set for the memo.\n  repeated MemoRelation relations = 2 [(google.api.field_behavior) = REQUIRED];\n}\n\nmessage ListMemoRelationsRequest {\n  // Required. The resource name of the memo.\n  // Format: memos/{memo}\n  string name = 1 [\n    (google.api.field_behavior) = REQUIRED,\n    (google.api.resource_reference) = {type: \"memos.api.v1/Memo\"}\n  ];\n\n  // Optional. The maximum number of relations to return.\n  int32 page_size = 2 [(google.api.field_behavior) = OPTIONAL];\n\n  // Optional. A page token for pagination.\n  string page_token = 3 [(google.api.field_behavior) = OPTIONAL];\n}\n\nmessage ListMemoRelationsResponse {\n  // The list of relations.\n  repeated MemoRelation relations = 1;\n\n  // A token for the next page of results.\n  string next_page_token = 2;\n}\n\nmessage CreateMemoCommentRequest {\n  // Required. The resource name of the memo.\n  // Format: memos/{memo}\n  string name = 1 [\n    (google.api.field_behavior) = REQUIRED,\n    (google.api.resource_reference) = {type: \"memos.api.v1/Memo\"}\n  ];\n\n  // Required. The comment to create.\n  Memo comment = 2 [(google.api.field_behavior) = REQUIRED];\n\n  // Optional. The comment ID to use.\n  string comment_id = 3 [(google.api.field_behavior) = OPTIONAL];\n}\n\nmessage ListMemoCommentsRequest {\n  // Required. The resource name of the memo.\n  // Format: memos/{memo}\n  string name = 1 [\n    (google.api.field_behavior) = REQUIRED,\n    (google.api.resource_reference) = {type: \"memos.api.v1/Memo\"}\n  ];\n\n  // Optional. The maximum number of comments to return.\n  int32 page_size = 2 [(google.api.field_behavior) = OPTIONAL];\n\n  // Optional. A page token for pagination.\n  string page_token = 3 [(google.api.field_behavior) = OPTIONAL];\n\n  // Optional. The order to sort results by.\n  string order_by = 4 [(google.api.field_behavior) = OPTIONAL];\n}\n\nmessage ListMemoCommentsResponse {\n  // The list of comment memos.\n  repeated Memo memos = 1;\n\n  // A token for the next page of results.\n  string next_page_token = 2;\n\n  // The total count of comments.\n  int32 total_size = 3;\n}\n\nmessage ListMemoReactionsRequest {\n  // Required. The resource name of the memo.\n  // Format: memos/{memo}\n  string name = 1 [\n    (google.api.field_behavior) = REQUIRED,\n    (google.api.resource_reference) = {type: \"memos.api.v1/Memo\"}\n  ];\n\n  // Optional. The maximum number of reactions to return.\n  int32 page_size = 2 [(google.api.field_behavior) = OPTIONAL];\n\n  // Optional. A page token for pagination.\n  string page_token = 3 [(google.api.field_behavior) = OPTIONAL];\n}\n\nmessage ListMemoReactionsResponse {\n  // The list of reactions.\n  repeated Reaction reactions = 1;\n\n  // A token for the next page of results.\n  string next_page_token = 2;\n\n  // The total count of reactions.\n  int32 total_size = 3;\n}\n\nmessage UpsertMemoReactionRequest {\n  // Required. The resource name of the memo.\n  // Format: memos/{memo}\n  string name = 1 [\n    (google.api.field_behavior) = REQUIRED,\n    (google.api.resource_reference) = {type: \"memos.api.v1/Memo\"}\n  ];\n\n  // Required. The reaction to upsert.\n  Reaction reaction = 2 [(google.api.field_behavior) = REQUIRED];\n}\n\nmessage DeleteMemoReactionRequest {\n  // Required. The resource name of the reaction to delete.\n  // Format: memos/{memo}/reactions/{reaction}\n  string name = 1 [\n    (google.api.field_behavior) = REQUIRED,\n    (google.api.resource_reference) = {type: \"memos.api.v1/Reaction\"}\n  ];\n}\n\n// MemoShare is an access grant that permits read-only access to a memo via an opaque bearer token.\nmessage MemoShare {\n  option (google.api.resource) = {\n    type: \"memos.api.v1/MemoShare\"\n    pattern: \"memos/{memo}/shares/{share}\"\n    singular: \"share\"\n    plural: \"shares\"\n  };\n\n  // The resource name of the share. Format: memos/{memo}/shares/{share}\n  // The {share} segment is the opaque token used in the share URL.\n  string name = 1 [(google.api.field_behavior) = IDENTIFIER];\n\n  // Output only. When this share link was created.\n  google.protobuf.Timestamp create_time = 2 [(google.api.field_behavior) = OUTPUT_ONLY];\n\n  // Optional. When set, the share link stops working after this time.\n  // If unset, the link never expires.\n  optional google.protobuf.Timestamp expire_time = 3 [(google.api.field_behavior) = OPTIONAL];\n}\n\nmessage CreateMemoShareRequest {\n  // Required. The resource name of the memo to share.\n  // Format: memos/{memo}\n  string parent = 1 [\n    (google.api.field_behavior) = REQUIRED,\n    (google.api.resource_reference) = {type: \"memos.api.v1/Memo\"}\n  ];\n\n  // Required. The share to create.\n  MemoShare memo_share = 2 [(google.api.field_behavior) = REQUIRED];\n}\n\nmessage ListMemoSharesRequest {\n  // Required. The resource name of the memo.\n  // Format: memos/{memo}\n  string parent = 1 [\n    (google.api.field_behavior) = REQUIRED,\n    (google.api.resource_reference) = {type: \"memos.api.v1/Memo\"}\n  ];\n}\n\nmessage ListMemoSharesResponse {\n  // The list of share links.\n  repeated MemoShare memo_shares = 1;\n}\n\nmessage DeleteMemoShareRequest {\n  // Required. The resource name of the share to delete.\n  // Format: memos/{memo}/shares/{share}\n  string name = 1 [\n    (google.api.field_behavior) = REQUIRED,\n    (google.api.resource_reference) = {type: \"memos.api.v1/MemoShare\"}\n  ];\n}\n\nmessage GetMemoByShareRequest {\n  // Required. The share token extracted from the share URL (/s/{share_id}).\n  string share_id = 1 [(google.api.field_behavior) = REQUIRED];\n}\n"
  },
  {
    "path": "proto/api/v1/shortcut_service.proto",
    "content": "syntax = \"proto3\";\n\npackage memos.api.v1;\n\nimport \"google/api/annotations.proto\";\nimport \"google/api/client.proto\";\nimport \"google/api/field_behavior.proto\";\nimport \"google/api/resource.proto\";\nimport \"google/protobuf/empty.proto\";\nimport \"google/protobuf/field_mask.proto\";\n\noption go_package = \"gen/api/v1\";\n\nservice ShortcutService {\n  // ListShortcuts returns a list of shortcuts for a user.\n  rpc ListShortcuts(ListShortcutsRequest) returns (ListShortcutsResponse) {\n    option (google.api.http) = {get: \"/api/v1/{parent=users/*}/shortcuts\"};\n    option (google.api.method_signature) = \"parent\";\n  }\n\n  // GetShortcut gets a shortcut by name.\n  rpc GetShortcut(GetShortcutRequest) returns (Shortcut) {\n    option (google.api.http) = {get: \"/api/v1/{name=users/*/shortcuts/*}\"};\n    option (google.api.method_signature) = \"name\";\n  }\n\n  // CreateShortcut creates a new shortcut for a user.\n  rpc CreateShortcut(CreateShortcutRequest) returns (Shortcut) {\n    option (google.api.http) = {\n      post: \"/api/v1/{parent=users/*}/shortcuts\"\n      body: \"shortcut\"\n    };\n    option (google.api.method_signature) = \"parent,shortcut\";\n  }\n\n  // UpdateShortcut updates a shortcut for a user.\n  rpc UpdateShortcut(UpdateShortcutRequest) returns (Shortcut) {\n    option (google.api.http) = {\n      patch: \"/api/v1/{shortcut.name=users/*/shortcuts/*}\"\n      body: \"shortcut\"\n    };\n    option (google.api.method_signature) = \"shortcut,update_mask\";\n  }\n\n  // DeleteShortcut deletes a shortcut for a user.\n  rpc DeleteShortcut(DeleteShortcutRequest) returns (google.protobuf.Empty) {\n    option (google.api.http) = {delete: \"/api/v1/{name=users/*/shortcuts/*}\"};\n    option (google.api.method_signature) = \"name\";\n  }\n}\n\nmessage Shortcut {\n  option (google.api.resource) = {\n    type: \"memos.api.v1/Shortcut\"\n    pattern: \"users/{user}/shortcuts/{shortcut}\"\n    singular: \"shortcut\"\n    plural: \"shortcuts\"\n  };\n\n  // The resource name of the shortcut.\n  // Format: users/{user}/shortcuts/{shortcut}\n  string name = 1 [(google.api.field_behavior) = IDENTIFIER];\n\n  // The title of the shortcut.\n  string title = 2 [(google.api.field_behavior) = REQUIRED];\n\n  // The filter expression for the shortcut.\n  string filter = 3 [(google.api.field_behavior) = OPTIONAL];\n}\n\nmessage ListShortcutsRequest {\n  // Required. The parent resource where shortcuts are listed.\n  // Format: users/{user}\n  string parent = 1 [\n    (google.api.field_behavior) = REQUIRED,\n    (google.api.resource_reference) = {child_type: \"memos.api.v1/Shortcut\"}\n  ];\n}\n\nmessage ListShortcutsResponse {\n  // The list of shortcuts.\n  repeated Shortcut shortcuts = 1;\n}\n\nmessage GetShortcutRequest {\n  // Required. The resource name of the shortcut to retrieve.\n  // Format: users/{user}/shortcuts/{shortcut}\n  string name = 1 [\n    (google.api.field_behavior) = REQUIRED,\n    (google.api.resource_reference) = {type: \"memos.api.v1/Shortcut\"}\n  ];\n}\n\nmessage CreateShortcutRequest {\n  // Required. The parent resource where this shortcut will be created.\n  // Format: users/{user}\n  string parent = 1 [\n    (google.api.field_behavior) = REQUIRED,\n    (google.api.resource_reference) = {child_type: \"memos.api.v1/Shortcut\"}\n  ];\n\n  // Required. The shortcut to create.\n  Shortcut shortcut = 2 [(google.api.field_behavior) = REQUIRED];\n\n  // Optional. If set, validate the request, but do not actually create the shortcut.\n  bool validate_only = 3 [(google.api.field_behavior) = OPTIONAL];\n}\n\nmessage UpdateShortcutRequest {\n  // Required. The shortcut resource which replaces the resource on the server.\n  Shortcut shortcut = 1 [(google.api.field_behavior) = REQUIRED];\n\n  // Optional. The list of fields to update.\n  google.protobuf.FieldMask update_mask = 2 [(google.api.field_behavior) = OPTIONAL];\n}\n\nmessage DeleteShortcutRequest {\n  // Required. The resource name of the shortcut to delete.\n  // Format: users/{user}/shortcuts/{shortcut}\n  string name = 1 [\n    (google.api.field_behavior) = REQUIRED,\n    (google.api.resource_reference) = {type: \"memos.api.v1/Shortcut\"}\n  ];\n}\n"
  },
  {
    "path": "proto/api/v1/user_service.proto",
    "content": "syntax = \"proto3\";\n\npackage memos.api.v1;\n\nimport \"api/v1/common.proto\";\nimport \"google/api/annotations.proto\";\nimport \"google/api/client.proto\";\nimport \"google/api/field_behavior.proto\";\nimport \"google/api/resource.proto\";\nimport \"google/protobuf/empty.proto\";\nimport \"google/protobuf/field_mask.proto\";\nimport \"google/protobuf/timestamp.proto\";\n\noption go_package = \"gen/api/v1\";\n\nservice UserService {\n  // ListUsers returns a list of users.\n  rpc ListUsers(ListUsersRequest) returns (ListUsersResponse) {\n    option (google.api.http) = {get: \"/api/v1/users\"};\n  }\n\n  // GetUser gets a user by ID or username.\n  // Supports both numeric IDs and username strings:\n  //   - users/{id}       (e.g., users/101)\n  //   - users/{username} (e.g., users/steven)\n  rpc GetUser(GetUserRequest) returns (User) {\n    option (google.api.http) = {get: \"/api/v1/{name=users/*}\"};\n    option (google.api.method_signature) = \"name\";\n  }\n\n  // CreateUser creates a new user.\n  rpc CreateUser(CreateUserRequest) returns (User) {\n    option (google.api.http) = {\n      post: \"/api/v1/users\"\n      body: \"user\"\n    };\n    option (google.api.method_signature) = \"user\";\n  }\n\n  // UpdateUser updates a user.\n  rpc UpdateUser(UpdateUserRequest) returns (User) {\n    option (google.api.http) = {\n      patch: \"/api/v1/{user.name=users/*}\"\n      body: \"user\"\n    };\n    option (google.api.method_signature) = \"user,update_mask\";\n  }\n\n  // DeleteUser deletes a user.\n  rpc DeleteUser(DeleteUserRequest) returns (google.protobuf.Empty) {\n    option (google.api.http) = {delete: \"/api/v1/{name=users/*}\"};\n    option (google.api.method_signature) = \"name\";\n  }\n\n  // ListAllUserStats returns statistics for all users.\n  rpc ListAllUserStats(ListAllUserStatsRequest) returns (ListAllUserStatsResponse) {\n    option (google.api.http) = {get: \"/api/v1/users:stats\"};\n  }\n\n  // GetUserStats returns statistics for a specific user.\n  rpc GetUserStats(GetUserStatsRequest) returns (UserStats) {\n    option (google.api.http) = {get: \"/api/v1/{name=users/*}:getStats\"};\n    option (google.api.method_signature) = \"name\";\n  }\n\n  // GetUserSetting returns the user setting.\n  rpc GetUserSetting(GetUserSettingRequest) returns (UserSetting) {\n    option (google.api.http) = {get: \"/api/v1/{name=users/*/settings/*}\"};\n    option (google.api.method_signature) = \"name\";\n  }\n\n  // UpdateUserSetting updates the user setting.\n  rpc UpdateUserSetting(UpdateUserSettingRequest) returns (UserSetting) {\n    option (google.api.http) = {\n      patch: \"/api/v1/{setting.name=users/*/settings/*}\"\n      body: \"setting\"\n    };\n    option (google.api.method_signature) = \"setting,update_mask\";\n  }\n\n  // ListUserSettings returns a list of user settings.\n  rpc ListUserSettings(ListUserSettingsRequest) returns (ListUserSettingsResponse) {\n    option (google.api.http) = {get: \"/api/v1/{parent=users/*}/settings\"};\n    option (google.api.method_signature) = \"parent\";\n  }\n\n  // ListPersonalAccessTokens returns a list of Personal Access Tokens (PATs) for a user.\n  // PATs are long-lived tokens for API/script access, distinct from short-lived JWT access tokens.\n  rpc ListPersonalAccessTokens(ListPersonalAccessTokensRequest) returns (ListPersonalAccessTokensResponse) {\n    option (google.api.http) = {get: \"/api/v1/{parent=users/*}/personalAccessTokens\"};\n    option (google.api.method_signature) = \"parent\";\n  }\n\n  // CreatePersonalAccessToken creates a new Personal Access Token for a user.\n  // The token value is only returned once upon creation.\n  rpc CreatePersonalAccessToken(CreatePersonalAccessTokenRequest) returns (CreatePersonalAccessTokenResponse) {\n    option (google.api.http) = {\n      post: \"/api/v1/{parent=users/*}/personalAccessTokens\"\n      body: \"*\"\n    };\n  }\n\n  // DeletePersonalAccessToken deletes a Personal Access Token.\n  rpc DeletePersonalAccessToken(DeletePersonalAccessTokenRequest) returns (google.protobuf.Empty) {\n    option (google.api.http) = {delete: \"/api/v1/{name=users/*/personalAccessTokens/*}\"};\n    option (google.api.method_signature) = \"name\";\n  }\n\n  // ListUserWebhooks returns a list of webhooks for a user.\n  rpc ListUserWebhooks(ListUserWebhooksRequest) returns (ListUserWebhooksResponse) {\n    option (google.api.http) = {get: \"/api/v1/{parent=users/*}/webhooks\"};\n    option (google.api.method_signature) = \"parent\";\n  }\n\n  // CreateUserWebhook creates a new webhook for a user.\n  rpc CreateUserWebhook(CreateUserWebhookRequest) returns (UserWebhook) {\n    option (google.api.http) = {\n      post: \"/api/v1/{parent=users/*}/webhooks\"\n      body: \"webhook\"\n    };\n    option (google.api.method_signature) = \"parent,webhook\";\n  }\n\n  // UpdateUserWebhook updates an existing webhook for a user.\n  rpc UpdateUserWebhook(UpdateUserWebhookRequest) returns (UserWebhook) {\n    option (google.api.http) = {\n      patch: \"/api/v1/{webhook.name=users/*/webhooks/*}\"\n      body: \"webhook\"\n    };\n    option (google.api.method_signature) = \"webhook,update_mask\";\n  }\n\n  // DeleteUserWebhook deletes a webhook for a user.\n  rpc DeleteUserWebhook(DeleteUserWebhookRequest) returns (google.protobuf.Empty) {\n    option (google.api.http) = {delete: \"/api/v1/{name=users/*/webhooks/*}\"};\n    option (google.api.method_signature) = \"name\";\n  }\n\n  // ListUserNotifications lists notifications for a user.\n  rpc ListUserNotifications(ListUserNotificationsRequest) returns (ListUserNotificationsResponse) {\n    option (google.api.http) = {get: \"/api/v1/{parent=users/*}/notifications\"};\n    option (google.api.method_signature) = \"parent\";\n  }\n\n  // UpdateUserNotification updates a notification.\n  rpc UpdateUserNotification(UpdateUserNotificationRequest) returns (UserNotification) {\n    option (google.api.http) = {\n      patch: \"/api/v1/{notification.name=users/*/notifications/*}\"\n      body: \"notification\"\n    };\n    option (google.api.method_signature) = \"notification,update_mask\";\n  }\n\n  // DeleteUserNotification deletes a notification.\n  rpc DeleteUserNotification(DeleteUserNotificationRequest) returns (google.protobuf.Empty) {\n    option (google.api.http) = {delete: \"/api/v1/{name=users/*/notifications/*}\"};\n    option (google.api.method_signature) = \"name\";\n  }\n}\n\nmessage User {\n  option (google.api.resource) = {\n    type: \"memos.api.v1/User\"\n    pattern: \"users/{user}\"\n    name_field: \"name\"\n    singular: \"user\"\n    plural: \"users\"\n  };\n\n  // The resource name of the user.\n  // Format: users/{user}\n  string name = 1 [(google.api.field_behavior) = IDENTIFIER];\n\n  // The role of the user.\n  Role role = 2 [(google.api.field_behavior) = REQUIRED];\n\n  // Required. The unique username for login.\n  string username = 3 [(google.api.field_behavior) = REQUIRED];\n\n  // Optional. The email address of the user.\n  string email = 4 [(google.api.field_behavior) = OPTIONAL];\n\n  // Optional. The display name of the user.\n  string display_name = 5 [(google.api.field_behavior) = OPTIONAL];\n\n  // Optional. The avatar URL of the user.\n  string avatar_url = 6 [(google.api.field_behavior) = OPTIONAL];\n\n  // Optional. The description of the user.\n  string description = 7 [(google.api.field_behavior) = OPTIONAL];\n\n  // Input only. The password for the user.\n  string password = 8 [(google.api.field_behavior) = INPUT_ONLY];\n\n  // The state of the user.\n  State state = 9 [(google.api.field_behavior) = REQUIRED];\n\n  // Output only. The creation timestamp.\n  google.protobuf.Timestamp create_time = 10 [(google.api.field_behavior) = OUTPUT_ONLY];\n\n  // Output only. The last update timestamp.\n  google.protobuf.Timestamp update_time = 11 [(google.api.field_behavior) = OUTPUT_ONLY];\n\n  // User role enumeration.\n  enum Role {\n    ROLE_UNSPECIFIED = 0;\n    // Admin role with system access.\n    ADMIN = 2;\n    // User role with limited access.\n    USER = 3;\n  }\n}\n\nmessage ListUsersRequest {\n  // Optional. The maximum number of users to return.\n  // The service may return fewer than this value.\n  // If unspecified, at most 50 users will be returned.\n  // The maximum value is 1000; values above 1000 will be coerced to 1000.\n  int32 page_size = 1 [(google.api.field_behavior) = OPTIONAL];\n\n  // Optional. A page token, received from a previous `ListUsers` call.\n  // Provide this to retrieve the subsequent page.\n  string page_token = 2 [(google.api.field_behavior) = OPTIONAL];\n\n  // Optional. Filter to apply to the list results.\n  // Example: \"username == 'steven'\"\n  // Supported operators: ==\n  // Supported fields: username\n  string filter = 3 [(google.api.field_behavior) = OPTIONAL];\n\n  // Optional. If true, show deleted users in the response.\n  bool show_deleted = 4 [(google.api.field_behavior) = OPTIONAL];\n}\n\nmessage ListUsersResponse {\n  // The list of users.\n  repeated User users = 1;\n\n  // A token that can be sent as `page_token` to retrieve the next page.\n  // If this field is omitted, there are no subsequent pages.\n  string next_page_token = 2;\n\n  // The total count of users (may be approximate).\n  int32 total_size = 3;\n}\n\nmessage GetUserRequest {\n  // Required. The resource name of the user.\n  // Supports both numeric IDs and username strings:\n  //   - users/{id}       (e.g., users/101)\n  //   - users/{username} (e.g., users/steven)\n  // Format: users/{id_or_username}\n  string name = 1 [\n    (google.api.field_behavior) = REQUIRED,\n    (google.api.resource_reference) = {type: \"memos.api.v1/User\"}\n  ];\n\n  // Optional. The fields to return in the response.\n  // If not specified, all fields are returned.\n  google.protobuf.FieldMask read_mask = 2 [(google.api.field_behavior) = OPTIONAL];\n}\n\nmessage CreateUserRequest {\n  // Required. The user to create.\n  User user = 1 [\n    (google.api.field_behavior) = REQUIRED,\n    (google.api.field_behavior) = INPUT_ONLY\n  ];\n\n  // Optional. The user ID to use for this user.\n  // If empty, a unique ID will be generated.\n  // Must match the pattern [a-z0-9-]+\n  string user_id = 2 [(google.api.field_behavior) = OPTIONAL];\n\n  // Optional. If set, validate the request but don't actually create the user.\n  bool validate_only = 3 [(google.api.field_behavior) = OPTIONAL];\n\n  // Optional. An idempotency token that can be used to ensure that multiple\n  // requests to create a user have the same result.\n  string request_id = 4 [(google.api.field_behavior) = OPTIONAL];\n}\n\nmessage UpdateUserRequest {\n  // Required. The user to update.\n  User user = 1 [(google.api.field_behavior) = REQUIRED];\n\n  // Required. The list of fields to update.\n  google.protobuf.FieldMask update_mask = 2 [(google.api.field_behavior) = REQUIRED];\n\n  // Optional. If set to true, allows updating sensitive fields.\n  bool allow_missing = 3 [(google.api.field_behavior) = OPTIONAL];\n}\n\nmessage DeleteUserRequest {\n  // Required. The resource name of the user to delete.\n  // Format: users/{user}\n  string name = 1 [\n    (google.api.field_behavior) = REQUIRED,\n    (google.api.resource_reference) = {type: \"memos.api.v1/User\"}\n  ];\n\n  // Optional. If set to true, the user will be deleted even if they have associated data.\n  bool force = 2 [(google.api.field_behavior) = OPTIONAL];\n}\n\n// User statistics messages\nmessage UserStats {\n  option (google.api.resource) = {\n    type: \"memos.api.v1/UserStats\"\n    pattern: \"users/{user}\"\n    singular: \"userStats\"\n    plural: \"userStats\"\n  };\n\n  // The resource name of the user whose stats these are.\n  // Format: users/{user}\n  string name = 1 [(google.api.field_behavior) = IDENTIFIER];\n\n  // The timestamps when the memos were displayed.\n  repeated google.protobuf.Timestamp memo_display_timestamps = 2;\n\n  // The stats of memo types.\n  MemoTypeStats memo_type_stats = 3;\n\n  // The count of tags.\n  map<string, int32> tag_count = 4;\n\n  // The pinned memos of the user.\n  repeated string pinned_memos = 5;\n\n  // Total memo count.\n  int32 total_memo_count = 6;\n\n  // Memo type statistics.\n  message MemoTypeStats {\n    int32 link_count = 1;\n    int32 code_count = 2;\n    int32 todo_count = 3;\n    int32 undo_count = 4;\n  }\n}\n\nmessage GetUserStatsRequest {\n  // Required. The resource name of the user.\n  // Format: users/{user}\n  string name = 1 [\n    (google.api.field_behavior) = REQUIRED,\n    (google.api.resource_reference) = {type: \"memos.api.v1/User\"}\n  ];\n}\n\nmessage ListAllUserStatsRequest {\n  // This endpoint doesn't take any parameters.\n}\n\nmessage ListAllUserStatsResponse {\n  // The list of user statistics.\n  repeated UserStats stats = 1;\n}\n\n// User settings message\nmessage UserSetting {\n  option (google.api.resource) = {\n    type: \"memos.api.v1/UserSetting\"\n    pattern: \"users/{user}/settings/{setting}\"\n    singular: \"userSetting\"\n    plural: \"userSettings\"\n  };\n\n  // The name of the user setting.\n  // Format: users/{user}/settings/{setting}, {setting} is the key for the setting.\n  // For example, \"users/123/settings/GENERAL\" for general settings.\n  string name = 1 [(google.api.field_behavior) = IDENTIFIER];\n\n  oneof value {\n    GeneralSetting general_setting = 2;\n    WebhooksSetting webhooks_setting = 5;\n  }\n\n  // Enumeration of user setting keys.\n  enum Key {\n    KEY_UNSPECIFIED = 0;\n    // GENERAL is the key for general user settings.\n    GENERAL = 1;\n    // WEBHOOKS is the key for user webhooks.\n    WEBHOOKS = 4;\n  }\n\n  // General user settings configuration.\n  message GeneralSetting {\n    // The preferred locale of the user.\n    string locale = 1 [(google.api.field_behavior) = OPTIONAL];\n    // The default visibility of the memo.\n    string memo_visibility = 3 [(google.api.field_behavior) = OPTIONAL];\n    // The preferred theme of the user.\n    // This references a CSS file in the web/public/themes/ directory.\n    // If not set, the default theme will be used.\n    string theme = 4 [(google.api.field_behavior) = OPTIONAL];\n  }\n\n  // User webhooks configuration.\n  message WebhooksSetting {\n    // List of user webhooks.\n    repeated UserWebhook webhooks = 1;\n  }\n}\n\nmessage GetUserSettingRequest {\n  // Required. The resource name of the user setting.\n  // Format: users/{user}/settings/{setting}\n  string name = 1 [\n    (google.api.field_behavior) = REQUIRED,\n    (google.api.resource_reference) = {type: \"memos.api.v1/UserSetting\"}\n  ];\n}\n\nmessage UpdateUserSettingRequest {\n  // Required. The user setting to update.\n  UserSetting setting = 1 [(google.api.field_behavior) = REQUIRED];\n\n  // Required. The list of fields to update.\n  google.protobuf.FieldMask update_mask = 2 [(google.api.field_behavior) = REQUIRED];\n}\n\n// Request message for ListUserSettings method.\nmessage ListUserSettingsRequest {\n  // Required. The parent resource whose settings will be listed.\n  // Format: users/{user}\n  string parent = 1 [\n    (google.api.field_behavior) = REQUIRED,\n    (google.api.resource_reference) = {type: \"memos.api.v1/User\"}\n  ];\n\n  // Optional. The maximum number of settings to return.\n  // The service may return fewer than this value.\n  // If unspecified, at most 50 settings will be returned.\n  // The maximum value is 1000; values above 1000 will be coerced to 1000.\n  int32 page_size = 2 [(google.api.field_behavior) = OPTIONAL];\n\n  // Optional. A page token, received from a previous `ListUserSettings` call.\n  // Provide this to retrieve the subsequent page.\n  string page_token = 3 [(google.api.field_behavior) = OPTIONAL];\n}\n\n// Response message for ListUserSettings method.\nmessage ListUserSettingsResponse {\n  // The list of user settings.\n  repeated UserSetting settings = 1;\n\n  // A token that can be sent as `page_token` to retrieve the next page.\n  // If this field is omitted, there are no subsequent pages.\n  string next_page_token = 2;\n\n  // The total count of settings (may be approximate).\n  int32 total_size = 3;\n}\n\n// PersonalAccessToken represents a long-lived token for API/script access.\n// PATs are distinct from short-lived JWT access tokens used for session authentication.\nmessage PersonalAccessToken {\n  option (google.api.resource) = {\n    type: \"memos.api.v1/PersonalAccessToken\"\n    pattern: \"users/{user}/personalAccessTokens/{personal_access_token}\"\n    singular: \"personalAccessToken\"\n    plural: \"personalAccessTokens\"\n  };\n\n  // The resource name of the personal access token.\n  // Format: users/{user}/personalAccessTokens/{personal_access_token}\n  string name = 1 [(google.api.field_behavior) = IDENTIFIER];\n\n  // The description of the token.\n  string description = 2 [(google.api.field_behavior) = OPTIONAL];\n\n  // Output only. The creation timestamp.\n  google.protobuf.Timestamp created_at = 3 [(google.api.field_behavior) = OUTPUT_ONLY];\n\n  // Optional. The expiration timestamp.\n  google.protobuf.Timestamp expires_at = 4 [(google.api.field_behavior) = OPTIONAL];\n\n  // Output only. The last used timestamp.\n  google.protobuf.Timestamp last_used_at = 5 [(google.api.field_behavior) = OUTPUT_ONLY];\n}\n\nmessage ListPersonalAccessTokensRequest {\n  // Required. The parent resource whose personal access tokens will be listed.\n  // Format: users/{user}\n  string parent = 1 [\n    (google.api.field_behavior) = REQUIRED,\n    (google.api.resource_reference) = {type: \"memos.api.v1/User\"}\n  ];\n\n  // Optional. The maximum number of tokens to return.\n  int32 page_size = 2 [(google.api.field_behavior) = OPTIONAL];\n\n  // Optional. A page token for pagination.\n  string page_token = 3 [(google.api.field_behavior) = OPTIONAL];\n}\n\nmessage ListPersonalAccessTokensResponse {\n  // The list of personal access tokens.\n  repeated PersonalAccessToken personal_access_tokens = 1;\n\n  // A token for the next page of results.\n  string next_page_token = 2;\n\n  // The total count of personal access tokens.\n  int32 total_size = 3;\n}\n\nmessage CreatePersonalAccessTokenRequest {\n  // Required. The parent resource where this token will be created.\n  // Format: users/{user}\n  string parent = 1 [\n    (google.api.field_behavior) = REQUIRED,\n    (google.api.resource_reference) = {type: \"memos.api.v1/User\"}\n  ];\n\n  // Optional. Description of the personal access token.\n  string description = 2 [(google.api.field_behavior) = OPTIONAL];\n\n  // Optional. Expiration duration in days (0 = never expires).\n  int32 expires_in_days = 3 [(google.api.field_behavior) = OPTIONAL];\n}\n\nmessage CreatePersonalAccessTokenResponse {\n  // The personal access token metadata.\n  PersonalAccessToken personal_access_token = 1;\n\n  // The actual token value - only returned on creation.\n  // This is the only time the token value will be visible.\n  string token = 2;\n}\n\nmessage DeletePersonalAccessTokenRequest {\n  // Required. The resource name of the personal access token to delete.\n  // Format: users/{user}/personalAccessTokens/{personal_access_token}\n  string name = 1 [\n    (google.api.field_behavior) = REQUIRED,\n    (google.api.resource_reference) = {type: \"memos.api.v1/PersonalAccessToken\"}\n  ];\n}\n\n// UserWebhook represents a webhook owned by a user.\nmessage UserWebhook {\n  // The name of the webhook.\n  // Format: users/{user}/webhooks/{webhook}\n  string name = 1;\n\n  // The URL to send the webhook to.\n  string url = 2;\n\n  // Optional. Human-readable name for the webhook.\n  string display_name = 3;\n\n  // The creation time of the webhook.\n  google.protobuf.Timestamp create_time = 4 [(google.api.field_behavior) = OUTPUT_ONLY];\n\n  // The last update time of the webhook.\n  google.protobuf.Timestamp update_time = 5 [(google.api.field_behavior) = OUTPUT_ONLY];\n}\n\nmessage ListUserWebhooksRequest {\n  // The parent user resource.\n  // Format: users/{user}\n  string parent = 1 [(google.api.field_behavior) = REQUIRED];\n}\n\nmessage ListUserWebhooksResponse {\n  // The list of webhooks.\n  repeated UserWebhook webhooks = 1;\n}\n\nmessage CreateUserWebhookRequest {\n  // The parent user resource.\n  // Format: users/{user}\n  string parent = 1 [(google.api.field_behavior) = REQUIRED];\n\n  // The webhook to create.\n  UserWebhook webhook = 2 [(google.api.field_behavior) = REQUIRED];\n}\n\nmessage UpdateUserWebhookRequest {\n  // The webhook to update.\n  UserWebhook webhook = 1 [(google.api.field_behavior) = REQUIRED];\n\n  // The list of fields to update.\n  google.protobuf.FieldMask update_mask = 2;\n}\n\nmessage DeleteUserWebhookRequest {\n  // The name of the webhook to delete.\n  // Format: users/{user}/webhooks/{webhook}\n  string name = 1 [(google.api.field_behavior) = REQUIRED];\n}\n\nmessage UserNotification {\n  option (google.api.resource) = {\n    type: \"memos.api.v1/UserNotification\"\n    pattern: \"users/{user}/notifications/{notification}\"\n    name_field: \"name\"\n    singular: \"notification\"\n    plural: \"notifications\"\n  };\n\n  // The resource name of the notification.\n  // Format: users/{user}/notifications/{notification}\n  string name = 1 [\n    (google.api.field_behavior) = OUTPUT_ONLY,\n    (google.api.field_behavior) = IDENTIFIER\n  ];\n\n  // The sender of the notification.\n  // Format: users/{user}\n  string sender = 2 [\n    (google.api.field_behavior) = OUTPUT_ONLY,\n    (google.api.resource_reference) = {type: \"memos.api.v1/User\"}\n  ];\n\n  // The status of the notification.\n  Status status = 3 [(google.api.field_behavior) = OPTIONAL];\n\n  // The creation timestamp.\n  google.protobuf.Timestamp create_time = 4 [(google.api.field_behavior) = OUTPUT_ONLY];\n\n  // The type of the notification.\n  Type type = 5 [(google.api.field_behavior) = OUTPUT_ONLY];\n\n  oneof payload {\n    MemoCommentPayload memo_comment = 6 [(google.api.field_behavior) = OUTPUT_ONLY];\n  }\n\n  message MemoCommentPayload {\n    // The memo name of comment.\n    // Format: memos/{memo}\n    string memo = 1;\n\n    // The name of related memo.\n    // Format: memos/{memo}\n    string related_memo = 2;\n  }\n\n  enum Status {\n    STATUS_UNSPECIFIED = 0;\n    UNREAD = 1;\n    ARCHIVED = 2;\n  }\n\n  enum Type {\n    TYPE_UNSPECIFIED = 0;\n    MEMO_COMMENT = 1;\n  }\n}\n\nmessage ListUserNotificationsRequest {\n  // The parent user resource.\n  // Format: users/{user}\n  string parent = 1 [\n    (google.api.field_behavior) = REQUIRED,\n    (google.api.resource_reference) = {type: \"memos.api.v1/User\"}\n  ];\n\n  int32 page_size = 2 [(google.api.field_behavior) = OPTIONAL];\n  string page_token = 3 [(google.api.field_behavior) = OPTIONAL];\n  string filter = 4 [(google.api.field_behavior) = OPTIONAL];\n}\n\nmessage ListUserNotificationsResponse {\n  repeated UserNotification notifications = 1;\n  string next_page_token = 2;\n}\n\nmessage UpdateUserNotificationRequest {\n  UserNotification notification = 1 [(google.api.field_behavior) = REQUIRED];\n  google.protobuf.FieldMask update_mask = 2 [(google.api.field_behavior) = REQUIRED];\n}\n\nmessage DeleteUserNotificationRequest {\n  // Format: users/{user}/notifications/{notification}\n  string name = 1 [\n    (google.api.field_behavior) = REQUIRED,\n    (google.api.resource_reference) = {type: \"memos.api.v1/UserNotification\"}\n  ];\n}\n"
  },
  {
    "path": "proto/buf.gen.yaml",
    "content": "version: v2\nmanaged:\n  enabled: true\n  disable:\n    - file_option: go_package\n      module: buf.build/googleapis/googleapis\n  override:\n    - file_option: go_package_prefix\n      value: github.com/usememos/memos/proto/gen\nplugins:\n  - remote: buf.build/protocolbuffers/go\n    out: gen\n    opt: paths=source_relative\n  - remote: buf.build/grpc/go\n    out: gen\n    opt: paths=source_relative\n  - remote: buf.build/connectrpc/go\n    out: gen\n    opt: paths=source_relative\n  - remote: buf.build/grpc-ecosystem/gateway\n    out: gen\n    opt: paths=source_relative\n  - remote: buf.build/community/google-gnostic-openapi\n    out: gen\n    opt: enum_type=string\n  - remote: buf.build/bufbuild/es\n    out: ../web/src/types/proto\n    opt:\n      - target=ts\n    include_imports: true\n"
  },
  {
    "path": "proto/buf.yaml",
    "content": "version: v2\ndeps:\n  - buf.build/googleapis/googleapis\nlint:\n  use:\n    - BASIC\n  except:\n    - ENUM_VALUE_PREFIX\n    - FIELD_NOT_REQUIRED\n    - PACKAGE_DIRECTORY_MATCH\n    - PACKAGE_NO_IMPORT_CYCLE\n    - PACKAGE_VERSION_SUFFIX\n  disallow_comment_ignores: true\nbreaking:\n  use:\n    - FILE\n  except:\n    - EXTENSION_NO_DELETE\n    - FIELD_SAME_DEFAULT\n"
  },
  {
    "path": "proto/gen/api/v1/apiv1connect/attachment_service.connect.go",
    "content": "// Code generated by protoc-gen-connect-go. DO NOT EDIT.\n//\n// Source: api/v1/attachment_service.proto\n\npackage apiv1connect\n\nimport (\n\tconnect \"connectrpc.com/connect\"\n\tcontext \"context\"\n\terrors \"errors\"\n\tv1 \"github.com/usememos/memos/proto/gen/api/v1\"\n\temptypb \"google.golang.org/protobuf/types/known/emptypb\"\n\thttp \"net/http\"\n\tstrings \"strings\"\n)\n\n// This is a compile-time assertion to ensure that this generated file and the connect package are\n// compatible. If you get a compiler error that this constant is not defined, this code was\n// generated with a version of connect newer than the one compiled into your binary. You can fix the\n// problem by either regenerating this code with an older version of connect or updating the connect\n// version compiled into your binary.\nconst _ = connect.IsAtLeastVersion1_13_0\n\nconst (\n\t// AttachmentServiceName is the fully-qualified name of the AttachmentService service.\n\tAttachmentServiceName = \"memos.api.v1.AttachmentService\"\n)\n\n// These constants are the fully-qualified names of the RPCs defined in this package. They're\n// exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route.\n//\n// Note that these are different from the fully-qualified method names used by\n// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to\n// reflection-formatted method names, remove the leading slash and convert the remaining slash to a\n// period.\nconst (\n\t// AttachmentServiceCreateAttachmentProcedure is the fully-qualified name of the AttachmentService's\n\t// CreateAttachment RPC.\n\tAttachmentServiceCreateAttachmentProcedure = \"/memos.api.v1.AttachmentService/CreateAttachment\"\n\t// AttachmentServiceListAttachmentsProcedure is the fully-qualified name of the AttachmentService's\n\t// ListAttachments RPC.\n\tAttachmentServiceListAttachmentsProcedure = \"/memos.api.v1.AttachmentService/ListAttachments\"\n\t// AttachmentServiceGetAttachmentProcedure is the fully-qualified name of the AttachmentService's\n\t// GetAttachment RPC.\n\tAttachmentServiceGetAttachmentProcedure = \"/memos.api.v1.AttachmentService/GetAttachment\"\n\t// AttachmentServiceUpdateAttachmentProcedure is the fully-qualified name of the AttachmentService's\n\t// UpdateAttachment RPC.\n\tAttachmentServiceUpdateAttachmentProcedure = \"/memos.api.v1.AttachmentService/UpdateAttachment\"\n\t// AttachmentServiceDeleteAttachmentProcedure is the fully-qualified name of the AttachmentService's\n\t// DeleteAttachment RPC.\n\tAttachmentServiceDeleteAttachmentProcedure = \"/memos.api.v1.AttachmentService/DeleteAttachment\"\n)\n\n// AttachmentServiceClient is a client for the memos.api.v1.AttachmentService service.\ntype AttachmentServiceClient interface {\n\t// CreateAttachment creates a new attachment.\n\tCreateAttachment(context.Context, *connect.Request[v1.CreateAttachmentRequest]) (*connect.Response[v1.Attachment], error)\n\t// ListAttachments lists all attachments.\n\tListAttachments(context.Context, *connect.Request[v1.ListAttachmentsRequest]) (*connect.Response[v1.ListAttachmentsResponse], error)\n\t// GetAttachment returns an attachment by name.\n\tGetAttachment(context.Context, *connect.Request[v1.GetAttachmentRequest]) (*connect.Response[v1.Attachment], error)\n\t// UpdateAttachment updates an attachment.\n\tUpdateAttachment(context.Context, *connect.Request[v1.UpdateAttachmentRequest]) (*connect.Response[v1.Attachment], error)\n\t// DeleteAttachment deletes an attachment by name.\n\tDeleteAttachment(context.Context, *connect.Request[v1.DeleteAttachmentRequest]) (*connect.Response[emptypb.Empty], error)\n}\n\n// NewAttachmentServiceClient constructs a client for the memos.api.v1.AttachmentService service. By\n// default, it uses the Connect protocol with the binary Protobuf Codec, asks for gzipped responses,\n// and sends uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the\n// connect.WithGRPC() or connect.WithGRPCWeb() options.\n//\n// The URL supplied here should be the base URL for the Connect or gRPC server (for example,\n// http://api.acme.com or https://acme.com/grpc).\nfunc NewAttachmentServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) AttachmentServiceClient {\n\tbaseURL = strings.TrimRight(baseURL, \"/\")\n\tattachmentServiceMethods := v1.File_api_v1_attachment_service_proto.Services().ByName(\"AttachmentService\").Methods()\n\treturn &attachmentServiceClient{\n\t\tcreateAttachment: connect.NewClient[v1.CreateAttachmentRequest, v1.Attachment](\n\t\t\thttpClient,\n\t\t\tbaseURL+AttachmentServiceCreateAttachmentProcedure,\n\t\t\tconnect.WithSchema(attachmentServiceMethods.ByName(\"CreateAttachment\")),\n\t\t\tconnect.WithClientOptions(opts...),\n\t\t),\n\t\tlistAttachments: connect.NewClient[v1.ListAttachmentsRequest, v1.ListAttachmentsResponse](\n\t\t\thttpClient,\n\t\t\tbaseURL+AttachmentServiceListAttachmentsProcedure,\n\t\t\tconnect.WithSchema(attachmentServiceMethods.ByName(\"ListAttachments\")),\n\t\t\tconnect.WithClientOptions(opts...),\n\t\t),\n\t\tgetAttachment: connect.NewClient[v1.GetAttachmentRequest, v1.Attachment](\n\t\t\thttpClient,\n\t\t\tbaseURL+AttachmentServiceGetAttachmentProcedure,\n\t\t\tconnect.WithSchema(attachmentServiceMethods.ByName(\"GetAttachment\")),\n\t\t\tconnect.WithClientOptions(opts...),\n\t\t),\n\t\tupdateAttachment: connect.NewClient[v1.UpdateAttachmentRequest, v1.Attachment](\n\t\t\thttpClient,\n\t\t\tbaseURL+AttachmentServiceUpdateAttachmentProcedure,\n\t\t\tconnect.WithSchema(attachmentServiceMethods.ByName(\"UpdateAttachment\")),\n\t\t\tconnect.WithClientOptions(opts...),\n\t\t),\n\t\tdeleteAttachment: connect.NewClient[v1.DeleteAttachmentRequest, emptypb.Empty](\n\t\t\thttpClient,\n\t\t\tbaseURL+AttachmentServiceDeleteAttachmentProcedure,\n\t\t\tconnect.WithSchema(attachmentServiceMethods.ByName(\"DeleteAttachment\")),\n\t\t\tconnect.WithClientOptions(opts...),\n\t\t),\n\t}\n}\n\n// attachmentServiceClient implements AttachmentServiceClient.\ntype attachmentServiceClient struct {\n\tcreateAttachment *connect.Client[v1.CreateAttachmentRequest, v1.Attachment]\n\tlistAttachments  *connect.Client[v1.ListAttachmentsRequest, v1.ListAttachmentsResponse]\n\tgetAttachment    *connect.Client[v1.GetAttachmentRequest, v1.Attachment]\n\tupdateAttachment *connect.Client[v1.UpdateAttachmentRequest, v1.Attachment]\n\tdeleteAttachment *connect.Client[v1.DeleteAttachmentRequest, emptypb.Empty]\n}\n\n// CreateAttachment calls memos.api.v1.AttachmentService.CreateAttachment.\nfunc (c *attachmentServiceClient) CreateAttachment(ctx context.Context, req *connect.Request[v1.CreateAttachmentRequest]) (*connect.Response[v1.Attachment], error) {\n\treturn c.createAttachment.CallUnary(ctx, req)\n}\n\n// ListAttachments calls memos.api.v1.AttachmentService.ListAttachments.\nfunc (c *attachmentServiceClient) ListAttachments(ctx context.Context, req *connect.Request[v1.ListAttachmentsRequest]) (*connect.Response[v1.ListAttachmentsResponse], error) {\n\treturn c.listAttachments.CallUnary(ctx, req)\n}\n\n// GetAttachment calls memos.api.v1.AttachmentService.GetAttachment.\nfunc (c *attachmentServiceClient) GetAttachment(ctx context.Context, req *connect.Request[v1.GetAttachmentRequest]) (*connect.Response[v1.Attachment], error) {\n\treturn c.getAttachment.CallUnary(ctx, req)\n}\n\n// UpdateAttachment calls memos.api.v1.AttachmentService.UpdateAttachment.\nfunc (c *attachmentServiceClient) UpdateAttachment(ctx context.Context, req *connect.Request[v1.UpdateAttachmentRequest]) (*connect.Response[v1.Attachment], error) {\n\treturn c.updateAttachment.CallUnary(ctx, req)\n}\n\n// DeleteAttachment calls memos.api.v1.AttachmentService.DeleteAttachment.\nfunc (c *attachmentServiceClient) DeleteAttachment(ctx context.Context, req *connect.Request[v1.DeleteAttachmentRequest]) (*connect.Response[emptypb.Empty], error) {\n\treturn c.deleteAttachment.CallUnary(ctx, req)\n}\n\n// AttachmentServiceHandler is an implementation of the memos.api.v1.AttachmentService service.\ntype AttachmentServiceHandler interface {\n\t// CreateAttachment creates a new attachment.\n\tCreateAttachment(context.Context, *connect.Request[v1.CreateAttachmentRequest]) (*connect.Response[v1.Attachment], error)\n\t// ListAttachments lists all attachments.\n\tListAttachments(context.Context, *connect.Request[v1.ListAttachmentsRequest]) (*connect.Response[v1.ListAttachmentsResponse], error)\n\t// GetAttachment returns an attachment by name.\n\tGetAttachment(context.Context, *connect.Request[v1.GetAttachmentRequest]) (*connect.Response[v1.Attachment], error)\n\t// UpdateAttachment updates an attachment.\n\tUpdateAttachment(context.Context, *connect.Request[v1.UpdateAttachmentRequest]) (*connect.Response[v1.Attachment], error)\n\t// DeleteAttachment deletes an attachment by name.\n\tDeleteAttachment(context.Context, *connect.Request[v1.DeleteAttachmentRequest]) (*connect.Response[emptypb.Empty], error)\n}\n\n// NewAttachmentServiceHandler builds an HTTP handler from the service implementation. It returns\n// the path on which to mount the handler and the handler itself.\n//\n// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf\n// and JSON codecs. They also support gzip compression.\nfunc NewAttachmentServiceHandler(svc AttachmentServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) {\n\tattachmentServiceMethods := v1.File_api_v1_attachment_service_proto.Services().ByName(\"AttachmentService\").Methods()\n\tattachmentServiceCreateAttachmentHandler := connect.NewUnaryHandler(\n\t\tAttachmentServiceCreateAttachmentProcedure,\n\t\tsvc.CreateAttachment,\n\t\tconnect.WithSchema(attachmentServiceMethods.ByName(\"CreateAttachment\")),\n\t\tconnect.WithHandlerOptions(opts...),\n\t)\n\tattachmentServiceListAttachmentsHandler := connect.NewUnaryHandler(\n\t\tAttachmentServiceListAttachmentsProcedure,\n\t\tsvc.ListAttachments,\n\t\tconnect.WithSchema(attachmentServiceMethods.ByName(\"ListAttachments\")),\n\t\tconnect.WithHandlerOptions(opts...),\n\t)\n\tattachmentServiceGetAttachmentHandler := connect.NewUnaryHandler(\n\t\tAttachmentServiceGetAttachmentProcedure,\n\t\tsvc.GetAttachment,\n\t\tconnect.WithSchema(attachmentServiceMethods.ByName(\"GetAttachment\")),\n\t\tconnect.WithHandlerOptions(opts...),\n\t)\n\tattachmentServiceUpdateAttachmentHandler := connect.NewUnaryHandler(\n\t\tAttachmentServiceUpdateAttachmentProcedure,\n\t\tsvc.UpdateAttachment,\n\t\tconnect.WithSchema(attachmentServiceMethods.ByName(\"UpdateAttachment\")),\n\t\tconnect.WithHandlerOptions(opts...),\n\t)\n\tattachmentServiceDeleteAttachmentHandler := connect.NewUnaryHandler(\n\t\tAttachmentServiceDeleteAttachmentProcedure,\n\t\tsvc.DeleteAttachment,\n\t\tconnect.WithSchema(attachmentServiceMethods.ByName(\"DeleteAttachment\")),\n\t\tconnect.WithHandlerOptions(opts...),\n\t)\n\treturn \"/memos.api.v1.AttachmentService/\", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tswitch r.URL.Path {\n\t\tcase AttachmentServiceCreateAttachmentProcedure:\n\t\t\tattachmentServiceCreateAttachmentHandler.ServeHTTP(w, r)\n\t\tcase AttachmentServiceListAttachmentsProcedure:\n\t\t\tattachmentServiceListAttachmentsHandler.ServeHTTP(w, r)\n\t\tcase AttachmentServiceGetAttachmentProcedure:\n\t\t\tattachmentServiceGetAttachmentHandler.ServeHTTP(w, r)\n\t\tcase AttachmentServiceUpdateAttachmentProcedure:\n\t\t\tattachmentServiceUpdateAttachmentHandler.ServeHTTP(w, r)\n\t\tcase AttachmentServiceDeleteAttachmentProcedure:\n\t\t\tattachmentServiceDeleteAttachmentHandler.ServeHTTP(w, r)\n\t\tdefault:\n\t\t\thttp.NotFound(w, r)\n\t\t}\n\t})\n}\n\n// UnimplementedAttachmentServiceHandler returns CodeUnimplemented from all methods.\ntype UnimplementedAttachmentServiceHandler struct{}\n\nfunc (UnimplementedAttachmentServiceHandler) CreateAttachment(context.Context, *connect.Request[v1.CreateAttachmentRequest]) (*connect.Response[v1.Attachment], error) {\n\treturn nil, connect.NewError(connect.CodeUnimplemented, errors.New(\"memos.api.v1.AttachmentService.CreateAttachment is not implemented\"))\n}\n\nfunc (UnimplementedAttachmentServiceHandler) ListAttachments(context.Context, *connect.Request[v1.ListAttachmentsRequest]) (*connect.Response[v1.ListAttachmentsResponse], error) {\n\treturn nil, connect.NewError(connect.CodeUnimplemented, errors.New(\"memos.api.v1.AttachmentService.ListAttachments is not implemented\"))\n}\n\nfunc (UnimplementedAttachmentServiceHandler) GetAttachment(context.Context, *connect.Request[v1.GetAttachmentRequest]) (*connect.Response[v1.Attachment], error) {\n\treturn nil, connect.NewError(connect.CodeUnimplemented, errors.New(\"memos.api.v1.AttachmentService.GetAttachment is not implemented\"))\n}\n\nfunc (UnimplementedAttachmentServiceHandler) UpdateAttachment(context.Context, *connect.Request[v1.UpdateAttachmentRequest]) (*connect.Response[v1.Attachment], error) {\n\treturn nil, connect.NewError(connect.CodeUnimplemented, errors.New(\"memos.api.v1.AttachmentService.UpdateAttachment is not implemented\"))\n}\n\nfunc (UnimplementedAttachmentServiceHandler) DeleteAttachment(context.Context, *connect.Request[v1.DeleteAttachmentRequest]) (*connect.Response[emptypb.Empty], error) {\n\treturn nil, connect.NewError(connect.CodeUnimplemented, errors.New(\"memos.api.v1.AttachmentService.DeleteAttachment is not implemented\"))\n}\n"
  },
  {
    "path": "proto/gen/api/v1/apiv1connect/auth_service.connect.go",
    "content": "// Code generated by protoc-gen-connect-go. DO NOT EDIT.\n//\n// Source: api/v1/auth_service.proto\n\npackage apiv1connect\n\nimport (\n\tconnect \"connectrpc.com/connect\"\n\tcontext \"context\"\n\terrors \"errors\"\n\tv1 \"github.com/usememos/memos/proto/gen/api/v1\"\n\temptypb \"google.golang.org/protobuf/types/known/emptypb\"\n\thttp \"net/http\"\n\tstrings \"strings\"\n)\n\n// This is a compile-time assertion to ensure that this generated file and the connect package are\n// compatible. If you get a compiler error that this constant is not defined, this code was\n// generated with a version of connect newer than the one compiled into your binary. You can fix the\n// problem by either regenerating this code with an older version of connect or updating the connect\n// version compiled into your binary.\nconst _ = connect.IsAtLeastVersion1_13_0\n\nconst (\n\t// AuthServiceName is the fully-qualified name of the AuthService service.\n\tAuthServiceName = \"memos.api.v1.AuthService\"\n)\n\n// These constants are the fully-qualified names of the RPCs defined in this package. They're\n// exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route.\n//\n// Note that these are different from the fully-qualified method names used by\n// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to\n// reflection-formatted method names, remove the leading slash and convert the remaining slash to a\n// period.\nconst (\n\t// AuthServiceGetCurrentUserProcedure is the fully-qualified name of the AuthService's\n\t// GetCurrentUser RPC.\n\tAuthServiceGetCurrentUserProcedure = \"/memos.api.v1.AuthService/GetCurrentUser\"\n\t// AuthServiceSignInProcedure is the fully-qualified name of the AuthService's SignIn RPC.\n\tAuthServiceSignInProcedure = \"/memos.api.v1.AuthService/SignIn\"\n\t// AuthServiceSignOutProcedure is the fully-qualified name of the AuthService's SignOut RPC.\n\tAuthServiceSignOutProcedure = \"/memos.api.v1.AuthService/SignOut\"\n\t// AuthServiceRefreshTokenProcedure is the fully-qualified name of the AuthService's RefreshToken\n\t// RPC.\n\tAuthServiceRefreshTokenProcedure = \"/memos.api.v1.AuthService/RefreshToken\"\n)\n\n// AuthServiceClient is a client for the memos.api.v1.AuthService service.\ntype AuthServiceClient interface {\n\t// GetCurrentUser returns the authenticated user's information.\n\t// Validates the access token and returns user details.\n\t// Similar to OIDC's /userinfo endpoint.\n\tGetCurrentUser(context.Context, *connect.Request[v1.GetCurrentUserRequest]) (*connect.Response[v1.GetCurrentUserResponse], error)\n\t// SignIn authenticates a user with credentials and returns tokens.\n\t// On success, returns an access token and sets a refresh token cookie.\n\t// Supports password-based and SSO authentication methods.\n\tSignIn(context.Context, *connect.Request[v1.SignInRequest]) (*connect.Response[v1.SignInResponse], error)\n\t// SignOut terminates the user's authentication.\n\t// Revokes the refresh token and clears the authentication cookie.\n\tSignOut(context.Context, *connect.Request[v1.SignOutRequest]) (*connect.Response[emptypb.Empty], error)\n\t// RefreshToken exchanges a valid refresh token for a new access token.\n\t// The refresh token is read from the HttpOnly cookie.\n\t// Returns a new short-lived access token.\n\tRefreshToken(context.Context, *connect.Request[v1.RefreshTokenRequest]) (*connect.Response[v1.RefreshTokenResponse], error)\n}\n\n// NewAuthServiceClient constructs a client for the memos.api.v1.AuthService service. By default, it\n// uses the Connect protocol with the binary Protobuf Codec, asks for gzipped responses, and sends\n// uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the connect.WithGRPC() or\n// connect.WithGRPCWeb() options.\n//\n// The URL supplied here should be the base URL for the Connect or gRPC server (for example,\n// http://api.acme.com or https://acme.com/grpc).\nfunc NewAuthServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) AuthServiceClient {\n\tbaseURL = strings.TrimRight(baseURL, \"/\")\n\tauthServiceMethods := v1.File_api_v1_auth_service_proto.Services().ByName(\"AuthService\").Methods()\n\treturn &authServiceClient{\n\t\tgetCurrentUser: connect.NewClient[v1.GetCurrentUserRequest, v1.GetCurrentUserResponse](\n\t\t\thttpClient,\n\t\t\tbaseURL+AuthServiceGetCurrentUserProcedure,\n\t\t\tconnect.WithSchema(authServiceMethods.ByName(\"GetCurrentUser\")),\n\t\t\tconnect.WithClientOptions(opts...),\n\t\t),\n\t\tsignIn: connect.NewClient[v1.SignInRequest, v1.SignInResponse](\n\t\t\thttpClient,\n\t\t\tbaseURL+AuthServiceSignInProcedure,\n\t\t\tconnect.WithSchema(authServiceMethods.ByName(\"SignIn\")),\n\t\t\tconnect.WithClientOptions(opts...),\n\t\t),\n\t\tsignOut: connect.NewClient[v1.SignOutRequest, emptypb.Empty](\n\t\t\thttpClient,\n\t\t\tbaseURL+AuthServiceSignOutProcedure,\n\t\t\tconnect.WithSchema(authServiceMethods.ByName(\"SignOut\")),\n\t\t\tconnect.WithClientOptions(opts...),\n\t\t),\n\t\trefreshToken: connect.NewClient[v1.RefreshTokenRequest, v1.RefreshTokenResponse](\n\t\t\thttpClient,\n\t\t\tbaseURL+AuthServiceRefreshTokenProcedure,\n\t\t\tconnect.WithSchema(authServiceMethods.ByName(\"RefreshToken\")),\n\t\t\tconnect.WithClientOptions(opts...),\n\t\t),\n\t}\n}\n\n// authServiceClient implements AuthServiceClient.\ntype authServiceClient struct {\n\tgetCurrentUser *connect.Client[v1.GetCurrentUserRequest, v1.GetCurrentUserResponse]\n\tsignIn         *connect.Client[v1.SignInRequest, v1.SignInResponse]\n\tsignOut        *connect.Client[v1.SignOutRequest, emptypb.Empty]\n\trefreshToken   *connect.Client[v1.RefreshTokenRequest, v1.RefreshTokenResponse]\n}\n\n// GetCurrentUser calls memos.api.v1.AuthService.GetCurrentUser.\nfunc (c *authServiceClient) GetCurrentUser(ctx context.Context, req *connect.Request[v1.GetCurrentUserRequest]) (*connect.Response[v1.GetCurrentUserResponse], error) {\n\treturn c.getCurrentUser.CallUnary(ctx, req)\n}\n\n// SignIn calls memos.api.v1.AuthService.SignIn.\nfunc (c *authServiceClient) SignIn(ctx context.Context, req *connect.Request[v1.SignInRequest]) (*connect.Response[v1.SignInResponse], error) {\n\treturn c.signIn.CallUnary(ctx, req)\n}\n\n// SignOut calls memos.api.v1.AuthService.SignOut.\nfunc (c *authServiceClient) SignOut(ctx context.Context, req *connect.Request[v1.SignOutRequest]) (*connect.Response[emptypb.Empty], error) {\n\treturn c.signOut.CallUnary(ctx, req)\n}\n\n// RefreshToken calls memos.api.v1.AuthService.RefreshToken.\nfunc (c *authServiceClient) RefreshToken(ctx context.Context, req *connect.Request[v1.RefreshTokenRequest]) (*connect.Response[v1.RefreshTokenResponse], error) {\n\treturn c.refreshToken.CallUnary(ctx, req)\n}\n\n// AuthServiceHandler is an implementation of the memos.api.v1.AuthService service.\ntype AuthServiceHandler interface {\n\t// GetCurrentUser returns the authenticated user's information.\n\t// Validates the access token and returns user details.\n\t// Similar to OIDC's /userinfo endpoint.\n\tGetCurrentUser(context.Context, *connect.Request[v1.GetCurrentUserRequest]) (*connect.Response[v1.GetCurrentUserResponse], error)\n\t// SignIn authenticates a user with credentials and returns tokens.\n\t// On success, returns an access token and sets a refresh token cookie.\n\t// Supports password-based and SSO authentication methods.\n\tSignIn(context.Context, *connect.Request[v1.SignInRequest]) (*connect.Response[v1.SignInResponse], error)\n\t// SignOut terminates the user's authentication.\n\t// Revokes the refresh token and clears the authentication cookie.\n\tSignOut(context.Context, *connect.Request[v1.SignOutRequest]) (*connect.Response[emptypb.Empty], error)\n\t// RefreshToken exchanges a valid refresh token for a new access token.\n\t// The refresh token is read from the HttpOnly cookie.\n\t// Returns a new short-lived access token.\n\tRefreshToken(context.Context, *connect.Request[v1.RefreshTokenRequest]) (*connect.Response[v1.RefreshTokenResponse], error)\n}\n\n// NewAuthServiceHandler builds an HTTP handler from the service implementation. It returns the path\n// on which to mount the handler and the handler itself.\n//\n// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf\n// and JSON codecs. They also support gzip compression.\nfunc NewAuthServiceHandler(svc AuthServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) {\n\tauthServiceMethods := v1.File_api_v1_auth_service_proto.Services().ByName(\"AuthService\").Methods()\n\tauthServiceGetCurrentUserHandler := connect.NewUnaryHandler(\n\t\tAuthServiceGetCurrentUserProcedure,\n\t\tsvc.GetCurrentUser,\n\t\tconnect.WithSchema(authServiceMethods.ByName(\"GetCurrentUser\")),\n\t\tconnect.WithHandlerOptions(opts...),\n\t)\n\tauthServiceSignInHandler := connect.NewUnaryHandler(\n\t\tAuthServiceSignInProcedure,\n\t\tsvc.SignIn,\n\t\tconnect.WithSchema(authServiceMethods.ByName(\"SignIn\")),\n\t\tconnect.WithHandlerOptions(opts...),\n\t)\n\tauthServiceSignOutHandler := connect.NewUnaryHandler(\n\t\tAuthServiceSignOutProcedure,\n\t\tsvc.SignOut,\n\t\tconnect.WithSchema(authServiceMethods.ByName(\"SignOut\")),\n\t\tconnect.WithHandlerOptions(opts...),\n\t)\n\tauthServiceRefreshTokenHandler := connect.NewUnaryHandler(\n\t\tAuthServiceRefreshTokenProcedure,\n\t\tsvc.RefreshToken,\n\t\tconnect.WithSchema(authServiceMethods.ByName(\"RefreshToken\")),\n\t\tconnect.WithHandlerOptions(opts...),\n\t)\n\treturn \"/memos.api.v1.AuthService/\", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tswitch r.URL.Path {\n\t\tcase AuthServiceGetCurrentUserProcedure:\n\t\t\tauthServiceGetCurrentUserHandler.ServeHTTP(w, r)\n\t\tcase AuthServiceSignInProcedure:\n\t\t\tauthServiceSignInHandler.ServeHTTP(w, r)\n\t\tcase AuthServiceSignOutProcedure:\n\t\t\tauthServiceSignOutHandler.ServeHTTP(w, r)\n\t\tcase AuthServiceRefreshTokenProcedure:\n\t\t\tauthServiceRefreshTokenHandler.ServeHTTP(w, r)\n\t\tdefault:\n\t\t\thttp.NotFound(w, r)\n\t\t}\n\t})\n}\n\n// UnimplementedAuthServiceHandler returns CodeUnimplemented from all methods.\ntype UnimplementedAuthServiceHandler struct{}\n\nfunc (UnimplementedAuthServiceHandler) GetCurrentUser(context.Context, *connect.Request[v1.GetCurrentUserRequest]) (*connect.Response[v1.GetCurrentUserResponse], error) {\n\treturn nil, connect.NewError(connect.CodeUnimplemented, errors.New(\"memos.api.v1.AuthService.GetCurrentUser is not implemented\"))\n}\n\nfunc (UnimplementedAuthServiceHandler) SignIn(context.Context, *connect.Request[v1.SignInRequest]) (*connect.Response[v1.SignInResponse], error) {\n\treturn nil, connect.NewError(connect.CodeUnimplemented, errors.New(\"memos.api.v1.AuthService.SignIn is not implemented\"))\n}\n\nfunc (UnimplementedAuthServiceHandler) SignOut(context.Context, *connect.Request[v1.SignOutRequest]) (*connect.Response[emptypb.Empty], error) {\n\treturn nil, connect.NewError(connect.CodeUnimplemented, errors.New(\"memos.api.v1.AuthService.SignOut is not implemented\"))\n}\n\nfunc (UnimplementedAuthServiceHandler) RefreshToken(context.Context, *connect.Request[v1.RefreshTokenRequest]) (*connect.Response[v1.RefreshTokenResponse], error) {\n\treturn nil, connect.NewError(connect.CodeUnimplemented, errors.New(\"memos.api.v1.AuthService.RefreshToken is not implemented\"))\n}\n"
  },
  {
    "path": "proto/gen/api/v1/apiv1connect/idp_service.connect.go",
    "content": "// Code generated by protoc-gen-connect-go. DO NOT EDIT.\n//\n// Source: api/v1/idp_service.proto\n\npackage apiv1connect\n\nimport (\n\tconnect \"connectrpc.com/connect\"\n\tcontext \"context\"\n\terrors \"errors\"\n\tv1 \"github.com/usememos/memos/proto/gen/api/v1\"\n\temptypb \"google.golang.org/protobuf/types/known/emptypb\"\n\thttp \"net/http\"\n\tstrings \"strings\"\n)\n\n// This is a compile-time assertion to ensure that this generated file and the connect package are\n// compatible. If you get a compiler error that this constant is not defined, this code was\n// generated with a version of connect newer than the one compiled into your binary. You can fix the\n// problem by either regenerating this code with an older version of connect or updating the connect\n// version compiled into your binary.\nconst _ = connect.IsAtLeastVersion1_13_0\n\nconst (\n\t// IdentityProviderServiceName is the fully-qualified name of the IdentityProviderService service.\n\tIdentityProviderServiceName = \"memos.api.v1.IdentityProviderService\"\n)\n\n// These constants are the fully-qualified names of the RPCs defined in this package. They're\n// exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route.\n//\n// Note that these are different from the fully-qualified method names used by\n// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to\n// reflection-formatted method names, remove the leading slash and convert the remaining slash to a\n// period.\nconst (\n\t// IdentityProviderServiceListIdentityProvidersProcedure is the fully-qualified name of the\n\t// IdentityProviderService's ListIdentityProviders RPC.\n\tIdentityProviderServiceListIdentityProvidersProcedure = \"/memos.api.v1.IdentityProviderService/ListIdentityProviders\"\n\t// IdentityProviderServiceGetIdentityProviderProcedure is the fully-qualified name of the\n\t// IdentityProviderService's GetIdentityProvider RPC.\n\tIdentityProviderServiceGetIdentityProviderProcedure = \"/memos.api.v1.IdentityProviderService/GetIdentityProvider\"\n\t// IdentityProviderServiceCreateIdentityProviderProcedure is the fully-qualified name of the\n\t// IdentityProviderService's CreateIdentityProvider RPC.\n\tIdentityProviderServiceCreateIdentityProviderProcedure = \"/memos.api.v1.IdentityProviderService/CreateIdentityProvider\"\n\t// IdentityProviderServiceUpdateIdentityProviderProcedure is the fully-qualified name of the\n\t// IdentityProviderService's UpdateIdentityProvider RPC.\n\tIdentityProviderServiceUpdateIdentityProviderProcedure = \"/memos.api.v1.IdentityProviderService/UpdateIdentityProvider\"\n\t// IdentityProviderServiceDeleteIdentityProviderProcedure is the fully-qualified name of the\n\t// IdentityProviderService's DeleteIdentityProvider RPC.\n\tIdentityProviderServiceDeleteIdentityProviderProcedure = \"/memos.api.v1.IdentityProviderService/DeleteIdentityProvider\"\n)\n\n// IdentityProviderServiceClient is a client for the memos.api.v1.IdentityProviderService service.\ntype IdentityProviderServiceClient interface {\n\t// ListIdentityProviders lists identity providers.\n\tListIdentityProviders(context.Context, *connect.Request[v1.ListIdentityProvidersRequest]) (*connect.Response[v1.ListIdentityProvidersResponse], error)\n\t// GetIdentityProvider gets an identity provider.\n\tGetIdentityProvider(context.Context, *connect.Request[v1.GetIdentityProviderRequest]) (*connect.Response[v1.IdentityProvider], error)\n\t// CreateIdentityProvider creates an identity provider.\n\tCreateIdentityProvider(context.Context, *connect.Request[v1.CreateIdentityProviderRequest]) (*connect.Response[v1.IdentityProvider], error)\n\t// UpdateIdentityProvider updates an identity provider.\n\tUpdateIdentityProvider(context.Context, *connect.Request[v1.UpdateIdentityProviderRequest]) (*connect.Response[v1.IdentityProvider], error)\n\t// DeleteIdentityProvider deletes an identity provider.\n\tDeleteIdentityProvider(context.Context, *connect.Request[v1.DeleteIdentityProviderRequest]) (*connect.Response[emptypb.Empty], error)\n}\n\n// NewIdentityProviderServiceClient constructs a client for the memos.api.v1.IdentityProviderService\n// service. By default, it uses the Connect protocol with the binary Protobuf Codec, asks for\n// gzipped responses, and sends uncompressed requests. To use the gRPC or gRPC-Web protocols, supply\n// the connect.WithGRPC() or connect.WithGRPCWeb() options.\n//\n// The URL supplied here should be the base URL for the Connect or gRPC server (for example,\n// http://api.acme.com or https://acme.com/grpc).\nfunc NewIdentityProviderServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) IdentityProviderServiceClient {\n\tbaseURL = strings.TrimRight(baseURL, \"/\")\n\tidentityProviderServiceMethods := v1.File_api_v1_idp_service_proto.Services().ByName(\"IdentityProviderService\").Methods()\n\treturn &identityProviderServiceClient{\n\t\tlistIdentityProviders: connect.NewClient[v1.ListIdentityProvidersRequest, v1.ListIdentityProvidersResponse](\n\t\t\thttpClient,\n\t\t\tbaseURL+IdentityProviderServiceListIdentityProvidersProcedure,\n\t\t\tconnect.WithSchema(identityProviderServiceMethods.ByName(\"ListIdentityProviders\")),\n\t\t\tconnect.WithClientOptions(opts...),\n\t\t),\n\t\tgetIdentityProvider: connect.NewClient[v1.GetIdentityProviderRequest, v1.IdentityProvider](\n\t\t\thttpClient,\n\t\t\tbaseURL+IdentityProviderServiceGetIdentityProviderProcedure,\n\t\t\tconnect.WithSchema(identityProviderServiceMethods.ByName(\"GetIdentityProvider\")),\n\t\t\tconnect.WithClientOptions(opts...),\n\t\t),\n\t\tcreateIdentityProvider: connect.NewClient[v1.CreateIdentityProviderRequest, v1.IdentityProvider](\n\t\t\thttpClient,\n\t\t\tbaseURL+IdentityProviderServiceCreateIdentityProviderProcedure,\n\t\t\tconnect.WithSchema(identityProviderServiceMethods.ByName(\"CreateIdentityProvider\")),\n\t\t\tconnect.WithClientOptions(opts...),\n\t\t),\n\t\tupdateIdentityProvider: connect.NewClient[v1.UpdateIdentityProviderRequest, v1.IdentityProvider](\n\t\t\thttpClient,\n\t\t\tbaseURL+IdentityProviderServiceUpdateIdentityProviderProcedure,\n\t\t\tconnect.WithSchema(identityProviderServiceMethods.ByName(\"UpdateIdentityProvider\")),\n\t\t\tconnect.WithClientOptions(opts...),\n\t\t),\n\t\tdeleteIdentityProvider: connect.NewClient[v1.DeleteIdentityProviderRequest, emptypb.Empty](\n\t\t\thttpClient,\n\t\t\tbaseURL+IdentityProviderServiceDeleteIdentityProviderProcedure,\n\t\t\tconnect.WithSchema(identityProviderServiceMethods.ByName(\"DeleteIdentityProvider\")),\n\t\t\tconnect.WithClientOptions(opts...),\n\t\t),\n\t}\n}\n\n// identityProviderServiceClient implements IdentityProviderServiceClient.\ntype identityProviderServiceClient struct {\n\tlistIdentityProviders  *connect.Client[v1.ListIdentityProvidersRequest, v1.ListIdentityProvidersResponse]\n\tgetIdentityProvider    *connect.Client[v1.GetIdentityProviderRequest, v1.IdentityProvider]\n\tcreateIdentityProvider *connect.Client[v1.CreateIdentityProviderRequest, v1.IdentityProvider]\n\tupdateIdentityProvider *connect.Client[v1.UpdateIdentityProviderRequest, v1.IdentityProvider]\n\tdeleteIdentityProvider *connect.Client[v1.DeleteIdentityProviderRequest, emptypb.Empty]\n}\n\n// ListIdentityProviders calls memos.api.v1.IdentityProviderService.ListIdentityProviders.\nfunc (c *identityProviderServiceClient) ListIdentityProviders(ctx context.Context, req *connect.Request[v1.ListIdentityProvidersRequest]) (*connect.Response[v1.ListIdentityProvidersResponse], error) {\n\treturn c.listIdentityProviders.CallUnary(ctx, req)\n}\n\n// GetIdentityProvider calls memos.api.v1.IdentityProviderService.GetIdentityProvider.\nfunc (c *identityProviderServiceClient) GetIdentityProvider(ctx context.Context, req *connect.Request[v1.GetIdentityProviderRequest]) (*connect.Response[v1.IdentityProvider], error) {\n\treturn c.getIdentityProvider.CallUnary(ctx, req)\n}\n\n// CreateIdentityProvider calls memos.api.v1.IdentityProviderService.CreateIdentityProvider.\nfunc (c *identityProviderServiceClient) CreateIdentityProvider(ctx context.Context, req *connect.Request[v1.CreateIdentityProviderRequest]) (*connect.Response[v1.IdentityProvider], error) {\n\treturn c.createIdentityProvider.CallUnary(ctx, req)\n}\n\n// UpdateIdentityProvider calls memos.api.v1.IdentityProviderService.UpdateIdentityProvider.\nfunc (c *identityProviderServiceClient) UpdateIdentityProvider(ctx context.Context, req *connect.Request[v1.UpdateIdentityProviderRequest]) (*connect.Response[v1.IdentityProvider], error) {\n\treturn c.updateIdentityProvider.CallUnary(ctx, req)\n}\n\n// DeleteIdentityProvider calls memos.api.v1.IdentityProviderService.DeleteIdentityProvider.\nfunc (c *identityProviderServiceClient) DeleteIdentityProvider(ctx context.Context, req *connect.Request[v1.DeleteIdentityProviderRequest]) (*connect.Response[emptypb.Empty], error) {\n\treturn c.deleteIdentityProvider.CallUnary(ctx, req)\n}\n\n// IdentityProviderServiceHandler is an implementation of the memos.api.v1.IdentityProviderService\n// service.\ntype IdentityProviderServiceHandler interface {\n\t// ListIdentityProviders lists identity providers.\n\tListIdentityProviders(context.Context, *connect.Request[v1.ListIdentityProvidersRequest]) (*connect.Response[v1.ListIdentityProvidersResponse], error)\n\t// GetIdentityProvider gets an identity provider.\n\tGetIdentityProvider(context.Context, *connect.Request[v1.GetIdentityProviderRequest]) (*connect.Response[v1.IdentityProvider], error)\n\t// CreateIdentityProvider creates an identity provider.\n\tCreateIdentityProvider(context.Context, *connect.Request[v1.CreateIdentityProviderRequest]) (*connect.Response[v1.IdentityProvider], error)\n\t// UpdateIdentityProvider updates an identity provider.\n\tUpdateIdentityProvider(context.Context, *connect.Request[v1.UpdateIdentityProviderRequest]) (*connect.Response[v1.IdentityProvider], error)\n\t// DeleteIdentityProvider deletes an identity provider.\n\tDeleteIdentityProvider(context.Context, *connect.Request[v1.DeleteIdentityProviderRequest]) (*connect.Response[emptypb.Empty], error)\n}\n\n// NewIdentityProviderServiceHandler builds an HTTP handler from the service implementation. It\n// returns the path on which to mount the handler and the handler itself.\n//\n// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf\n// and JSON codecs. They also support gzip compression.\nfunc NewIdentityProviderServiceHandler(svc IdentityProviderServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) {\n\tidentityProviderServiceMethods := v1.File_api_v1_idp_service_proto.Services().ByName(\"IdentityProviderService\").Methods()\n\tidentityProviderServiceListIdentityProvidersHandler := connect.NewUnaryHandler(\n\t\tIdentityProviderServiceListIdentityProvidersProcedure,\n\t\tsvc.ListIdentityProviders,\n\t\tconnect.WithSchema(identityProviderServiceMethods.ByName(\"ListIdentityProviders\")),\n\t\tconnect.WithHandlerOptions(opts...),\n\t)\n\tidentityProviderServiceGetIdentityProviderHandler := connect.NewUnaryHandler(\n\t\tIdentityProviderServiceGetIdentityProviderProcedure,\n\t\tsvc.GetIdentityProvider,\n\t\tconnect.WithSchema(identityProviderServiceMethods.ByName(\"GetIdentityProvider\")),\n\t\tconnect.WithHandlerOptions(opts...),\n\t)\n\tidentityProviderServiceCreateIdentityProviderHandler := connect.NewUnaryHandler(\n\t\tIdentityProviderServiceCreateIdentityProviderProcedure,\n\t\tsvc.CreateIdentityProvider,\n\t\tconnect.WithSchema(identityProviderServiceMethods.ByName(\"CreateIdentityProvider\")),\n\t\tconnect.WithHandlerOptions(opts...),\n\t)\n\tidentityProviderServiceUpdateIdentityProviderHandler := connect.NewUnaryHandler(\n\t\tIdentityProviderServiceUpdateIdentityProviderProcedure,\n\t\tsvc.UpdateIdentityProvider,\n\t\tconnect.WithSchema(identityProviderServiceMethods.ByName(\"UpdateIdentityProvider\")),\n\t\tconnect.WithHandlerOptions(opts...),\n\t)\n\tidentityProviderServiceDeleteIdentityProviderHandler := connect.NewUnaryHandler(\n\t\tIdentityProviderServiceDeleteIdentityProviderProcedure,\n\t\tsvc.DeleteIdentityProvider,\n\t\tconnect.WithSchema(identityProviderServiceMethods.ByName(\"DeleteIdentityProvider\")),\n\t\tconnect.WithHandlerOptions(opts...),\n\t)\n\treturn \"/memos.api.v1.IdentityProviderService/\", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tswitch r.URL.Path {\n\t\tcase IdentityProviderServiceListIdentityProvidersProcedure:\n\t\t\tidentityProviderServiceListIdentityProvidersHandler.ServeHTTP(w, r)\n\t\tcase IdentityProviderServiceGetIdentityProviderProcedure:\n\t\t\tidentityProviderServiceGetIdentityProviderHandler.ServeHTTP(w, r)\n\t\tcase IdentityProviderServiceCreateIdentityProviderProcedure:\n\t\t\tidentityProviderServiceCreateIdentityProviderHandler.ServeHTTP(w, r)\n\t\tcase IdentityProviderServiceUpdateIdentityProviderProcedure:\n\t\t\tidentityProviderServiceUpdateIdentityProviderHandler.ServeHTTP(w, r)\n\t\tcase IdentityProviderServiceDeleteIdentityProviderProcedure:\n\t\t\tidentityProviderServiceDeleteIdentityProviderHandler.ServeHTTP(w, r)\n\t\tdefault:\n\t\t\thttp.NotFound(w, r)\n\t\t}\n\t})\n}\n\n// UnimplementedIdentityProviderServiceHandler returns CodeUnimplemented from all methods.\ntype UnimplementedIdentityProviderServiceHandler struct{}\n\nfunc (UnimplementedIdentityProviderServiceHandler) ListIdentityProviders(context.Context, *connect.Request[v1.ListIdentityProvidersRequest]) (*connect.Response[v1.ListIdentityProvidersResponse], error) {\n\treturn nil, connect.NewError(connect.CodeUnimplemented, errors.New(\"memos.api.v1.IdentityProviderService.ListIdentityProviders is not implemented\"))\n}\n\nfunc (UnimplementedIdentityProviderServiceHandler) GetIdentityProvider(context.Context, *connect.Request[v1.GetIdentityProviderRequest]) (*connect.Response[v1.IdentityProvider], error) {\n\treturn nil, connect.NewError(connect.CodeUnimplemented, errors.New(\"memos.api.v1.IdentityProviderService.GetIdentityProvider is not implemented\"))\n}\n\nfunc (UnimplementedIdentityProviderServiceHandler) CreateIdentityProvider(context.Context, *connect.Request[v1.CreateIdentityProviderRequest]) (*connect.Response[v1.IdentityProvider], error) {\n\treturn nil, connect.NewError(connect.CodeUnimplemented, errors.New(\"memos.api.v1.IdentityProviderService.CreateIdentityProvider is not implemented\"))\n}\n\nfunc (UnimplementedIdentityProviderServiceHandler) UpdateIdentityProvider(context.Context, *connect.Request[v1.UpdateIdentityProviderRequest]) (*connect.Response[v1.IdentityProvider], error) {\n\treturn nil, connect.NewError(connect.CodeUnimplemented, errors.New(\"memos.api.v1.IdentityProviderService.UpdateIdentityProvider is not implemented\"))\n}\n\nfunc (UnimplementedIdentityProviderServiceHandler) DeleteIdentityProvider(context.Context, *connect.Request[v1.DeleteIdentityProviderRequest]) (*connect.Response[emptypb.Empty], error) {\n\treturn nil, connect.NewError(connect.CodeUnimplemented, errors.New(\"memos.api.v1.IdentityProviderService.DeleteIdentityProvider is not implemented\"))\n}\n"
  },
  {
    "path": "proto/gen/api/v1/apiv1connect/instance_service.connect.go",
    "content": "// Code generated by protoc-gen-connect-go. DO NOT EDIT.\n//\n// Source: api/v1/instance_service.proto\n\npackage apiv1connect\n\nimport (\n\tconnect \"connectrpc.com/connect\"\n\tcontext \"context\"\n\terrors \"errors\"\n\tv1 \"github.com/usememos/memos/proto/gen/api/v1\"\n\thttp \"net/http\"\n\tstrings \"strings\"\n)\n\n// This is a compile-time assertion to ensure that this generated file and the connect package are\n// compatible. If you get a compiler error that this constant is not defined, this code was\n// generated with a version of connect newer than the one compiled into your binary. You can fix the\n// problem by either regenerating this code with an older version of connect or updating the connect\n// version compiled into your binary.\nconst _ = connect.IsAtLeastVersion1_13_0\n\nconst (\n\t// InstanceServiceName is the fully-qualified name of the InstanceService service.\n\tInstanceServiceName = \"memos.api.v1.InstanceService\"\n)\n\n// These constants are the fully-qualified names of the RPCs defined in this package. They're\n// exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route.\n//\n// Note that these are different from the fully-qualified method names used by\n// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to\n// reflection-formatted method names, remove the leading slash and convert the remaining slash to a\n// period.\nconst (\n\t// InstanceServiceGetInstanceProfileProcedure is the fully-qualified name of the InstanceService's\n\t// GetInstanceProfile RPC.\n\tInstanceServiceGetInstanceProfileProcedure = \"/memos.api.v1.InstanceService/GetInstanceProfile\"\n\t// InstanceServiceGetInstanceSettingProcedure is the fully-qualified name of the InstanceService's\n\t// GetInstanceSetting RPC.\n\tInstanceServiceGetInstanceSettingProcedure = \"/memos.api.v1.InstanceService/GetInstanceSetting\"\n\t// InstanceServiceUpdateInstanceSettingProcedure is the fully-qualified name of the\n\t// InstanceService's UpdateInstanceSetting RPC.\n\tInstanceServiceUpdateInstanceSettingProcedure = \"/memos.api.v1.InstanceService/UpdateInstanceSetting\"\n)\n\n// InstanceServiceClient is a client for the memos.api.v1.InstanceService service.\ntype InstanceServiceClient interface {\n\t// Gets the instance profile.\n\tGetInstanceProfile(context.Context, *connect.Request[v1.GetInstanceProfileRequest]) (*connect.Response[v1.InstanceProfile], error)\n\t// Gets an instance setting.\n\tGetInstanceSetting(context.Context, *connect.Request[v1.GetInstanceSettingRequest]) (*connect.Response[v1.InstanceSetting], error)\n\t// Updates an instance setting.\n\tUpdateInstanceSetting(context.Context, *connect.Request[v1.UpdateInstanceSettingRequest]) (*connect.Response[v1.InstanceSetting], error)\n}\n\n// NewInstanceServiceClient constructs a client for the memos.api.v1.InstanceService service. By\n// default, it uses the Connect protocol with the binary Protobuf Codec, asks for gzipped responses,\n// and sends uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the\n// connect.WithGRPC() or connect.WithGRPCWeb() options.\n//\n// The URL supplied here should be the base URL for the Connect or gRPC server (for example,\n// http://api.acme.com or https://acme.com/grpc).\nfunc NewInstanceServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) InstanceServiceClient {\n\tbaseURL = strings.TrimRight(baseURL, \"/\")\n\tinstanceServiceMethods := v1.File_api_v1_instance_service_proto.Services().ByName(\"InstanceService\").Methods()\n\treturn &instanceServiceClient{\n\t\tgetInstanceProfile: connect.NewClient[v1.GetInstanceProfileRequest, v1.InstanceProfile](\n\t\t\thttpClient,\n\t\t\tbaseURL+InstanceServiceGetInstanceProfileProcedure,\n\t\t\tconnect.WithSchema(instanceServiceMethods.ByName(\"GetInstanceProfile\")),\n\t\t\tconnect.WithClientOptions(opts...),\n\t\t),\n\t\tgetInstanceSetting: connect.NewClient[v1.GetInstanceSettingRequest, v1.InstanceSetting](\n\t\t\thttpClient,\n\t\t\tbaseURL+InstanceServiceGetInstanceSettingProcedure,\n\t\t\tconnect.WithSchema(instanceServiceMethods.ByName(\"GetInstanceSetting\")),\n\t\t\tconnect.WithClientOptions(opts...),\n\t\t),\n\t\tupdateInstanceSetting: connect.NewClient[v1.UpdateInstanceSettingRequest, v1.InstanceSetting](\n\t\t\thttpClient,\n\t\t\tbaseURL+InstanceServiceUpdateInstanceSettingProcedure,\n\t\t\tconnect.WithSchema(instanceServiceMethods.ByName(\"UpdateInstanceSetting\")),\n\t\t\tconnect.WithClientOptions(opts...),\n\t\t),\n\t}\n}\n\n// instanceServiceClient implements InstanceServiceClient.\ntype instanceServiceClient struct {\n\tgetInstanceProfile    *connect.Client[v1.GetInstanceProfileRequest, v1.InstanceProfile]\n\tgetInstanceSetting    *connect.Client[v1.GetInstanceSettingRequest, v1.InstanceSetting]\n\tupdateInstanceSetting *connect.Client[v1.UpdateInstanceSettingRequest, v1.InstanceSetting]\n}\n\n// GetInstanceProfile calls memos.api.v1.InstanceService.GetInstanceProfile.\nfunc (c *instanceServiceClient) GetInstanceProfile(ctx context.Context, req *connect.Request[v1.GetInstanceProfileRequest]) (*connect.Response[v1.InstanceProfile], error) {\n\treturn c.getInstanceProfile.CallUnary(ctx, req)\n}\n\n// GetInstanceSetting calls memos.api.v1.InstanceService.GetInstanceSetting.\nfunc (c *instanceServiceClient) GetInstanceSetting(ctx context.Context, req *connect.Request[v1.GetInstanceSettingRequest]) (*connect.Response[v1.InstanceSetting], error) {\n\treturn c.getInstanceSetting.CallUnary(ctx, req)\n}\n\n// UpdateInstanceSetting calls memos.api.v1.InstanceService.UpdateInstanceSetting.\nfunc (c *instanceServiceClient) UpdateInstanceSetting(ctx context.Context, req *connect.Request[v1.UpdateInstanceSettingRequest]) (*connect.Response[v1.InstanceSetting], error) {\n\treturn c.updateInstanceSetting.CallUnary(ctx, req)\n}\n\n// InstanceServiceHandler is an implementation of the memos.api.v1.InstanceService service.\ntype InstanceServiceHandler interface {\n\t// Gets the instance profile.\n\tGetInstanceProfile(context.Context, *connect.Request[v1.GetInstanceProfileRequest]) (*connect.Response[v1.InstanceProfile], error)\n\t// Gets an instance setting.\n\tGetInstanceSetting(context.Context, *connect.Request[v1.GetInstanceSettingRequest]) (*connect.Response[v1.InstanceSetting], error)\n\t// Updates an instance setting.\n\tUpdateInstanceSetting(context.Context, *connect.Request[v1.UpdateInstanceSettingRequest]) (*connect.Response[v1.InstanceSetting], error)\n}\n\n// NewInstanceServiceHandler builds an HTTP handler from the service implementation. It returns the\n// path on which to mount the handler and the handler itself.\n//\n// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf\n// and JSON codecs. They also support gzip compression.\nfunc NewInstanceServiceHandler(svc InstanceServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) {\n\tinstanceServiceMethods := v1.File_api_v1_instance_service_proto.Services().ByName(\"InstanceService\").Methods()\n\tinstanceServiceGetInstanceProfileHandler := connect.NewUnaryHandler(\n\t\tInstanceServiceGetInstanceProfileProcedure,\n\t\tsvc.GetInstanceProfile,\n\t\tconnect.WithSchema(instanceServiceMethods.ByName(\"GetInstanceProfile\")),\n\t\tconnect.WithHandlerOptions(opts...),\n\t)\n\tinstanceServiceGetInstanceSettingHandler := connect.NewUnaryHandler(\n\t\tInstanceServiceGetInstanceSettingProcedure,\n\t\tsvc.GetInstanceSetting,\n\t\tconnect.WithSchema(instanceServiceMethods.ByName(\"GetInstanceSetting\")),\n\t\tconnect.WithHandlerOptions(opts...),\n\t)\n\tinstanceServiceUpdateInstanceSettingHandler := connect.NewUnaryHandler(\n\t\tInstanceServiceUpdateInstanceSettingProcedure,\n\t\tsvc.UpdateInstanceSetting,\n\t\tconnect.WithSchema(instanceServiceMethods.ByName(\"UpdateInstanceSetting\")),\n\t\tconnect.WithHandlerOptions(opts...),\n\t)\n\treturn \"/memos.api.v1.InstanceService/\", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tswitch r.URL.Path {\n\t\tcase InstanceServiceGetInstanceProfileProcedure:\n\t\t\tinstanceServiceGetInstanceProfileHandler.ServeHTTP(w, r)\n\t\tcase InstanceServiceGetInstanceSettingProcedure:\n\t\t\tinstanceServiceGetInstanceSettingHandler.ServeHTTP(w, r)\n\t\tcase InstanceServiceUpdateInstanceSettingProcedure:\n\t\t\tinstanceServiceUpdateInstanceSettingHandler.ServeHTTP(w, r)\n\t\tdefault:\n\t\t\thttp.NotFound(w, r)\n\t\t}\n\t})\n}\n\n// UnimplementedInstanceServiceHandler returns CodeUnimplemented from all methods.\ntype UnimplementedInstanceServiceHandler struct{}\n\nfunc (UnimplementedInstanceServiceHandler) GetInstanceProfile(context.Context, *connect.Request[v1.GetInstanceProfileRequest]) (*connect.Response[v1.InstanceProfile], error) {\n\treturn nil, connect.NewError(connect.CodeUnimplemented, errors.New(\"memos.api.v1.InstanceService.GetInstanceProfile is not implemented\"))\n}\n\nfunc (UnimplementedInstanceServiceHandler) GetInstanceSetting(context.Context, *connect.Request[v1.GetInstanceSettingRequest]) (*connect.Response[v1.InstanceSetting], error) {\n\treturn nil, connect.NewError(connect.CodeUnimplemented, errors.New(\"memos.api.v1.InstanceService.GetInstanceSetting is not implemented\"))\n}\n\nfunc (UnimplementedInstanceServiceHandler) UpdateInstanceSetting(context.Context, *connect.Request[v1.UpdateInstanceSettingRequest]) (*connect.Response[v1.InstanceSetting], error) {\n\treturn nil, connect.NewError(connect.CodeUnimplemented, errors.New(\"memos.api.v1.InstanceService.UpdateInstanceSetting is not implemented\"))\n}\n"
  },
  {
    "path": "proto/gen/api/v1/apiv1connect/memo_service.connect.go",
    "content": "// Code generated by protoc-gen-connect-go. DO NOT EDIT.\n//\n// Source: api/v1/memo_service.proto\n\npackage apiv1connect\n\nimport (\n\tconnect \"connectrpc.com/connect\"\n\tcontext \"context\"\n\terrors \"errors\"\n\tv1 \"github.com/usememos/memos/proto/gen/api/v1\"\n\temptypb \"google.golang.org/protobuf/types/known/emptypb\"\n\thttp \"net/http\"\n\tstrings \"strings\"\n)\n\n// This is a compile-time assertion to ensure that this generated file and the connect package are\n// compatible. If you get a compiler error that this constant is not defined, this code was\n// generated with a version of connect newer than the one compiled into your binary. You can fix the\n// problem by either regenerating this code with an older version of connect or updating the connect\n// version compiled into your binary.\nconst _ = connect.IsAtLeastVersion1_13_0\n\nconst (\n\t// MemoServiceName is the fully-qualified name of the MemoService service.\n\tMemoServiceName = \"memos.api.v1.MemoService\"\n)\n\n// These constants are the fully-qualified names of the RPCs defined in this package. They're\n// exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route.\n//\n// Note that these are different from the fully-qualified method names used by\n// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to\n// reflection-formatted method names, remove the leading slash and convert the remaining slash to a\n// period.\nconst (\n\t// MemoServiceCreateMemoProcedure is the fully-qualified name of the MemoService's CreateMemo RPC.\n\tMemoServiceCreateMemoProcedure = \"/memos.api.v1.MemoService/CreateMemo\"\n\t// MemoServiceListMemosProcedure is the fully-qualified name of the MemoService's ListMemos RPC.\n\tMemoServiceListMemosProcedure = \"/memos.api.v1.MemoService/ListMemos\"\n\t// MemoServiceGetMemoProcedure is the fully-qualified name of the MemoService's GetMemo RPC.\n\tMemoServiceGetMemoProcedure = \"/memos.api.v1.MemoService/GetMemo\"\n\t// MemoServiceUpdateMemoProcedure is the fully-qualified name of the MemoService's UpdateMemo RPC.\n\tMemoServiceUpdateMemoProcedure = \"/memos.api.v1.MemoService/UpdateMemo\"\n\t// MemoServiceDeleteMemoProcedure is the fully-qualified name of the MemoService's DeleteMemo RPC.\n\tMemoServiceDeleteMemoProcedure = \"/memos.api.v1.MemoService/DeleteMemo\"\n\t// MemoServiceSetMemoAttachmentsProcedure is the fully-qualified name of the MemoService's\n\t// SetMemoAttachments RPC.\n\tMemoServiceSetMemoAttachmentsProcedure = \"/memos.api.v1.MemoService/SetMemoAttachments\"\n\t// MemoServiceListMemoAttachmentsProcedure is the fully-qualified name of the MemoService's\n\t// ListMemoAttachments RPC.\n\tMemoServiceListMemoAttachmentsProcedure = \"/memos.api.v1.MemoService/ListMemoAttachments\"\n\t// MemoServiceSetMemoRelationsProcedure is the fully-qualified name of the MemoService's\n\t// SetMemoRelations RPC.\n\tMemoServiceSetMemoRelationsProcedure = \"/memos.api.v1.MemoService/SetMemoRelations\"\n\t// MemoServiceListMemoRelationsProcedure is the fully-qualified name of the MemoService's\n\t// ListMemoRelations RPC.\n\tMemoServiceListMemoRelationsProcedure = \"/memos.api.v1.MemoService/ListMemoRelations\"\n\t// MemoServiceCreateMemoCommentProcedure is the fully-qualified name of the MemoService's\n\t// CreateMemoComment RPC.\n\tMemoServiceCreateMemoCommentProcedure = \"/memos.api.v1.MemoService/CreateMemoComment\"\n\t// MemoServiceListMemoCommentsProcedure is the fully-qualified name of the MemoService's\n\t// ListMemoComments RPC.\n\tMemoServiceListMemoCommentsProcedure = \"/memos.api.v1.MemoService/ListMemoComments\"\n\t// MemoServiceListMemoReactionsProcedure is the fully-qualified name of the MemoService's\n\t// ListMemoReactions RPC.\n\tMemoServiceListMemoReactionsProcedure = \"/memos.api.v1.MemoService/ListMemoReactions\"\n\t// MemoServiceUpsertMemoReactionProcedure is the fully-qualified name of the MemoService's\n\t// UpsertMemoReaction RPC.\n\tMemoServiceUpsertMemoReactionProcedure = \"/memos.api.v1.MemoService/UpsertMemoReaction\"\n\t// MemoServiceDeleteMemoReactionProcedure is the fully-qualified name of the MemoService's\n\t// DeleteMemoReaction RPC.\n\tMemoServiceDeleteMemoReactionProcedure = \"/memos.api.v1.MemoService/DeleteMemoReaction\"\n\t// MemoServiceCreateMemoShareProcedure is the fully-qualified name of the MemoService's\n\t// CreateMemoShare RPC.\n\tMemoServiceCreateMemoShareProcedure = \"/memos.api.v1.MemoService/CreateMemoShare\"\n\t// MemoServiceListMemoSharesProcedure is the fully-qualified name of the MemoService's\n\t// ListMemoShares RPC.\n\tMemoServiceListMemoSharesProcedure = \"/memos.api.v1.MemoService/ListMemoShares\"\n\t// MemoServiceDeleteMemoShareProcedure is the fully-qualified name of the MemoService's\n\t// DeleteMemoShare RPC.\n\tMemoServiceDeleteMemoShareProcedure = \"/memos.api.v1.MemoService/DeleteMemoShare\"\n\t// MemoServiceGetMemoByShareProcedure is the fully-qualified name of the MemoService's\n\t// GetMemoByShare RPC.\n\tMemoServiceGetMemoByShareProcedure = \"/memos.api.v1.MemoService/GetMemoByShare\"\n)\n\n// MemoServiceClient is a client for the memos.api.v1.MemoService service.\ntype MemoServiceClient interface {\n\t// CreateMemo creates a memo.\n\tCreateMemo(context.Context, *connect.Request[v1.CreateMemoRequest]) (*connect.Response[v1.Memo], error)\n\t// ListMemos lists memos with pagination and filter.\n\tListMemos(context.Context, *connect.Request[v1.ListMemosRequest]) (*connect.Response[v1.ListMemosResponse], error)\n\t// GetMemo gets a memo.\n\tGetMemo(context.Context, *connect.Request[v1.GetMemoRequest]) (*connect.Response[v1.Memo], error)\n\t// UpdateMemo updates a memo.\n\tUpdateMemo(context.Context, *connect.Request[v1.UpdateMemoRequest]) (*connect.Response[v1.Memo], error)\n\t// DeleteMemo deletes a memo.\n\tDeleteMemo(context.Context, *connect.Request[v1.DeleteMemoRequest]) (*connect.Response[emptypb.Empty], error)\n\t// SetMemoAttachments sets attachments for a memo.\n\tSetMemoAttachments(context.Context, *connect.Request[v1.SetMemoAttachmentsRequest]) (*connect.Response[emptypb.Empty], error)\n\t// ListMemoAttachments lists attachments for a memo.\n\tListMemoAttachments(context.Context, *connect.Request[v1.ListMemoAttachmentsRequest]) (*connect.Response[v1.ListMemoAttachmentsResponse], error)\n\t// SetMemoRelations sets relations for a memo.\n\tSetMemoRelations(context.Context, *connect.Request[v1.SetMemoRelationsRequest]) (*connect.Response[emptypb.Empty], error)\n\t// ListMemoRelations lists relations for a memo.\n\tListMemoRelations(context.Context, *connect.Request[v1.ListMemoRelationsRequest]) (*connect.Response[v1.ListMemoRelationsResponse], error)\n\t// CreateMemoComment creates a comment for a memo.\n\tCreateMemoComment(context.Context, *connect.Request[v1.CreateMemoCommentRequest]) (*connect.Response[v1.Memo], error)\n\t// ListMemoComments lists comments for a memo.\n\tListMemoComments(context.Context, *connect.Request[v1.ListMemoCommentsRequest]) (*connect.Response[v1.ListMemoCommentsResponse], error)\n\t// ListMemoReactions lists reactions for a memo.\n\tListMemoReactions(context.Context, *connect.Request[v1.ListMemoReactionsRequest]) (*connect.Response[v1.ListMemoReactionsResponse], error)\n\t// UpsertMemoReaction upserts a reaction for a memo.\n\tUpsertMemoReaction(context.Context, *connect.Request[v1.UpsertMemoReactionRequest]) (*connect.Response[v1.Reaction], error)\n\t// DeleteMemoReaction deletes a reaction for a memo.\n\tDeleteMemoReaction(context.Context, *connect.Request[v1.DeleteMemoReactionRequest]) (*connect.Response[emptypb.Empty], error)\n\t// CreateMemoShare creates a share link for a memo. Requires authentication as the memo creator.\n\tCreateMemoShare(context.Context, *connect.Request[v1.CreateMemoShareRequest]) (*connect.Response[v1.MemoShare], error)\n\t// ListMemoShares lists all share links for a memo. Requires authentication as the memo creator.\n\tListMemoShares(context.Context, *connect.Request[v1.ListMemoSharesRequest]) (*connect.Response[v1.ListMemoSharesResponse], error)\n\t// DeleteMemoShare revokes a share link. Requires authentication as the memo creator.\n\tDeleteMemoShare(context.Context, *connect.Request[v1.DeleteMemoShareRequest]) (*connect.Response[emptypb.Empty], error)\n\t// GetMemoByShare resolves a share token to its memo. No authentication required.\n\t// Returns NOT_FOUND if the token is invalid or expired.\n\tGetMemoByShare(context.Context, *connect.Request[v1.GetMemoByShareRequest]) (*connect.Response[v1.Memo], error)\n}\n\n// NewMemoServiceClient constructs a client for the memos.api.v1.MemoService service. By default, it\n// uses the Connect protocol with the binary Protobuf Codec, asks for gzipped responses, and sends\n// uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the connect.WithGRPC() or\n// connect.WithGRPCWeb() options.\n//\n// The URL supplied here should be the base URL for the Connect or gRPC server (for example,\n// http://api.acme.com or https://acme.com/grpc).\nfunc NewMemoServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) MemoServiceClient {\n\tbaseURL = strings.TrimRight(baseURL, \"/\")\n\tmemoServiceMethods := v1.File_api_v1_memo_service_proto.Services().ByName(\"MemoService\").Methods()\n\treturn &memoServiceClient{\n\t\tcreateMemo: connect.NewClient[v1.CreateMemoRequest, v1.Memo](\n\t\t\thttpClient,\n\t\t\tbaseURL+MemoServiceCreateMemoProcedure,\n\t\t\tconnect.WithSchema(memoServiceMethods.ByName(\"CreateMemo\")),\n\t\t\tconnect.WithClientOptions(opts...),\n\t\t),\n\t\tlistMemos: connect.NewClient[v1.ListMemosRequest, v1.ListMemosResponse](\n\t\t\thttpClient,\n\t\t\tbaseURL+MemoServiceListMemosProcedure,\n\t\t\tconnect.WithSchema(memoServiceMethods.ByName(\"ListMemos\")),\n\t\t\tconnect.WithClientOptions(opts...),\n\t\t),\n\t\tgetMemo: connect.NewClient[v1.GetMemoRequest, v1.Memo](\n\t\t\thttpClient,\n\t\t\tbaseURL+MemoServiceGetMemoProcedure,\n\t\t\tconnect.WithSchema(memoServiceMethods.ByName(\"GetMemo\")),\n\t\t\tconnect.WithClientOptions(opts...),\n\t\t),\n\t\tupdateMemo: connect.NewClient[v1.UpdateMemoRequest, v1.Memo](\n\t\t\thttpClient,\n\t\t\tbaseURL+MemoServiceUpdateMemoProcedure,\n\t\t\tconnect.WithSchema(memoServiceMethods.ByName(\"UpdateMemo\")),\n\t\t\tconnect.WithClientOptions(opts...),\n\t\t),\n\t\tdeleteMemo: connect.NewClient[v1.DeleteMemoRequest, emptypb.Empty](\n\t\t\thttpClient,\n\t\t\tbaseURL+MemoServiceDeleteMemoProcedure,\n\t\t\tconnect.WithSchema(memoServiceMethods.ByName(\"DeleteMemo\")),\n\t\t\tconnect.WithClientOptions(opts...),\n\t\t),\n\t\tsetMemoAttachments: connect.NewClient[v1.SetMemoAttachmentsRequest, emptypb.Empty](\n\t\t\thttpClient,\n\t\t\tbaseURL+MemoServiceSetMemoAttachmentsProcedure,\n\t\t\tconnect.WithSchema(memoServiceMethods.ByName(\"SetMemoAttachments\")),\n\t\t\tconnect.WithClientOptions(opts...),\n\t\t),\n\t\tlistMemoAttachments: connect.NewClient[v1.ListMemoAttachmentsRequest, v1.ListMemoAttachmentsResponse](\n\t\t\thttpClient,\n\t\t\tbaseURL+MemoServiceListMemoAttachmentsProcedure,\n\t\t\tconnect.WithSchema(memoServiceMethods.ByName(\"ListMemoAttachments\")),\n\t\t\tconnect.WithClientOptions(opts...),\n\t\t),\n\t\tsetMemoRelations: connect.NewClient[v1.SetMemoRelationsRequest, emptypb.Empty](\n\t\t\thttpClient,\n\t\t\tbaseURL+MemoServiceSetMemoRelationsProcedure,\n\t\t\tconnect.WithSchema(memoServiceMethods.ByName(\"SetMemoRelations\")),\n\t\t\tconnect.WithClientOptions(opts...),\n\t\t),\n\t\tlistMemoRelations: connect.NewClient[v1.ListMemoRelationsRequest, v1.ListMemoRelationsResponse](\n\t\t\thttpClient,\n\t\t\tbaseURL+MemoServiceListMemoRelationsProcedure,\n\t\t\tconnect.WithSchema(memoServiceMethods.ByName(\"ListMemoRelations\")),\n\t\t\tconnect.WithClientOptions(opts...),\n\t\t),\n\t\tcreateMemoComment: connect.NewClient[v1.CreateMemoCommentRequest, v1.Memo](\n\t\t\thttpClient,\n\t\t\tbaseURL+MemoServiceCreateMemoCommentProcedure,\n\t\t\tconnect.WithSchema(memoServiceMethods.ByName(\"CreateMemoComment\")),\n\t\t\tconnect.WithClientOptions(opts...),\n\t\t),\n\t\tlistMemoComments: connect.NewClient[v1.ListMemoCommentsRequest, v1.ListMemoCommentsResponse](\n\t\t\thttpClient,\n\t\t\tbaseURL+MemoServiceListMemoCommentsProcedure,\n\t\t\tconnect.WithSchema(memoServiceMethods.ByName(\"ListMemoComments\")),\n\t\t\tconnect.WithClientOptions(opts...),\n\t\t),\n\t\tlistMemoReactions: connect.NewClient[v1.ListMemoReactionsRequest, v1.ListMemoReactionsResponse](\n\t\t\thttpClient,\n\t\t\tbaseURL+MemoServiceListMemoReactionsProcedure,\n\t\t\tconnect.WithSchema(memoServiceMethods.ByName(\"ListMemoReactions\")),\n\t\t\tconnect.WithClientOptions(opts...),\n\t\t),\n\t\tupsertMemoReaction: connect.NewClient[v1.UpsertMemoReactionRequest, v1.Reaction](\n\t\t\thttpClient,\n\t\t\tbaseURL+MemoServiceUpsertMemoReactionProcedure,\n\t\t\tconnect.WithSchema(memoServiceMethods.ByName(\"UpsertMemoReaction\")),\n\t\t\tconnect.WithClientOptions(opts...),\n\t\t),\n\t\tdeleteMemoReaction: connect.NewClient[v1.DeleteMemoReactionRequest, emptypb.Empty](\n\t\t\thttpClient,\n\t\t\tbaseURL+MemoServiceDeleteMemoReactionProcedure,\n\t\t\tconnect.WithSchema(memoServiceMethods.ByName(\"DeleteMemoReaction\")),\n\t\t\tconnect.WithClientOptions(opts...),\n\t\t),\n\t\tcreateMemoShare: connect.NewClient[v1.CreateMemoShareRequest, v1.MemoShare](\n\t\t\thttpClient,\n\t\t\tbaseURL+MemoServiceCreateMemoShareProcedure,\n\t\t\tconnect.WithSchema(memoServiceMethods.ByName(\"CreateMemoShare\")),\n\t\t\tconnect.WithClientOptions(opts...),\n\t\t),\n\t\tlistMemoShares: connect.NewClient[v1.ListMemoSharesRequest, v1.ListMemoSharesResponse](\n\t\t\thttpClient,\n\t\t\tbaseURL+MemoServiceListMemoSharesProcedure,\n\t\t\tconnect.WithSchema(memoServiceMethods.ByName(\"ListMemoShares\")),\n\t\t\tconnect.WithClientOptions(opts...),\n\t\t),\n\t\tdeleteMemoShare: connect.NewClient[v1.DeleteMemoShareRequest, emptypb.Empty](\n\t\t\thttpClient,\n\t\t\tbaseURL+MemoServiceDeleteMemoShareProcedure,\n\t\t\tconnect.WithSchema(memoServiceMethods.ByName(\"DeleteMemoShare\")),\n\t\t\tconnect.WithClientOptions(opts...),\n\t\t),\n\t\tgetMemoByShare: connect.NewClient[v1.GetMemoByShareRequest, v1.Memo](\n\t\t\thttpClient,\n\t\t\tbaseURL+MemoServiceGetMemoByShareProcedure,\n\t\t\tconnect.WithSchema(memoServiceMethods.ByName(\"GetMemoByShare\")),\n\t\t\tconnect.WithClientOptions(opts...),\n\t\t),\n\t}\n}\n\n// memoServiceClient implements MemoServiceClient.\ntype memoServiceClient struct {\n\tcreateMemo          *connect.Client[v1.CreateMemoRequest, v1.Memo]\n\tlistMemos           *connect.Client[v1.ListMemosRequest, v1.ListMemosResponse]\n\tgetMemo             *connect.Client[v1.GetMemoRequest, v1.Memo]\n\tupdateMemo          *connect.Client[v1.UpdateMemoRequest, v1.Memo]\n\tdeleteMemo          *connect.Client[v1.DeleteMemoRequest, emptypb.Empty]\n\tsetMemoAttachments  *connect.Client[v1.SetMemoAttachmentsRequest, emptypb.Empty]\n\tlistMemoAttachments *connect.Client[v1.ListMemoAttachmentsRequest, v1.ListMemoAttachmentsResponse]\n\tsetMemoRelations    *connect.Client[v1.SetMemoRelationsRequest, emptypb.Empty]\n\tlistMemoRelations   *connect.Client[v1.ListMemoRelationsRequest, v1.ListMemoRelationsResponse]\n\tcreateMemoComment   *connect.Client[v1.CreateMemoCommentRequest, v1.Memo]\n\tlistMemoComments    *connect.Client[v1.ListMemoCommentsRequest, v1.ListMemoCommentsResponse]\n\tlistMemoReactions   *connect.Client[v1.ListMemoReactionsRequest, v1.ListMemoReactionsResponse]\n\tupsertMemoReaction  *connect.Client[v1.UpsertMemoReactionRequest, v1.Reaction]\n\tdeleteMemoReaction  *connect.Client[v1.DeleteMemoReactionRequest, emptypb.Empty]\n\tcreateMemoShare     *connect.Client[v1.CreateMemoShareRequest, v1.MemoShare]\n\tlistMemoShares      *connect.Client[v1.ListMemoSharesRequest, v1.ListMemoSharesResponse]\n\tdeleteMemoShare     *connect.Client[v1.DeleteMemoShareRequest, emptypb.Empty]\n\tgetMemoByShare      *connect.Client[v1.GetMemoByShareRequest, v1.Memo]\n}\n\n// CreateMemo calls memos.api.v1.MemoService.CreateMemo.\nfunc (c *memoServiceClient) CreateMemo(ctx context.Context, req *connect.Request[v1.CreateMemoRequest]) (*connect.Response[v1.Memo], error) {\n\treturn c.createMemo.CallUnary(ctx, req)\n}\n\n// ListMemos calls memos.api.v1.MemoService.ListMemos.\nfunc (c *memoServiceClient) ListMemos(ctx context.Context, req *connect.Request[v1.ListMemosRequest]) (*connect.Response[v1.ListMemosResponse], error) {\n\treturn c.listMemos.CallUnary(ctx, req)\n}\n\n// GetMemo calls memos.api.v1.MemoService.GetMemo.\nfunc (c *memoServiceClient) GetMemo(ctx context.Context, req *connect.Request[v1.GetMemoRequest]) (*connect.Response[v1.Memo], error) {\n\treturn c.getMemo.CallUnary(ctx, req)\n}\n\n// UpdateMemo calls memos.api.v1.MemoService.UpdateMemo.\nfunc (c *memoServiceClient) UpdateMemo(ctx context.Context, req *connect.Request[v1.UpdateMemoRequest]) (*connect.Response[v1.Memo], error) {\n\treturn c.updateMemo.CallUnary(ctx, req)\n}\n\n// DeleteMemo calls memos.api.v1.MemoService.DeleteMemo.\nfunc (c *memoServiceClient) DeleteMemo(ctx context.Context, req *connect.Request[v1.DeleteMemoRequest]) (*connect.Response[emptypb.Empty], error) {\n\treturn c.deleteMemo.CallUnary(ctx, req)\n}\n\n// SetMemoAttachments calls memos.api.v1.MemoService.SetMemoAttachments.\nfunc (c *memoServiceClient) SetMemoAttachments(ctx context.Context, req *connect.Request[v1.SetMemoAttachmentsRequest]) (*connect.Response[emptypb.Empty], error) {\n\treturn c.setMemoAttachments.CallUnary(ctx, req)\n}\n\n// ListMemoAttachments calls memos.api.v1.MemoService.ListMemoAttachments.\nfunc (c *memoServiceClient) ListMemoAttachments(ctx context.Context, req *connect.Request[v1.ListMemoAttachmentsRequest]) (*connect.Response[v1.ListMemoAttachmentsResponse], error) {\n\treturn c.listMemoAttachments.CallUnary(ctx, req)\n}\n\n// SetMemoRelations calls memos.api.v1.MemoService.SetMemoRelations.\nfunc (c *memoServiceClient) SetMemoRelations(ctx context.Context, req *connect.Request[v1.SetMemoRelationsRequest]) (*connect.Response[emptypb.Empty], error) {\n\treturn c.setMemoRelations.CallUnary(ctx, req)\n}\n\n// ListMemoRelations calls memos.api.v1.MemoService.ListMemoRelations.\nfunc (c *memoServiceClient) ListMemoRelations(ctx context.Context, req *connect.Request[v1.ListMemoRelationsRequest]) (*connect.Response[v1.ListMemoRelationsResponse], error) {\n\treturn c.listMemoRelations.CallUnary(ctx, req)\n}\n\n// CreateMemoComment calls memos.api.v1.MemoService.CreateMemoComment.\nfunc (c *memoServiceClient) CreateMemoComment(ctx context.Context, req *connect.Request[v1.CreateMemoCommentRequest]) (*connect.Response[v1.Memo], error) {\n\treturn c.createMemoComment.CallUnary(ctx, req)\n}\n\n// ListMemoComments calls memos.api.v1.MemoService.ListMemoComments.\nfunc (c *memoServiceClient) ListMemoComments(ctx context.Context, req *connect.Request[v1.ListMemoCommentsRequest]) (*connect.Response[v1.ListMemoCommentsResponse], error) {\n\treturn c.listMemoComments.CallUnary(ctx, req)\n}\n\n// ListMemoReactions calls memos.api.v1.MemoService.ListMemoReactions.\nfunc (c *memoServiceClient) ListMemoReactions(ctx context.Context, req *connect.Request[v1.ListMemoReactionsRequest]) (*connect.Response[v1.ListMemoReactionsResponse], error) {\n\treturn c.listMemoReactions.CallUnary(ctx, req)\n}\n\n// UpsertMemoReaction calls memos.api.v1.MemoService.UpsertMemoReaction.\nfunc (c *memoServiceClient) UpsertMemoReaction(ctx context.Context, req *connect.Request[v1.UpsertMemoReactionRequest]) (*connect.Response[v1.Reaction], error) {\n\treturn c.upsertMemoReaction.CallUnary(ctx, req)\n}\n\n// DeleteMemoReaction calls memos.api.v1.MemoService.DeleteMemoReaction.\nfunc (c *memoServiceClient) DeleteMemoReaction(ctx context.Context, req *connect.Request[v1.DeleteMemoReactionRequest]) (*connect.Response[emptypb.Empty], error) {\n\treturn c.deleteMemoReaction.CallUnary(ctx, req)\n}\n\n// CreateMemoShare calls memos.api.v1.MemoService.CreateMemoShare.\nfunc (c *memoServiceClient) CreateMemoShare(ctx context.Context, req *connect.Request[v1.CreateMemoShareRequest]) (*connect.Response[v1.MemoShare], error) {\n\treturn c.createMemoShare.CallUnary(ctx, req)\n}\n\n// ListMemoShares calls memos.api.v1.MemoService.ListMemoShares.\nfunc (c *memoServiceClient) ListMemoShares(ctx context.Context, req *connect.Request[v1.ListMemoSharesRequest]) (*connect.Response[v1.ListMemoSharesResponse], error) {\n\treturn c.listMemoShares.CallUnary(ctx, req)\n}\n\n// DeleteMemoShare calls memos.api.v1.MemoService.DeleteMemoShare.\nfunc (c *memoServiceClient) DeleteMemoShare(ctx context.Context, req *connect.Request[v1.DeleteMemoShareRequest]) (*connect.Response[emptypb.Empty], error) {\n\treturn c.deleteMemoShare.CallUnary(ctx, req)\n}\n\n// GetMemoByShare calls memos.api.v1.MemoService.GetMemoByShare.\nfunc (c *memoServiceClient) GetMemoByShare(ctx context.Context, req *connect.Request[v1.GetMemoByShareRequest]) (*connect.Response[v1.Memo], error) {\n\treturn c.getMemoByShare.CallUnary(ctx, req)\n}\n\n// MemoServiceHandler is an implementation of the memos.api.v1.MemoService service.\ntype MemoServiceHandler interface {\n\t// CreateMemo creates a memo.\n\tCreateMemo(context.Context, *connect.Request[v1.CreateMemoRequest]) (*connect.Response[v1.Memo], error)\n\t// ListMemos lists memos with pagination and filter.\n\tListMemos(context.Context, *connect.Request[v1.ListMemosRequest]) (*connect.Response[v1.ListMemosResponse], error)\n\t// GetMemo gets a memo.\n\tGetMemo(context.Context, *connect.Request[v1.GetMemoRequest]) (*connect.Response[v1.Memo], error)\n\t// UpdateMemo updates a memo.\n\tUpdateMemo(context.Context, *connect.Request[v1.UpdateMemoRequest]) (*connect.Response[v1.Memo], error)\n\t// DeleteMemo deletes a memo.\n\tDeleteMemo(context.Context, *connect.Request[v1.DeleteMemoRequest]) (*connect.Response[emptypb.Empty], error)\n\t// SetMemoAttachments sets attachments for a memo.\n\tSetMemoAttachments(context.Context, *connect.Request[v1.SetMemoAttachmentsRequest]) (*connect.Response[emptypb.Empty], error)\n\t// ListMemoAttachments lists attachments for a memo.\n\tListMemoAttachments(context.Context, *connect.Request[v1.ListMemoAttachmentsRequest]) (*connect.Response[v1.ListMemoAttachmentsResponse], error)\n\t// SetMemoRelations sets relations for a memo.\n\tSetMemoRelations(context.Context, *connect.Request[v1.SetMemoRelationsRequest]) (*connect.Response[emptypb.Empty], error)\n\t// ListMemoRelations lists relations for a memo.\n\tListMemoRelations(context.Context, *connect.Request[v1.ListMemoRelationsRequest]) (*connect.Response[v1.ListMemoRelationsResponse], error)\n\t// CreateMemoComment creates a comment for a memo.\n\tCreateMemoComment(context.Context, *connect.Request[v1.CreateMemoCommentRequest]) (*connect.Response[v1.Memo], error)\n\t// ListMemoComments lists comments for a memo.\n\tListMemoComments(context.Context, *connect.Request[v1.ListMemoCommentsRequest]) (*connect.Response[v1.ListMemoCommentsResponse], error)\n\t// ListMemoReactions lists reactions for a memo.\n\tListMemoReactions(context.Context, *connect.Request[v1.ListMemoReactionsRequest]) (*connect.Response[v1.ListMemoReactionsResponse], error)\n\t// UpsertMemoReaction upserts a reaction for a memo.\n\tUpsertMemoReaction(context.Context, *connect.Request[v1.UpsertMemoReactionRequest]) (*connect.Response[v1.Reaction], error)\n\t// DeleteMemoReaction deletes a reaction for a memo.\n\tDeleteMemoReaction(context.Context, *connect.Request[v1.DeleteMemoReactionRequest]) (*connect.Response[emptypb.Empty], error)\n\t// CreateMemoShare creates a share link for a memo. Requires authentication as the memo creator.\n\tCreateMemoShare(context.Context, *connect.Request[v1.CreateMemoShareRequest]) (*connect.Response[v1.MemoShare], error)\n\t// ListMemoShares lists all share links for a memo. Requires authentication as the memo creator.\n\tListMemoShares(context.Context, *connect.Request[v1.ListMemoSharesRequest]) (*connect.Response[v1.ListMemoSharesResponse], error)\n\t// DeleteMemoShare revokes a share link. Requires authentication as the memo creator.\n\tDeleteMemoShare(context.Context, *connect.Request[v1.DeleteMemoShareRequest]) (*connect.Response[emptypb.Empty], error)\n\t// GetMemoByShare resolves a share token to its memo. No authentication required.\n\t// Returns NOT_FOUND if the token is invalid or expired.\n\tGetMemoByShare(context.Context, *connect.Request[v1.GetMemoByShareRequest]) (*connect.Response[v1.Memo], error)\n}\n\n// NewMemoServiceHandler builds an HTTP handler from the service implementation. It returns the path\n// on which to mount the handler and the handler itself.\n//\n// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf\n// and JSON codecs. They also support gzip compression.\nfunc NewMemoServiceHandler(svc MemoServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) {\n\tmemoServiceMethods := v1.File_api_v1_memo_service_proto.Services().ByName(\"MemoService\").Methods()\n\tmemoServiceCreateMemoHandler := connect.NewUnaryHandler(\n\t\tMemoServiceCreateMemoProcedure,\n\t\tsvc.CreateMemo,\n\t\tconnect.WithSchema(memoServiceMethods.ByName(\"CreateMemo\")),\n\t\tconnect.WithHandlerOptions(opts...),\n\t)\n\tmemoServiceListMemosHandler := connect.NewUnaryHandler(\n\t\tMemoServiceListMemosProcedure,\n\t\tsvc.ListMemos,\n\t\tconnect.WithSchema(memoServiceMethods.ByName(\"ListMemos\")),\n\t\tconnect.WithHandlerOptions(opts...),\n\t)\n\tmemoServiceGetMemoHandler := connect.NewUnaryHandler(\n\t\tMemoServiceGetMemoProcedure,\n\t\tsvc.GetMemo,\n\t\tconnect.WithSchema(memoServiceMethods.ByName(\"GetMemo\")),\n\t\tconnect.WithHandlerOptions(opts...),\n\t)\n\tmemoServiceUpdateMemoHandler := connect.NewUnaryHandler(\n\t\tMemoServiceUpdateMemoProcedure,\n\t\tsvc.UpdateMemo,\n\t\tconnect.WithSchema(memoServiceMethods.ByName(\"UpdateMemo\")),\n\t\tconnect.WithHandlerOptions(opts...),\n\t)\n\tmemoServiceDeleteMemoHandler := connect.NewUnaryHandler(\n\t\tMemoServiceDeleteMemoProcedure,\n\t\tsvc.DeleteMemo,\n\t\tconnect.WithSchema(memoServiceMethods.ByName(\"DeleteMemo\")),\n\t\tconnect.WithHandlerOptions(opts...),\n\t)\n\tmemoServiceSetMemoAttachmentsHandler := connect.NewUnaryHandler(\n\t\tMemoServiceSetMemoAttachmentsProcedure,\n\t\tsvc.SetMemoAttachments,\n\t\tconnect.WithSchema(memoServiceMethods.ByName(\"SetMemoAttachments\")),\n\t\tconnect.WithHandlerOptions(opts...),\n\t)\n\tmemoServiceListMemoAttachmentsHandler := connect.NewUnaryHandler(\n\t\tMemoServiceListMemoAttachmentsProcedure,\n\t\tsvc.ListMemoAttachments,\n\t\tconnect.WithSchema(memoServiceMethods.ByName(\"ListMemoAttachments\")),\n\t\tconnect.WithHandlerOptions(opts...),\n\t)\n\tmemoServiceSetMemoRelationsHandler := connect.NewUnaryHandler(\n\t\tMemoServiceSetMemoRelationsProcedure,\n\t\tsvc.SetMemoRelations,\n\t\tconnect.WithSchema(memoServiceMethods.ByName(\"SetMemoRelations\")),\n\t\tconnect.WithHandlerOptions(opts...),\n\t)\n\tmemoServiceListMemoRelationsHandler := connect.NewUnaryHandler(\n\t\tMemoServiceListMemoRelationsProcedure,\n\t\tsvc.ListMemoRelations,\n\t\tconnect.WithSchema(memoServiceMethods.ByName(\"ListMemoRelations\")),\n\t\tconnect.WithHandlerOptions(opts...),\n\t)\n\tmemoServiceCreateMemoCommentHandler := connect.NewUnaryHandler(\n\t\tMemoServiceCreateMemoCommentProcedure,\n\t\tsvc.CreateMemoComment,\n\t\tconnect.WithSchema(memoServiceMethods.ByName(\"CreateMemoComment\")),\n\t\tconnect.WithHandlerOptions(opts...),\n\t)\n\tmemoServiceListMemoCommentsHandler := connect.NewUnaryHandler(\n\t\tMemoServiceListMemoCommentsProcedure,\n\t\tsvc.ListMemoComments,\n\t\tconnect.WithSchema(memoServiceMethods.ByName(\"ListMemoComments\")),\n\t\tconnect.WithHandlerOptions(opts...),\n\t)\n\tmemoServiceListMemoReactionsHandler := connect.NewUnaryHandler(\n\t\tMemoServiceListMemoReactionsProcedure,\n\t\tsvc.ListMemoReactions,\n\t\tconnect.WithSchema(memoServiceMethods.ByName(\"ListMemoReactions\")),\n\t\tconnect.WithHandlerOptions(opts...),\n\t)\n\tmemoServiceUpsertMemoReactionHandler := connect.NewUnaryHandler(\n\t\tMemoServiceUpsertMemoReactionProcedure,\n\t\tsvc.UpsertMemoReaction,\n\t\tconnect.WithSchema(memoServiceMethods.ByName(\"UpsertMemoReaction\")),\n\t\tconnect.WithHandlerOptions(opts...),\n\t)\n\tmemoServiceDeleteMemoReactionHandler := connect.NewUnaryHandler(\n\t\tMemoServiceDeleteMemoReactionProcedure,\n\t\tsvc.DeleteMemoReaction,\n\t\tconnect.WithSchema(memoServiceMethods.ByName(\"DeleteMemoReaction\")),\n\t\tconnect.WithHandlerOptions(opts...),\n\t)\n\tmemoServiceCreateMemoShareHandler := connect.NewUnaryHandler(\n\t\tMemoServiceCreateMemoShareProcedure,\n\t\tsvc.CreateMemoShare,\n\t\tconnect.WithSchema(memoServiceMethods.ByName(\"CreateMemoShare\")),\n\t\tconnect.WithHandlerOptions(opts...),\n\t)\n\tmemoServiceListMemoSharesHandler := connect.NewUnaryHandler(\n\t\tMemoServiceListMemoSharesProcedure,\n\t\tsvc.ListMemoShares,\n\t\tconnect.WithSchema(memoServiceMethods.ByName(\"ListMemoShares\")),\n\t\tconnect.WithHandlerOptions(opts...),\n\t)\n\tmemoServiceDeleteMemoShareHandler := connect.NewUnaryHandler(\n\t\tMemoServiceDeleteMemoShareProcedure,\n\t\tsvc.DeleteMemoShare,\n\t\tconnect.WithSchema(memoServiceMethods.ByName(\"DeleteMemoShare\")),\n\t\tconnect.WithHandlerOptions(opts...),\n\t)\n\tmemoServiceGetMemoByShareHandler := connect.NewUnaryHandler(\n\t\tMemoServiceGetMemoByShareProcedure,\n\t\tsvc.GetMemoByShare,\n\t\tconnect.WithSchema(memoServiceMethods.ByName(\"GetMemoByShare\")),\n\t\tconnect.WithHandlerOptions(opts...),\n\t)\n\treturn \"/memos.api.v1.MemoService/\", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tswitch r.URL.Path {\n\t\tcase MemoServiceCreateMemoProcedure:\n\t\t\tmemoServiceCreateMemoHandler.ServeHTTP(w, r)\n\t\tcase MemoServiceListMemosProcedure:\n\t\t\tmemoServiceListMemosHandler.ServeHTTP(w, r)\n\t\tcase MemoServiceGetMemoProcedure:\n\t\t\tmemoServiceGetMemoHandler.ServeHTTP(w, r)\n\t\tcase MemoServiceUpdateMemoProcedure:\n\t\t\tmemoServiceUpdateMemoHandler.ServeHTTP(w, r)\n\t\tcase MemoServiceDeleteMemoProcedure:\n\t\t\tmemoServiceDeleteMemoHandler.ServeHTTP(w, r)\n\t\tcase MemoServiceSetMemoAttachmentsProcedure:\n\t\t\tmemoServiceSetMemoAttachmentsHandler.ServeHTTP(w, r)\n\t\tcase MemoServiceListMemoAttachmentsProcedure:\n\t\t\tmemoServiceListMemoAttachmentsHandler.ServeHTTP(w, r)\n\t\tcase MemoServiceSetMemoRelationsProcedure:\n\t\t\tmemoServiceSetMemoRelationsHandler.ServeHTTP(w, r)\n\t\tcase MemoServiceListMemoRelationsProcedure:\n\t\t\tmemoServiceListMemoRelationsHandler.ServeHTTP(w, r)\n\t\tcase MemoServiceCreateMemoCommentProcedure:\n\t\t\tmemoServiceCreateMemoCommentHandler.ServeHTTP(w, r)\n\t\tcase MemoServiceListMemoCommentsProcedure:\n\t\t\tmemoServiceListMemoCommentsHandler.ServeHTTP(w, r)\n\t\tcase MemoServiceListMemoReactionsProcedure:\n\t\t\tmemoServiceListMemoReactionsHandler.ServeHTTP(w, r)\n\t\tcase MemoServiceUpsertMemoReactionProcedure:\n\t\t\tmemoServiceUpsertMemoReactionHandler.ServeHTTP(w, r)\n\t\tcase MemoServiceDeleteMemoReactionProcedure:\n\t\t\tmemoServiceDeleteMemoReactionHandler.ServeHTTP(w, r)\n\t\tcase MemoServiceCreateMemoShareProcedure:\n\t\t\tmemoServiceCreateMemoShareHandler.ServeHTTP(w, r)\n\t\tcase MemoServiceListMemoSharesProcedure:\n\t\t\tmemoServiceListMemoSharesHandler.ServeHTTP(w, r)\n\t\tcase MemoServiceDeleteMemoShareProcedure:\n\t\t\tmemoServiceDeleteMemoShareHandler.ServeHTTP(w, r)\n\t\tcase MemoServiceGetMemoByShareProcedure:\n\t\t\tmemoServiceGetMemoByShareHandler.ServeHTTP(w, r)\n\t\tdefault:\n\t\t\thttp.NotFound(w, r)\n\t\t}\n\t})\n}\n\n// UnimplementedMemoServiceHandler returns CodeUnimplemented from all methods.\ntype UnimplementedMemoServiceHandler struct{}\n\nfunc (UnimplementedMemoServiceHandler) CreateMemo(context.Context, *connect.Request[v1.CreateMemoRequest]) (*connect.Response[v1.Memo], error) {\n\treturn nil, connect.NewError(connect.CodeUnimplemented, errors.New(\"memos.api.v1.MemoService.CreateMemo is not implemented\"))\n}\n\nfunc (UnimplementedMemoServiceHandler) ListMemos(context.Context, *connect.Request[v1.ListMemosRequest]) (*connect.Response[v1.ListMemosResponse], error) {\n\treturn nil, connect.NewError(connect.CodeUnimplemented, errors.New(\"memos.api.v1.MemoService.ListMemos is not implemented\"))\n}\n\nfunc (UnimplementedMemoServiceHandler) GetMemo(context.Context, *connect.Request[v1.GetMemoRequest]) (*connect.Response[v1.Memo], error) {\n\treturn nil, connect.NewError(connect.CodeUnimplemented, errors.New(\"memos.api.v1.MemoService.GetMemo is not implemented\"))\n}\n\nfunc (UnimplementedMemoServiceHandler) UpdateMemo(context.Context, *connect.Request[v1.UpdateMemoRequest]) (*connect.Response[v1.Memo], error) {\n\treturn nil, connect.NewError(connect.CodeUnimplemented, errors.New(\"memos.api.v1.MemoService.UpdateMemo is not implemented\"))\n}\n\nfunc (UnimplementedMemoServiceHandler) DeleteMemo(context.Context, *connect.Request[v1.DeleteMemoRequest]) (*connect.Response[emptypb.Empty], error) {\n\treturn nil, connect.NewError(connect.CodeUnimplemented, errors.New(\"memos.api.v1.MemoService.DeleteMemo is not implemented\"))\n}\n\nfunc (UnimplementedMemoServiceHandler) SetMemoAttachments(context.Context, *connect.Request[v1.SetMemoAttachmentsRequest]) (*connect.Response[emptypb.Empty], error) {\n\treturn nil, connect.NewError(connect.CodeUnimplemented, errors.New(\"memos.api.v1.MemoService.SetMemoAttachments is not implemented\"))\n}\n\nfunc (UnimplementedMemoServiceHandler) ListMemoAttachments(context.Context, *connect.Request[v1.ListMemoAttachmentsRequest]) (*connect.Response[v1.ListMemoAttachmentsResponse], error) {\n\treturn nil, connect.NewError(connect.CodeUnimplemented, errors.New(\"memos.api.v1.MemoService.ListMemoAttachments is not implemented\"))\n}\n\nfunc (UnimplementedMemoServiceHandler) SetMemoRelations(context.Context, *connect.Request[v1.SetMemoRelationsRequest]) (*connect.Response[emptypb.Empty], error) {\n\treturn nil, connect.NewError(connect.CodeUnimplemented, errors.New(\"memos.api.v1.MemoService.SetMemoRelations is not implemented\"))\n}\n\nfunc (UnimplementedMemoServiceHandler) ListMemoRelations(context.Context, *connect.Request[v1.ListMemoRelationsRequest]) (*connect.Response[v1.ListMemoRelationsResponse], error) {\n\treturn nil, connect.NewError(connect.CodeUnimplemented, errors.New(\"memos.api.v1.MemoService.ListMemoRelations is not implemented\"))\n}\n\nfunc (UnimplementedMemoServiceHandler) CreateMemoComment(context.Context, *connect.Request[v1.CreateMemoCommentRequest]) (*connect.Response[v1.Memo], error) {\n\treturn nil, connect.NewError(connect.CodeUnimplemented, errors.New(\"memos.api.v1.MemoService.CreateMemoComment is not implemented\"))\n}\n\nfunc (UnimplementedMemoServiceHandler) ListMemoComments(context.Context, *connect.Request[v1.ListMemoCommentsRequest]) (*connect.Response[v1.ListMemoCommentsResponse], error) {\n\treturn nil, connect.NewError(connect.CodeUnimplemented, errors.New(\"memos.api.v1.MemoService.ListMemoComments is not implemented\"))\n}\n\nfunc (UnimplementedMemoServiceHandler) ListMemoReactions(context.Context, *connect.Request[v1.ListMemoReactionsRequest]) (*connect.Response[v1.ListMemoReactionsResponse], error) {\n\treturn nil, connect.NewError(connect.CodeUnimplemented, errors.New(\"memos.api.v1.MemoService.ListMemoReactions is not implemented\"))\n}\n\nfunc (UnimplementedMemoServiceHandler) UpsertMemoReaction(context.Context, *connect.Request[v1.UpsertMemoReactionRequest]) (*connect.Response[v1.Reaction], error) {\n\treturn nil, connect.NewError(connect.CodeUnimplemented, errors.New(\"memos.api.v1.MemoService.UpsertMemoReaction is not implemented\"))\n}\n\nfunc (UnimplementedMemoServiceHandler) DeleteMemoReaction(context.Context, *connect.Request[v1.DeleteMemoReactionRequest]) (*connect.Response[emptypb.Empty], error) {\n\treturn nil, connect.NewError(connect.CodeUnimplemented, errors.New(\"memos.api.v1.MemoService.DeleteMemoReaction is not implemented\"))\n}\n\nfunc (UnimplementedMemoServiceHandler) CreateMemoShare(context.Context, *connect.Request[v1.CreateMemoShareRequest]) (*connect.Response[v1.MemoShare], error) {\n\treturn nil, connect.NewError(connect.CodeUnimplemented, errors.New(\"memos.api.v1.MemoService.CreateMemoShare is not implemented\"))\n}\n\nfunc (UnimplementedMemoServiceHandler) ListMemoShares(context.Context, *connect.Request[v1.ListMemoSharesRequest]) (*connect.Response[v1.ListMemoSharesResponse], error) {\n\treturn nil, connect.NewError(connect.CodeUnimplemented, errors.New(\"memos.api.v1.MemoService.ListMemoShares is not implemented\"))\n}\n\nfunc (UnimplementedMemoServiceHandler) DeleteMemoShare(context.Context, *connect.Request[v1.DeleteMemoShareRequest]) (*connect.Response[emptypb.Empty], error) {\n\treturn nil, connect.NewError(connect.CodeUnimplemented, errors.New(\"memos.api.v1.MemoService.DeleteMemoShare is not implemented\"))\n}\n\nfunc (UnimplementedMemoServiceHandler) GetMemoByShare(context.Context, *connect.Request[v1.GetMemoByShareRequest]) (*connect.Response[v1.Memo], error) {\n\treturn nil, connect.NewError(connect.CodeUnimplemented, errors.New(\"memos.api.v1.MemoService.GetMemoByShare is not implemented\"))\n}\n"
  },
  {
    "path": "proto/gen/api/v1/apiv1connect/shortcut_service.connect.go",
    "content": "// Code generated by protoc-gen-connect-go. DO NOT EDIT.\n//\n// Source: api/v1/shortcut_service.proto\n\npackage apiv1connect\n\nimport (\n\tconnect \"connectrpc.com/connect\"\n\tcontext \"context\"\n\terrors \"errors\"\n\tv1 \"github.com/usememos/memos/proto/gen/api/v1\"\n\temptypb \"google.golang.org/protobuf/types/known/emptypb\"\n\thttp \"net/http\"\n\tstrings \"strings\"\n)\n\n// This is a compile-time assertion to ensure that this generated file and the connect package are\n// compatible. If you get a compiler error that this constant is not defined, this code was\n// generated with a version of connect newer than the one compiled into your binary. You can fix the\n// problem by either regenerating this code with an older version of connect or updating the connect\n// version compiled into your binary.\nconst _ = connect.IsAtLeastVersion1_13_0\n\nconst (\n\t// ShortcutServiceName is the fully-qualified name of the ShortcutService service.\n\tShortcutServiceName = \"memos.api.v1.ShortcutService\"\n)\n\n// These constants are the fully-qualified names of the RPCs defined in this package. They're\n// exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route.\n//\n// Note that these are different from the fully-qualified method names used by\n// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to\n// reflection-formatted method names, remove the leading slash and convert the remaining slash to a\n// period.\nconst (\n\t// ShortcutServiceListShortcutsProcedure is the fully-qualified name of the ShortcutService's\n\t// ListShortcuts RPC.\n\tShortcutServiceListShortcutsProcedure = \"/memos.api.v1.ShortcutService/ListShortcuts\"\n\t// ShortcutServiceGetShortcutProcedure is the fully-qualified name of the ShortcutService's\n\t// GetShortcut RPC.\n\tShortcutServiceGetShortcutProcedure = \"/memos.api.v1.ShortcutService/GetShortcut\"\n\t// ShortcutServiceCreateShortcutProcedure is the fully-qualified name of the ShortcutService's\n\t// CreateShortcut RPC.\n\tShortcutServiceCreateShortcutProcedure = \"/memos.api.v1.ShortcutService/CreateShortcut\"\n\t// ShortcutServiceUpdateShortcutProcedure is the fully-qualified name of the ShortcutService's\n\t// UpdateShortcut RPC.\n\tShortcutServiceUpdateShortcutProcedure = \"/memos.api.v1.ShortcutService/UpdateShortcut\"\n\t// ShortcutServiceDeleteShortcutProcedure is the fully-qualified name of the ShortcutService's\n\t// DeleteShortcut RPC.\n\tShortcutServiceDeleteShortcutProcedure = \"/memos.api.v1.ShortcutService/DeleteShortcut\"\n)\n\n// ShortcutServiceClient is a client for the memos.api.v1.ShortcutService service.\ntype ShortcutServiceClient interface {\n\t// ListShortcuts returns a list of shortcuts for a user.\n\tListShortcuts(context.Context, *connect.Request[v1.ListShortcutsRequest]) (*connect.Response[v1.ListShortcutsResponse], error)\n\t// GetShortcut gets a shortcut by name.\n\tGetShortcut(context.Context, *connect.Request[v1.GetShortcutRequest]) (*connect.Response[v1.Shortcut], error)\n\t// CreateShortcut creates a new shortcut for a user.\n\tCreateShortcut(context.Context, *connect.Request[v1.CreateShortcutRequest]) (*connect.Response[v1.Shortcut], error)\n\t// UpdateShortcut updates a shortcut for a user.\n\tUpdateShortcut(context.Context, *connect.Request[v1.UpdateShortcutRequest]) (*connect.Response[v1.Shortcut], error)\n\t// DeleteShortcut deletes a shortcut for a user.\n\tDeleteShortcut(context.Context, *connect.Request[v1.DeleteShortcutRequest]) (*connect.Response[emptypb.Empty], error)\n}\n\n// NewShortcutServiceClient constructs a client for the memos.api.v1.ShortcutService service. By\n// default, it uses the Connect protocol with the binary Protobuf Codec, asks for gzipped responses,\n// and sends uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the\n// connect.WithGRPC() or connect.WithGRPCWeb() options.\n//\n// The URL supplied here should be the base URL for the Connect or gRPC server (for example,\n// http://api.acme.com or https://acme.com/grpc).\nfunc NewShortcutServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) ShortcutServiceClient {\n\tbaseURL = strings.TrimRight(baseURL, \"/\")\n\tshortcutServiceMethods := v1.File_api_v1_shortcut_service_proto.Services().ByName(\"ShortcutService\").Methods()\n\treturn &shortcutServiceClient{\n\t\tlistShortcuts: connect.NewClient[v1.ListShortcutsRequest, v1.ListShortcutsResponse](\n\t\t\thttpClient,\n\t\t\tbaseURL+ShortcutServiceListShortcutsProcedure,\n\t\t\tconnect.WithSchema(shortcutServiceMethods.ByName(\"ListShortcuts\")),\n\t\t\tconnect.WithClientOptions(opts...),\n\t\t),\n\t\tgetShortcut: connect.NewClient[v1.GetShortcutRequest, v1.Shortcut](\n\t\t\thttpClient,\n\t\t\tbaseURL+ShortcutServiceGetShortcutProcedure,\n\t\t\tconnect.WithSchema(shortcutServiceMethods.ByName(\"GetShortcut\")),\n\t\t\tconnect.WithClientOptions(opts...),\n\t\t),\n\t\tcreateShortcut: connect.NewClient[v1.CreateShortcutRequest, v1.Shortcut](\n\t\t\thttpClient,\n\t\t\tbaseURL+ShortcutServiceCreateShortcutProcedure,\n\t\t\tconnect.WithSchema(shortcutServiceMethods.ByName(\"CreateShortcut\")),\n\t\t\tconnect.WithClientOptions(opts...),\n\t\t),\n\t\tupdateShortcut: connect.NewClient[v1.UpdateShortcutRequest, v1.Shortcut](\n\t\t\thttpClient,\n\t\t\tbaseURL+ShortcutServiceUpdateShortcutProcedure,\n\t\t\tconnect.WithSchema(shortcutServiceMethods.ByName(\"UpdateShortcut\")),\n\t\t\tconnect.WithClientOptions(opts...),\n\t\t),\n\t\tdeleteShortcut: connect.NewClient[v1.DeleteShortcutRequest, emptypb.Empty](\n\t\t\thttpClient,\n\t\t\tbaseURL+ShortcutServiceDeleteShortcutProcedure,\n\t\t\tconnect.WithSchema(shortcutServiceMethods.ByName(\"DeleteShortcut\")),\n\t\t\tconnect.WithClientOptions(opts...),\n\t\t),\n\t}\n}\n\n// shortcutServiceClient implements ShortcutServiceClient.\ntype shortcutServiceClient struct {\n\tlistShortcuts  *connect.Client[v1.ListShortcutsRequest, v1.ListShortcutsResponse]\n\tgetShortcut    *connect.Client[v1.GetShortcutRequest, v1.Shortcut]\n\tcreateShortcut *connect.Client[v1.CreateShortcutRequest, v1.Shortcut]\n\tupdateShortcut *connect.Client[v1.UpdateShortcutRequest, v1.Shortcut]\n\tdeleteShortcut *connect.Client[v1.DeleteShortcutRequest, emptypb.Empty]\n}\n\n// ListShortcuts calls memos.api.v1.ShortcutService.ListShortcuts.\nfunc (c *shortcutServiceClient) ListShortcuts(ctx context.Context, req *connect.Request[v1.ListShortcutsRequest]) (*connect.Response[v1.ListShortcutsResponse], error) {\n\treturn c.listShortcuts.CallUnary(ctx, req)\n}\n\n// GetShortcut calls memos.api.v1.ShortcutService.GetShortcut.\nfunc (c *shortcutServiceClient) GetShortcut(ctx context.Context, req *connect.Request[v1.GetShortcutRequest]) (*connect.Response[v1.Shortcut], error) {\n\treturn c.getShortcut.CallUnary(ctx, req)\n}\n\n// CreateShortcut calls memos.api.v1.ShortcutService.CreateShortcut.\nfunc (c *shortcutServiceClient) CreateShortcut(ctx context.Context, req *connect.Request[v1.CreateShortcutRequest]) (*connect.Response[v1.Shortcut], error) {\n\treturn c.createShortcut.CallUnary(ctx, req)\n}\n\n// UpdateShortcut calls memos.api.v1.ShortcutService.UpdateShortcut.\nfunc (c *shortcutServiceClient) UpdateShortcut(ctx context.Context, req *connect.Request[v1.UpdateShortcutRequest]) (*connect.Response[v1.Shortcut], error) {\n\treturn c.updateShortcut.CallUnary(ctx, req)\n}\n\n// DeleteShortcut calls memos.api.v1.ShortcutService.DeleteShortcut.\nfunc (c *shortcutServiceClient) DeleteShortcut(ctx context.Context, req *connect.Request[v1.DeleteShortcutRequest]) (*connect.Response[emptypb.Empty], error) {\n\treturn c.deleteShortcut.CallUnary(ctx, req)\n}\n\n// ShortcutServiceHandler is an implementation of the memos.api.v1.ShortcutService service.\ntype ShortcutServiceHandler interface {\n\t// ListShortcuts returns a list of shortcuts for a user.\n\tListShortcuts(context.Context, *connect.Request[v1.ListShortcutsRequest]) (*connect.Response[v1.ListShortcutsResponse], error)\n\t// GetShortcut gets a shortcut by name.\n\tGetShortcut(context.Context, *connect.Request[v1.GetShortcutRequest]) (*connect.Response[v1.Shortcut], error)\n\t// CreateShortcut creates a new shortcut for a user.\n\tCreateShortcut(context.Context, *connect.Request[v1.CreateShortcutRequest]) (*connect.Response[v1.Shortcut], error)\n\t// UpdateShortcut updates a shortcut for a user.\n\tUpdateShortcut(context.Context, *connect.Request[v1.UpdateShortcutRequest]) (*connect.Response[v1.Shortcut], error)\n\t// DeleteShortcut deletes a shortcut for a user.\n\tDeleteShortcut(context.Context, *connect.Request[v1.DeleteShortcutRequest]) (*connect.Response[emptypb.Empty], error)\n}\n\n// NewShortcutServiceHandler builds an HTTP handler from the service implementation. It returns the\n// path on which to mount the handler and the handler itself.\n//\n// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf\n// and JSON codecs. They also support gzip compression.\nfunc NewShortcutServiceHandler(svc ShortcutServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) {\n\tshortcutServiceMethods := v1.File_api_v1_shortcut_service_proto.Services().ByName(\"ShortcutService\").Methods()\n\tshortcutServiceListShortcutsHandler := connect.NewUnaryHandler(\n\t\tShortcutServiceListShortcutsProcedure,\n\t\tsvc.ListShortcuts,\n\t\tconnect.WithSchema(shortcutServiceMethods.ByName(\"ListShortcuts\")),\n\t\tconnect.WithHandlerOptions(opts...),\n\t)\n\tshortcutServiceGetShortcutHandler := connect.NewUnaryHandler(\n\t\tShortcutServiceGetShortcutProcedure,\n\t\tsvc.GetShortcut,\n\t\tconnect.WithSchema(shortcutServiceMethods.ByName(\"GetShortcut\")),\n\t\tconnect.WithHandlerOptions(opts...),\n\t)\n\tshortcutServiceCreateShortcutHandler := connect.NewUnaryHandler(\n\t\tShortcutServiceCreateShortcutProcedure,\n\t\tsvc.CreateShortcut,\n\t\tconnect.WithSchema(shortcutServiceMethods.ByName(\"CreateShortcut\")),\n\t\tconnect.WithHandlerOptions(opts...),\n\t)\n\tshortcutServiceUpdateShortcutHandler := connect.NewUnaryHandler(\n\t\tShortcutServiceUpdateShortcutProcedure,\n\t\tsvc.UpdateShortcut,\n\t\tconnect.WithSchema(shortcutServiceMethods.ByName(\"UpdateShortcut\")),\n\t\tconnect.WithHandlerOptions(opts...),\n\t)\n\tshortcutServiceDeleteShortcutHandler := connect.NewUnaryHandler(\n\t\tShortcutServiceDeleteShortcutProcedure,\n\t\tsvc.DeleteShortcut,\n\t\tconnect.WithSchema(shortcutServiceMethods.ByName(\"DeleteShortcut\")),\n\t\tconnect.WithHandlerOptions(opts...),\n\t)\n\treturn \"/memos.api.v1.ShortcutService/\", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tswitch r.URL.Path {\n\t\tcase ShortcutServiceListShortcutsProcedure:\n\t\t\tshortcutServiceListShortcutsHandler.ServeHTTP(w, r)\n\t\tcase ShortcutServiceGetShortcutProcedure:\n\t\t\tshortcutServiceGetShortcutHandler.ServeHTTP(w, r)\n\t\tcase ShortcutServiceCreateShortcutProcedure:\n\t\t\tshortcutServiceCreateShortcutHandler.ServeHTTP(w, r)\n\t\tcase ShortcutServiceUpdateShortcutProcedure:\n\t\t\tshortcutServiceUpdateShortcutHandler.ServeHTTP(w, r)\n\t\tcase ShortcutServiceDeleteShortcutProcedure:\n\t\t\tshortcutServiceDeleteShortcutHandler.ServeHTTP(w, r)\n\t\tdefault:\n\t\t\thttp.NotFound(w, r)\n\t\t}\n\t})\n}\n\n// UnimplementedShortcutServiceHandler returns CodeUnimplemented from all methods.\ntype UnimplementedShortcutServiceHandler struct{}\n\nfunc (UnimplementedShortcutServiceHandler) ListShortcuts(context.Context, *connect.Request[v1.ListShortcutsRequest]) (*connect.Response[v1.ListShortcutsResponse], error) {\n\treturn nil, connect.NewError(connect.CodeUnimplemented, errors.New(\"memos.api.v1.ShortcutService.ListShortcuts is not implemented\"))\n}\n\nfunc (UnimplementedShortcutServiceHandler) GetShortcut(context.Context, *connect.Request[v1.GetShortcutRequest]) (*connect.Response[v1.Shortcut], error) {\n\treturn nil, connect.NewError(connect.CodeUnimplemented, errors.New(\"memos.api.v1.ShortcutService.GetShortcut is not implemented\"))\n}\n\nfunc (UnimplementedShortcutServiceHandler) CreateShortcut(context.Context, *connect.Request[v1.CreateShortcutRequest]) (*connect.Response[v1.Shortcut], error) {\n\treturn nil, connect.NewError(connect.CodeUnimplemented, errors.New(\"memos.api.v1.ShortcutService.CreateShortcut is not implemented\"))\n}\n\nfunc (UnimplementedShortcutServiceHandler) UpdateShortcut(context.Context, *connect.Request[v1.UpdateShortcutRequest]) (*connect.Response[v1.Shortcut], error) {\n\treturn nil, connect.NewError(connect.CodeUnimplemented, errors.New(\"memos.api.v1.ShortcutService.UpdateShortcut is not implemented\"))\n}\n\nfunc (UnimplementedShortcutServiceHandler) DeleteShortcut(context.Context, *connect.Request[v1.DeleteShortcutRequest]) (*connect.Response[emptypb.Empty], error) {\n\treturn nil, connect.NewError(connect.CodeUnimplemented, errors.New(\"memos.api.v1.ShortcutService.DeleteShortcut is not implemented\"))\n}\n"
  },
  {
    "path": "proto/gen/api/v1/apiv1connect/user_service.connect.go",
    "content": "// Code generated by protoc-gen-connect-go. DO NOT EDIT.\n//\n// Source: api/v1/user_service.proto\n\npackage apiv1connect\n\nimport (\n\tconnect \"connectrpc.com/connect\"\n\tcontext \"context\"\n\terrors \"errors\"\n\tv1 \"github.com/usememos/memos/proto/gen/api/v1\"\n\temptypb \"google.golang.org/protobuf/types/known/emptypb\"\n\thttp \"net/http\"\n\tstrings \"strings\"\n)\n\n// This is a compile-time assertion to ensure that this generated file and the connect package are\n// compatible. If you get a compiler error that this constant is not defined, this code was\n// generated with a version of connect newer than the one compiled into your binary. You can fix the\n// problem by either regenerating this code with an older version of connect or updating the connect\n// version compiled into your binary.\nconst _ = connect.IsAtLeastVersion1_13_0\n\nconst (\n\t// UserServiceName is the fully-qualified name of the UserService service.\n\tUserServiceName = \"memos.api.v1.UserService\"\n)\n\n// These constants are the fully-qualified names of the RPCs defined in this package. They're\n// exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route.\n//\n// Note that these are different from the fully-qualified method names used by\n// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to\n// reflection-formatted method names, remove the leading slash and convert the remaining slash to a\n// period.\nconst (\n\t// UserServiceListUsersProcedure is the fully-qualified name of the UserService's ListUsers RPC.\n\tUserServiceListUsersProcedure = \"/memos.api.v1.UserService/ListUsers\"\n\t// UserServiceGetUserProcedure is the fully-qualified name of the UserService's GetUser RPC.\n\tUserServiceGetUserProcedure = \"/memos.api.v1.UserService/GetUser\"\n\t// UserServiceCreateUserProcedure is the fully-qualified name of the UserService's CreateUser RPC.\n\tUserServiceCreateUserProcedure = \"/memos.api.v1.UserService/CreateUser\"\n\t// UserServiceUpdateUserProcedure is the fully-qualified name of the UserService's UpdateUser RPC.\n\tUserServiceUpdateUserProcedure = \"/memos.api.v1.UserService/UpdateUser\"\n\t// UserServiceDeleteUserProcedure is the fully-qualified name of the UserService's DeleteUser RPC.\n\tUserServiceDeleteUserProcedure = \"/memos.api.v1.UserService/DeleteUser\"\n\t// UserServiceListAllUserStatsProcedure is the fully-qualified name of the UserService's\n\t// ListAllUserStats RPC.\n\tUserServiceListAllUserStatsProcedure = \"/memos.api.v1.UserService/ListAllUserStats\"\n\t// UserServiceGetUserStatsProcedure is the fully-qualified name of the UserService's GetUserStats\n\t// RPC.\n\tUserServiceGetUserStatsProcedure = \"/memos.api.v1.UserService/GetUserStats\"\n\t// UserServiceGetUserSettingProcedure is the fully-qualified name of the UserService's\n\t// GetUserSetting RPC.\n\tUserServiceGetUserSettingProcedure = \"/memos.api.v1.UserService/GetUserSetting\"\n\t// UserServiceUpdateUserSettingProcedure is the fully-qualified name of the UserService's\n\t// UpdateUserSetting RPC.\n\tUserServiceUpdateUserSettingProcedure = \"/memos.api.v1.UserService/UpdateUserSetting\"\n\t// UserServiceListUserSettingsProcedure is the fully-qualified name of the UserService's\n\t// ListUserSettings RPC.\n\tUserServiceListUserSettingsProcedure = \"/memos.api.v1.UserService/ListUserSettings\"\n\t// UserServiceListPersonalAccessTokensProcedure is the fully-qualified name of the UserService's\n\t// ListPersonalAccessTokens RPC.\n\tUserServiceListPersonalAccessTokensProcedure = \"/memos.api.v1.UserService/ListPersonalAccessTokens\"\n\t// UserServiceCreatePersonalAccessTokenProcedure is the fully-qualified name of the UserService's\n\t// CreatePersonalAccessToken RPC.\n\tUserServiceCreatePersonalAccessTokenProcedure = \"/memos.api.v1.UserService/CreatePersonalAccessToken\"\n\t// UserServiceDeletePersonalAccessTokenProcedure is the fully-qualified name of the UserService's\n\t// DeletePersonalAccessToken RPC.\n\tUserServiceDeletePersonalAccessTokenProcedure = \"/memos.api.v1.UserService/DeletePersonalAccessToken\"\n\t// UserServiceListUserWebhooksProcedure is the fully-qualified name of the UserService's\n\t// ListUserWebhooks RPC.\n\tUserServiceListUserWebhooksProcedure = \"/memos.api.v1.UserService/ListUserWebhooks\"\n\t// UserServiceCreateUserWebhookProcedure is the fully-qualified name of the UserService's\n\t// CreateUserWebhook RPC.\n\tUserServiceCreateUserWebhookProcedure = \"/memos.api.v1.UserService/CreateUserWebhook\"\n\t// UserServiceUpdateUserWebhookProcedure is the fully-qualified name of the UserService's\n\t// UpdateUserWebhook RPC.\n\tUserServiceUpdateUserWebhookProcedure = \"/memos.api.v1.UserService/UpdateUserWebhook\"\n\t// UserServiceDeleteUserWebhookProcedure is the fully-qualified name of the UserService's\n\t// DeleteUserWebhook RPC.\n\tUserServiceDeleteUserWebhookProcedure = \"/memos.api.v1.UserService/DeleteUserWebhook\"\n\t// UserServiceListUserNotificationsProcedure is the fully-qualified name of the UserService's\n\t// ListUserNotifications RPC.\n\tUserServiceListUserNotificationsProcedure = \"/memos.api.v1.UserService/ListUserNotifications\"\n\t// UserServiceUpdateUserNotificationProcedure is the fully-qualified name of the UserService's\n\t// UpdateUserNotification RPC.\n\tUserServiceUpdateUserNotificationProcedure = \"/memos.api.v1.UserService/UpdateUserNotification\"\n\t// UserServiceDeleteUserNotificationProcedure is the fully-qualified name of the UserService's\n\t// DeleteUserNotification RPC.\n\tUserServiceDeleteUserNotificationProcedure = \"/memos.api.v1.UserService/DeleteUserNotification\"\n)\n\n// UserServiceClient is a client for the memos.api.v1.UserService service.\ntype UserServiceClient interface {\n\t// ListUsers returns a list of users.\n\tListUsers(context.Context, *connect.Request[v1.ListUsersRequest]) (*connect.Response[v1.ListUsersResponse], error)\n\t// GetUser gets a user by ID or username.\n\t// Supports both numeric IDs and username strings:\n\t//   - users/{id}       (e.g., users/101)\n\t//   - users/{username} (e.g., users/steven)\n\tGetUser(context.Context, *connect.Request[v1.GetUserRequest]) (*connect.Response[v1.User], error)\n\t// CreateUser creates a new user.\n\tCreateUser(context.Context, *connect.Request[v1.CreateUserRequest]) (*connect.Response[v1.User], error)\n\t// UpdateUser updates a user.\n\tUpdateUser(context.Context, *connect.Request[v1.UpdateUserRequest]) (*connect.Response[v1.User], error)\n\t// DeleteUser deletes a user.\n\tDeleteUser(context.Context, *connect.Request[v1.DeleteUserRequest]) (*connect.Response[emptypb.Empty], error)\n\t// ListAllUserStats returns statistics for all users.\n\tListAllUserStats(context.Context, *connect.Request[v1.ListAllUserStatsRequest]) (*connect.Response[v1.ListAllUserStatsResponse], error)\n\t// GetUserStats returns statistics for a specific user.\n\tGetUserStats(context.Context, *connect.Request[v1.GetUserStatsRequest]) (*connect.Response[v1.UserStats], error)\n\t// GetUserSetting returns the user setting.\n\tGetUserSetting(context.Context, *connect.Request[v1.GetUserSettingRequest]) (*connect.Response[v1.UserSetting], error)\n\t// UpdateUserSetting updates the user setting.\n\tUpdateUserSetting(context.Context, *connect.Request[v1.UpdateUserSettingRequest]) (*connect.Response[v1.UserSetting], error)\n\t// ListUserSettings returns a list of user settings.\n\tListUserSettings(context.Context, *connect.Request[v1.ListUserSettingsRequest]) (*connect.Response[v1.ListUserSettingsResponse], error)\n\t// ListPersonalAccessTokens returns a list of Personal Access Tokens (PATs) for a user.\n\t// PATs are long-lived tokens for API/script access, distinct from short-lived JWT access tokens.\n\tListPersonalAccessTokens(context.Context, *connect.Request[v1.ListPersonalAccessTokensRequest]) (*connect.Response[v1.ListPersonalAccessTokensResponse], error)\n\t// CreatePersonalAccessToken creates a new Personal Access Token for a user.\n\t// The token value is only returned once upon creation.\n\tCreatePersonalAccessToken(context.Context, *connect.Request[v1.CreatePersonalAccessTokenRequest]) (*connect.Response[v1.CreatePersonalAccessTokenResponse], error)\n\t// DeletePersonalAccessToken deletes a Personal Access Token.\n\tDeletePersonalAccessToken(context.Context, *connect.Request[v1.DeletePersonalAccessTokenRequest]) (*connect.Response[emptypb.Empty], error)\n\t// ListUserWebhooks returns a list of webhooks for a user.\n\tListUserWebhooks(context.Context, *connect.Request[v1.ListUserWebhooksRequest]) (*connect.Response[v1.ListUserWebhooksResponse], error)\n\t// CreateUserWebhook creates a new webhook for a user.\n\tCreateUserWebhook(context.Context, *connect.Request[v1.CreateUserWebhookRequest]) (*connect.Response[v1.UserWebhook], error)\n\t// UpdateUserWebhook updates an existing webhook for a user.\n\tUpdateUserWebhook(context.Context, *connect.Request[v1.UpdateUserWebhookRequest]) (*connect.Response[v1.UserWebhook], error)\n\t// DeleteUserWebhook deletes a webhook for a user.\n\tDeleteUserWebhook(context.Context, *connect.Request[v1.DeleteUserWebhookRequest]) (*connect.Response[emptypb.Empty], error)\n\t// ListUserNotifications lists notifications for a user.\n\tListUserNotifications(context.Context, *connect.Request[v1.ListUserNotificationsRequest]) (*connect.Response[v1.ListUserNotificationsResponse], error)\n\t// UpdateUserNotification updates a notification.\n\tUpdateUserNotification(context.Context, *connect.Request[v1.UpdateUserNotificationRequest]) (*connect.Response[v1.UserNotification], error)\n\t// DeleteUserNotification deletes a notification.\n\tDeleteUserNotification(context.Context, *connect.Request[v1.DeleteUserNotificationRequest]) (*connect.Response[emptypb.Empty], error)\n}\n\n// NewUserServiceClient constructs a client for the memos.api.v1.UserService service. By default, it\n// uses the Connect protocol with the binary Protobuf Codec, asks for gzipped responses, and sends\n// uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the connect.WithGRPC() or\n// connect.WithGRPCWeb() options.\n//\n// The URL supplied here should be the base URL for the Connect or gRPC server (for example,\n// http://api.acme.com or https://acme.com/grpc).\nfunc NewUserServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) UserServiceClient {\n\tbaseURL = strings.TrimRight(baseURL, \"/\")\n\tuserServiceMethods := v1.File_api_v1_user_service_proto.Services().ByName(\"UserService\").Methods()\n\treturn &userServiceClient{\n\t\tlistUsers: connect.NewClient[v1.ListUsersRequest, v1.ListUsersResponse](\n\t\t\thttpClient,\n\t\t\tbaseURL+UserServiceListUsersProcedure,\n\t\t\tconnect.WithSchema(userServiceMethods.ByName(\"ListUsers\")),\n\t\t\tconnect.WithClientOptions(opts...),\n\t\t),\n\t\tgetUser: connect.NewClient[v1.GetUserRequest, v1.User](\n\t\t\thttpClient,\n\t\t\tbaseURL+UserServiceGetUserProcedure,\n\t\t\tconnect.WithSchema(userServiceMethods.ByName(\"GetUser\")),\n\t\t\tconnect.WithClientOptions(opts...),\n\t\t),\n\t\tcreateUser: connect.NewClient[v1.CreateUserRequest, v1.User](\n\t\t\thttpClient,\n\t\t\tbaseURL+UserServiceCreateUserProcedure,\n\t\t\tconnect.WithSchema(userServiceMethods.ByName(\"CreateUser\")),\n\t\t\tconnect.WithClientOptions(opts...),\n\t\t),\n\t\tupdateUser: connect.NewClient[v1.UpdateUserRequest, v1.User](\n\t\t\thttpClient,\n\t\t\tbaseURL+UserServiceUpdateUserProcedure,\n\t\t\tconnect.WithSchema(userServiceMethods.ByName(\"UpdateUser\")),\n\t\t\tconnect.WithClientOptions(opts...),\n\t\t),\n\t\tdeleteUser: connect.NewClient[v1.DeleteUserRequest, emptypb.Empty](\n\t\t\thttpClient,\n\t\t\tbaseURL+UserServiceDeleteUserProcedure,\n\t\t\tconnect.WithSchema(userServiceMethods.ByName(\"DeleteUser\")),\n\t\t\tconnect.WithClientOptions(opts...),\n\t\t),\n\t\tlistAllUserStats: connect.NewClient[v1.ListAllUserStatsRequest, v1.ListAllUserStatsResponse](\n\t\t\thttpClient,\n\t\t\tbaseURL+UserServiceListAllUserStatsProcedure,\n\t\t\tconnect.WithSchema(userServiceMethods.ByName(\"ListAllUserStats\")),\n\t\t\tconnect.WithClientOptions(opts...),\n\t\t),\n\t\tgetUserStats: connect.NewClient[v1.GetUserStatsRequest, v1.UserStats](\n\t\t\thttpClient,\n\t\t\tbaseURL+UserServiceGetUserStatsProcedure,\n\t\t\tconnect.WithSchema(userServiceMethods.ByName(\"GetUserStats\")),\n\t\t\tconnect.WithClientOptions(opts...),\n\t\t),\n\t\tgetUserSetting: connect.NewClient[v1.GetUserSettingRequest, v1.UserSetting](\n\t\t\thttpClient,\n\t\t\tbaseURL+UserServiceGetUserSettingProcedure,\n\t\t\tconnect.WithSchema(userServiceMethods.ByName(\"GetUserSetting\")),\n\t\t\tconnect.WithClientOptions(opts...),\n\t\t),\n\t\tupdateUserSetting: connect.NewClient[v1.UpdateUserSettingRequest, v1.UserSetting](\n\t\t\thttpClient,\n\t\t\tbaseURL+UserServiceUpdateUserSettingProcedure,\n\t\t\tconnect.WithSchema(userServiceMethods.ByName(\"UpdateUserSetting\")),\n\t\t\tconnect.WithClientOptions(opts...),\n\t\t),\n\t\tlistUserSettings: connect.NewClient[v1.ListUserSettingsRequest, v1.ListUserSettingsResponse](\n\t\t\thttpClient,\n\t\t\tbaseURL+UserServiceListUserSettingsProcedure,\n\t\t\tconnect.WithSchema(userServiceMethods.ByName(\"ListUserSettings\")),\n\t\t\tconnect.WithClientOptions(opts...),\n\t\t),\n\t\tlistPersonalAccessTokens: connect.NewClient[v1.ListPersonalAccessTokensRequest, v1.ListPersonalAccessTokensResponse](\n\t\t\thttpClient,\n\t\t\tbaseURL+UserServiceListPersonalAccessTokensProcedure,\n\t\t\tconnect.WithSchema(userServiceMethods.ByName(\"ListPersonalAccessTokens\")),\n\t\t\tconnect.WithClientOptions(opts...),\n\t\t),\n\t\tcreatePersonalAccessToken: connect.NewClient[v1.CreatePersonalAccessTokenRequest, v1.CreatePersonalAccessTokenResponse](\n\t\t\thttpClient,\n\t\t\tbaseURL+UserServiceCreatePersonalAccessTokenProcedure,\n\t\t\tconnect.WithSchema(userServiceMethods.ByName(\"CreatePersonalAccessToken\")),\n\t\t\tconnect.WithClientOptions(opts...),\n\t\t),\n\t\tdeletePersonalAccessToken: connect.NewClient[v1.DeletePersonalAccessTokenRequest, emptypb.Empty](\n\t\t\thttpClient,\n\t\t\tbaseURL+UserServiceDeletePersonalAccessTokenProcedure,\n\t\t\tconnect.WithSchema(userServiceMethods.ByName(\"DeletePersonalAccessToken\")),\n\t\t\tconnect.WithClientOptions(opts...),\n\t\t),\n\t\tlistUserWebhooks: connect.NewClient[v1.ListUserWebhooksRequest, v1.ListUserWebhooksResponse](\n\t\t\thttpClient,\n\t\t\tbaseURL+UserServiceListUserWebhooksProcedure,\n\t\t\tconnect.WithSchema(userServiceMethods.ByName(\"ListUserWebhooks\")),\n\t\t\tconnect.WithClientOptions(opts...),\n\t\t),\n\t\tcreateUserWebhook: connect.NewClient[v1.CreateUserWebhookRequest, v1.UserWebhook](\n\t\t\thttpClient,\n\t\t\tbaseURL+UserServiceCreateUserWebhookProcedure,\n\t\t\tconnect.WithSchema(userServiceMethods.ByName(\"CreateUserWebhook\")),\n\t\t\tconnect.WithClientOptions(opts...),\n\t\t),\n\t\tupdateUserWebhook: connect.NewClient[v1.UpdateUserWebhookRequest, v1.UserWebhook](\n\t\t\thttpClient,\n\t\t\tbaseURL+UserServiceUpdateUserWebhookProcedure,\n\t\t\tconnect.WithSchema(userServiceMethods.ByName(\"UpdateUserWebhook\")),\n\t\t\tconnect.WithClientOptions(opts...),\n\t\t),\n\t\tdeleteUserWebhook: connect.NewClient[v1.DeleteUserWebhookRequest, emptypb.Empty](\n\t\t\thttpClient,\n\t\t\tbaseURL+UserServiceDeleteUserWebhookProcedure,\n\t\t\tconnect.WithSchema(userServiceMethods.ByName(\"DeleteUserWebhook\")),\n\t\t\tconnect.WithClientOptions(opts...),\n\t\t),\n\t\tlistUserNotifications: connect.NewClient[v1.ListUserNotificationsRequest, v1.ListUserNotificationsResponse](\n\t\t\thttpClient,\n\t\t\tbaseURL+UserServiceListUserNotificationsProcedure,\n\t\t\tconnect.WithSchema(userServiceMethods.ByName(\"ListUserNotifications\")),\n\t\t\tconnect.WithClientOptions(opts...),\n\t\t),\n\t\tupdateUserNotification: connect.NewClient[v1.UpdateUserNotificationRequest, v1.UserNotification](\n\t\t\thttpClient,\n\t\t\tbaseURL+UserServiceUpdateUserNotificationProcedure,\n\t\t\tconnect.WithSchema(userServiceMethods.ByName(\"UpdateUserNotification\")),\n\t\t\tconnect.WithClientOptions(opts...),\n\t\t),\n\t\tdeleteUserNotification: connect.NewClient[v1.DeleteUserNotificationRequest, emptypb.Empty](\n\t\t\thttpClient,\n\t\t\tbaseURL+UserServiceDeleteUserNotificationProcedure,\n\t\t\tconnect.WithSchema(userServiceMethods.ByName(\"DeleteUserNotification\")),\n\t\t\tconnect.WithClientOptions(opts...),\n\t\t),\n\t}\n}\n\n// userServiceClient implements UserServiceClient.\ntype userServiceClient struct {\n\tlistUsers                 *connect.Client[v1.ListUsersRequest, v1.ListUsersResponse]\n\tgetUser                   *connect.Client[v1.GetUserRequest, v1.User]\n\tcreateUser                *connect.Client[v1.CreateUserRequest, v1.User]\n\tupdateUser                *connect.Client[v1.UpdateUserRequest, v1.User]\n\tdeleteUser                *connect.Client[v1.DeleteUserRequest, emptypb.Empty]\n\tlistAllUserStats          *connect.Client[v1.ListAllUserStatsRequest, v1.ListAllUserStatsResponse]\n\tgetUserStats              *connect.Client[v1.GetUserStatsRequest, v1.UserStats]\n\tgetUserSetting            *connect.Client[v1.GetUserSettingRequest, v1.UserSetting]\n\tupdateUserSetting         *connect.Client[v1.UpdateUserSettingRequest, v1.UserSetting]\n\tlistUserSettings          *connect.Client[v1.ListUserSettingsRequest, v1.ListUserSettingsResponse]\n\tlistPersonalAccessTokens  *connect.Client[v1.ListPersonalAccessTokensRequest, v1.ListPersonalAccessTokensResponse]\n\tcreatePersonalAccessToken *connect.Client[v1.CreatePersonalAccessTokenRequest, v1.CreatePersonalAccessTokenResponse]\n\tdeletePersonalAccessToken *connect.Client[v1.DeletePersonalAccessTokenRequest, emptypb.Empty]\n\tlistUserWebhooks          *connect.Client[v1.ListUserWebhooksRequest, v1.ListUserWebhooksResponse]\n\tcreateUserWebhook         *connect.Client[v1.CreateUserWebhookRequest, v1.UserWebhook]\n\tupdateUserWebhook         *connect.Client[v1.UpdateUserWebhookRequest, v1.UserWebhook]\n\tdeleteUserWebhook         *connect.Client[v1.DeleteUserWebhookRequest, emptypb.Empty]\n\tlistUserNotifications     *connect.Client[v1.ListUserNotificationsRequest, v1.ListUserNotificationsResponse]\n\tupdateUserNotification    *connect.Client[v1.UpdateUserNotificationRequest, v1.UserNotification]\n\tdeleteUserNotification    *connect.Client[v1.DeleteUserNotificationRequest, emptypb.Empty]\n}\n\n// ListUsers calls memos.api.v1.UserService.ListUsers.\nfunc (c *userServiceClient) ListUsers(ctx context.Context, req *connect.Request[v1.ListUsersRequest]) (*connect.Response[v1.ListUsersResponse], error) {\n\treturn c.listUsers.CallUnary(ctx, req)\n}\n\n// GetUser calls memos.api.v1.UserService.GetUser.\nfunc (c *userServiceClient) GetUser(ctx context.Context, req *connect.Request[v1.GetUserRequest]) (*connect.Response[v1.User], error) {\n\treturn c.getUser.CallUnary(ctx, req)\n}\n\n// CreateUser calls memos.api.v1.UserService.CreateUser.\nfunc (c *userServiceClient) CreateUser(ctx context.Context, req *connect.Request[v1.CreateUserRequest]) (*connect.Response[v1.User], error) {\n\treturn c.createUser.CallUnary(ctx, req)\n}\n\n// UpdateUser calls memos.api.v1.UserService.UpdateUser.\nfunc (c *userServiceClient) UpdateUser(ctx context.Context, req *connect.Request[v1.UpdateUserRequest]) (*connect.Response[v1.User], error) {\n\treturn c.updateUser.CallUnary(ctx, req)\n}\n\n// DeleteUser calls memos.api.v1.UserService.DeleteUser.\nfunc (c *userServiceClient) DeleteUser(ctx context.Context, req *connect.Request[v1.DeleteUserRequest]) (*connect.Response[emptypb.Empty], error) {\n\treturn c.deleteUser.CallUnary(ctx, req)\n}\n\n// ListAllUserStats calls memos.api.v1.UserService.ListAllUserStats.\nfunc (c *userServiceClient) ListAllUserStats(ctx context.Context, req *connect.Request[v1.ListAllUserStatsRequest]) (*connect.Response[v1.ListAllUserStatsResponse], error) {\n\treturn c.listAllUserStats.CallUnary(ctx, req)\n}\n\n// GetUserStats calls memos.api.v1.UserService.GetUserStats.\nfunc (c *userServiceClient) GetUserStats(ctx context.Context, req *connect.Request[v1.GetUserStatsRequest]) (*connect.Response[v1.UserStats], error) {\n\treturn c.getUserStats.CallUnary(ctx, req)\n}\n\n// GetUserSetting calls memos.api.v1.UserService.GetUserSetting.\nfunc (c *userServiceClient) GetUserSetting(ctx context.Context, req *connect.Request[v1.GetUserSettingRequest]) (*connect.Response[v1.UserSetting], error) {\n\treturn c.getUserSetting.CallUnary(ctx, req)\n}\n\n// UpdateUserSetting calls memos.api.v1.UserService.UpdateUserSetting.\nfunc (c *userServiceClient) UpdateUserSetting(ctx context.Context, req *connect.Request[v1.UpdateUserSettingRequest]) (*connect.Response[v1.UserSetting], error) {\n\treturn c.updateUserSetting.CallUnary(ctx, req)\n}\n\n// ListUserSettings calls memos.api.v1.UserService.ListUserSettings.\nfunc (c *userServiceClient) ListUserSettings(ctx context.Context, req *connect.Request[v1.ListUserSettingsRequest]) (*connect.Response[v1.ListUserSettingsResponse], error) {\n\treturn c.listUserSettings.CallUnary(ctx, req)\n}\n\n// ListPersonalAccessTokens calls memos.api.v1.UserService.ListPersonalAccessTokens.\nfunc (c *userServiceClient) ListPersonalAccessTokens(ctx context.Context, req *connect.Request[v1.ListPersonalAccessTokensRequest]) (*connect.Response[v1.ListPersonalAccessTokensResponse], error) {\n\treturn c.listPersonalAccessTokens.CallUnary(ctx, req)\n}\n\n// CreatePersonalAccessToken calls memos.api.v1.UserService.CreatePersonalAccessToken.\nfunc (c *userServiceClient) CreatePersonalAccessToken(ctx context.Context, req *connect.Request[v1.CreatePersonalAccessTokenRequest]) (*connect.Response[v1.CreatePersonalAccessTokenResponse], error) {\n\treturn c.createPersonalAccessToken.CallUnary(ctx, req)\n}\n\n// DeletePersonalAccessToken calls memos.api.v1.UserService.DeletePersonalAccessToken.\nfunc (c *userServiceClient) DeletePersonalAccessToken(ctx context.Context, req *connect.Request[v1.DeletePersonalAccessTokenRequest]) (*connect.Response[emptypb.Empty], error) {\n\treturn c.deletePersonalAccessToken.CallUnary(ctx, req)\n}\n\n// ListUserWebhooks calls memos.api.v1.UserService.ListUserWebhooks.\nfunc (c *userServiceClient) ListUserWebhooks(ctx context.Context, req *connect.Request[v1.ListUserWebhooksRequest]) (*connect.Response[v1.ListUserWebhooksResponse], error) {\n\treturn c.listUserWebhooks.CallUnary(ctx, req)\n}\n\n// CreateUserWebhook calls memos.api.v1.UserService.CreateUserWebhook.\nfunc (c *userServiceClient) CreateUserWebhook(ctx context.Context, req *connect.Request[v1.CreateUserWebhookRequest]) (*connect.Response[v1.UserWebhook], error) {\n\treturn c.createUserWebhook.CallUnary(ctx, req)\n}\n\n// UpdateUserWebhook calls memos.api.v1.UserService.UpdateUserWebhook.\nfunc (c *userServiceClient) UpdateUserWebhook(ctx context.Context, req *connect.Request[v1.UpdateUserWebhookRequest]) (*connect.Response[v1.UserWebhook], error) {\n\treturn c.updateUserWebhook.CallUnary(ctx, req)\n}\n\n// DeleteUserWebhook calls memos.api.v1.UserService.DeleteUserWebhook.\nfunc (c *userServiceClient) DeleteUserWebhook(ctx context.Context, req *connect.Request[v1.DeleteUserWebhookRequest]) (*connect.Response[emptypb.Empty], error) {\n\treturn c.deleteUserWebhook.CallUnary(ctx, req)\n}\n\n// ListUserNotifications calls memos.api.v1.UserService.ListUserNotifications.\nfunc (c *userServiceClient) ListUserNotifications(ctx context.Context, req *connect.Request[v1.ListUserNotificationsRequest]) (*connect.Response[v1.ListUserNotificationsResponse], error) {\n\treturn c.listUserNotifications.CallUnary(ctx, req)\n}\n\n// UpdateUserNotification calls memos.api.v1.UserService.UpdateUserNotification.\nfunc (c *userServiceClient) UpdateUserNotification(ctx context.Context, req *connect.Request[v1.UpdateUserNotificationRequest]) (*connect.Response[v1.UserNotification], error) {\n\treturn c.updateUserNotification.CallUnary(ctx, req)\n}\n\n// DeleteUserNotification calls memos.api.v1.UserService.DeleteUserNotification.\nfunc (c *userServiceClient) DeleteUserNotification(ctx context.Context, req *connect.Request[v1.DeleteUserNotificationRequest]) (*connect.Response[emptypb.Empty], error) {\n\treturn c.deleteUserNotification.CallUnary(ctx, req)\n}\n\n// UserServiceHandler is an implementation of the memos.api.v1.UserService service.\ntype UserServiceHandler interface {\n\t// ListUsers returns a list of users.\n\tListUsers(context.Context, *connect.Request[v1.ListUsersRequest]) (*connect.Response[v1.ListUsersResponse], error)\n\t// GetUser gets a user by ID or username.\n\t// Supports both numeric IDs and username strings:\n\t//   - users/{id}       (e.g., users/101)\n\t//   - users/{username} (e.g., users/steven)\n\tGetUser(context.Context, *connect.Request[v1.GetUserRequest]) (*connect.Response[v1.User], error)\n\t// CreateUser creates a new user.\n\tCreateUser(context.Context, *connect.Request[v1.CreateUserRequest]) (*connect.Response[v1.User], error)\n\t// UpdateUser updates a user.\n\tUpdateUser(context.Context, *connect.Request[v1.UpdateUserRequest]) (*connect.Response[v1.User], error)\n\t// DeleteUser deletes a user.\n\tDeleteUser(context.Context, *connect.Request[v1.DeleteUserRequest]) (*connect.Response[emptypb.Empty], error)\n\t// ListAllUserStats returns statistics for all users.\n\tListAllUserStats(context.Context, *connect.Request[v1.ListAllUserStatsRequest]) (*connect.Response[v1.ListAllUserStatsResponse], error)\n\t// GetUserStats returns statistics for a specific user.\n\tGetUserStats(context.Context, *connect.Request[v1.GetUserStatsRequest]) (*connect.Response[v1.UserStats], error)\n\t// GetUserSetting returns the user setting.\n\tGetUserSetting(context.Context, *connect.Request[v1.GetUserSettingRequest]) (*connect.Response[v1.UserSetting], error)\n\t// UpdateUserSetting updates the user setting.\n\tUpdateUserSetting(context.Context, *connect.Request[v1.UpdateUserSettingRequest]) (*connect.Response[v1.UserSetting], error)\n\t// ListUserSettings returns a list of user settings.\n\tListUserSettings(context.Context, *connect.Request[v1.ListUserSettingsRequest]) (*connect.Response[v1.ListUserSettingsResponse], error)\n\t// ListPersonalAccessTokens returns a list of Personal Access Tokens (PATs) for a user.\n\t// PATs are long-lived tokens for API/script access, distinct from short-lived JWT access tokens.\n\tListPersonalAccessTokens(context.Context, *connect.Request[v1.ListPersonalAccessTokensRequest]) (*connect.Response[v1.ListPersonalAccessTokensResponse], error)\n\t// CreatePersonalAccessToken creates a new Personal Access Token for a user.\n\t// The token value is only returned once upon creation.\n\tCreatePersonalAccessToken(context.Context, *connect.Request[v1.CreatePersonalAccessTokenRequest]) (*connect.Response[v1.CreatePersonalAccessTokenResponse], error)\n\t// DeletePersonalAccessToken deletes a Personal Access Token.\n\tDeletePersonalAccessToken(context.Context, *connect.Request[v1.DeletePersonalAccessTokenRequest]) (*connect.Response[emptypb.Empty], error)\n\t// ListUserWebhooks returns a list of webhooks for a user.\n\tListUserWebhooks(context.Context, *connect.Request[v1.ListUserWebhooksRequest]) (*connect.Response[v1.ListUserWebhooksResponse], error)\n\t// CreateUserWebhook creates a new webhook for a user.\n\tCreateUserWebhook(context.Context, *connect.Request[v1.CreateUserWebhookRequest]) (*connect.Response[v1.UserWebhook], error)\n\t// UpdateUserWebhook updates an existing webhook for a user.\n\tUpdateUserWebhook(context.Context, *connect.Request[v1.UpdateUserWebhookRequest]) (*connect.Response[v1.UserWebhook], error)\n\t// DeleteUserWebhook deletes a webhook for a user.\n\tDeleteUserWebhook(context.Context, *connect.Request[v1.DeleteUserWebhookRequest]) (*connect.Response[emptypb.Empty], error)\n\t// ListUserNotifications lists notifications for a user.\n\tListUserNotifications(context.Context, *connect.Request[v1.ListUserNotificationsRequest]) (*connect.Response[v1.ListUserNotificationsResponse], error)\n\t// UpdateUserNotification updates a notification.\n\tUpdateUserNotification(context.Context, *connect.Request[v1.UpdateUserNotificationRequest]) (*connect.Response[v1.UserNotification], error)\n\t// DeleteUserNotification deletes a notification.\n\tDeleteUserNotification(context.Context, *connect.Request[v1.DeleteUserNotificationRequest]) (*connect.Response[emptypb.Empty], error)\n}\n\n// NewUserServiceHandler builds an HTTP handler from the service implementation. It returns the path\n// on which to mount the handler and the handler itself.\n//\n// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf\n// and JSON codecs. They also support gzip compression.\nfunc NewUserServiceHandler(svc UserServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) {\n\tuserServiceMethods := v1.File_api_v1_user_service_proto.Services().ByName(\"UserService\").Methods()\n\tuserServiceListUsersHandler := connect.NewUnaryHandler(\n\t\tUserServiceListUsersProcedure,\n\t\tsvc.ListUsers,\n\t\tconnect.WithSchema(userServiceMethods.ByName(\"ListUsers\")),\n\t\tconnect.WithHandlerOptions(opts...),\n\t)\n\tuserServiceGetUserHandler := connect.NewUnaryHandler(\n\t\tUserServiceGetUserProcedure,\n\t\tsvc.GetUser,\n\t\tconnect.WithSchema(userServiceMethods.ByName(\"GetUser\")),\n\t\tconnect.WithHandlerOptions(opts...),\n\t)\n\tuserServiceCreateUserHandler := connect.NewUnaryHandler(\n\t\tUserServiceCreateUserProcedure,\n\t\tsvc.CreateUser,\n\t\tconnect.WithSchema(userServiceMethods.ByName(\"CreateUser\")),\n\t\tconnect.WithHandlerOptions(opts...),\n\t)\n\tuserServiceUpdateUserHandler := connect.NewUnaryHandler(\n\t\tUserServiceUpdateUserProcedure,\n\t\tsvc.UpdateUser,\n\t\tconnect.WithSchema(userServiceMethods.ByName(\"UpdateUser\")),\n\t\tconnect.WithHandlerOptions(opts...),\n\t)\n\tuserServiceDeleteUserHandler := connect.NewUnaryHandler(\n\t\tUserServiceDeleteUserProcedure,\n\t\tsvc.DeleteUser,\n\t\tconnect.WithSchema(userServiceMethods.ByName(\"DeleteUser\")),\n\t\tconnect.WithHandlerOptions(opts...),\n\t)\n\tuserServiceListAllUserStatsHandler := connect.NewUnaryHandler(\n\t\tUserServiceListAllUserStatsProcedure,\n\t\tsvc.ListAllUserStats,\n\t\tconnect.WithSchema(userServiceMethods.ByName(\"ListAllUserStats\")),\n\t\tconnect.WithHandlerOptions(opts...),\n\t)\n\tuserServiceGetUserStatsHandler := connect.NewUnaryHandler(\n\t\tUserServiceGetUserStatsProcedure,\n\t\tsvc.GetUserStats,\n\t\tconnect.WithSchema(userServiceMethods.ByName(\"GetUserStats\")),\n\t\tconnect.WithHandlerOptions(opts...),\n\t)\n\tuserServiceGetUserSettingHandler := connect.NewUnaryHandler(\n\t\tUserServiceGetUserSettingProcedure,\n\t\tsvc.GetUserSetting,\n\t\tconnect.WithSchema(userServiceMethods.ByName(\"GetUserSetting\")),\n\t\tconnect.WithHandlerOptions(opts...),\n\t)\n\tuserServiceUpdateUserSettingHandler := connect.NewUnaryHandler(\n\t\tUserServiceUpdateUserSettingProcedure,\n\t\tsvc.UpdateUserSetting,\n\t\tconnect.WithSchema(userServiceMethods.ByName(\"UpdateUserSetting\")),\n\t\tconnect.WithHandlerOptions(opts...),\n\t)\n\tuserServiceListUserSettingsHandler := connect.NewUnaryHandler(\n\t\tUserServiceListUserSettingsProcedure,\n\t\tsvc.ListUserSettings,\n\t\tconnect.WithSchema(userServiceMethods.ByName(\"ListUserSettings\")),\n\t\tconnect.WithHandlerOptions(opts...),\n\t)\n\tuserServiceListPersonalAccessTokensHandler := connect.NewUnaryHandler(\n\t\tUserServiceListPersonalAccessTokensProcedure,\n\t\tsvc.ListPersonalAccessTokens,\n\t\tconnect.WithSchema(userServiceMethods.ByName(\"ListPersonalAccessTokens\")),\n\t\tconnect.WithHandlerOptions(opts...),\n\t)\n\tuserServiceCreatePersonalAccessTokenHandler := connect.NewUnaryHandler(\n\t\tUserServiceCreatePersonalAccessTokenProcedure,\n\t\tsvc.CreatePersonalAccessToken,\n\t\tconnect.WithSchema(userServiceMethods.ByName(\"CreatePersonalAccessToken\")),\n\t\tconnect.WithHandlerOptions(opts...),\n\t)\n\tuserServiceDeletePersonalAccessTokenHandler := connect.NewUnaryHandler(\n\t\tUserServiceDeletePersonalAccessTokenProcedure,\n\t\tsvc.DeletePersonalAccessToken,\n\t\tconnect.WithSchema(userServiceMethods.ByName(\"DeletePersonalAccessToken\")),\n\t\tconnect.WithHandlerOptions(opts...),\n\t)\n\tuserServiceListUserWebhooksHandler := connect.NewUnaryHandler(\n\t\tUserServiceListUserWebhooksProcedure,\n\t\tsvc.ListUserWebhooks,\n\t\tconnect.WithSchema(userServiceMethods.ByName(\"ListUserWebhooks\")),\n\t\tconnect.WithHandlerOptions(opts...),\n\t)\n\tuserServiceCreateUserWebhookHandler := connect.NewUnaryHandler(\n\t\tUserServiceCreateUserWebhookProcedure,\n\t\tsvc.CreateUserWebhook,\n\t\tconnect.WithSchema(userServiceMethods.ByName(\"CreateUserWebhook\")),\n\t\tconnect.WithHandlerOptions(opts...),\n\t)\n\tuserServiceUpdateUserWebhookHandler := connect.NewUnaryHandler(\n\t\tUserServiceUpdateUserWebhookProcedure,\n\t\tsvc.UpdateUserWebhook,\n\t\tconnect.WithSchema(userServiceMethods.ByName(\"UpdateUserWebhook\")),\n\t\tconnect.WithHandlerOptions(opts...),\n\t)\n\tuserServiceDeleteUserWebhookHandler := connect.NewUnaryHandler(\n\t\tUserServiceDeleteUserWebhookProcedure,\n\t\tsvc.DeleteUserWebhook,\n\t\tconnect.WithSchema(userServiceMethods.ByName(\"DeleteUserWebhook\")),\n\t\tconnect.WithHandlerOptions(opts...),\n\t)\n\tuserServiceListUserNotificationsHandler := connect.NewUnaryHandler(\n\t\tUserServiceListUserNotificationsProcedure,\n\t\tsvc.ListUserNotifications,\n\t\tconnect.WithSchema(userServiceMethods.ByName(\"ListUserNotifications\")),\n\t\tconnect.WithHandlerOptions(opts...),\n\t)\n\tuserServiceUpdateUserNotificationHandler := connect.NewUnaryHandler(\n\t\tUserServiceUpdateUserNotificationProcedure,\n\t\tsvc.UpdateUserNotification,\n\t\tconnect.WithSchema(userServiceMethods.ByName(\"UpdateUserNotification\")),\n\t\tconnect.WithHandlerOptions(opts...),\n\t)\n\tuserServiceDeleteUserNotificationHandler := connect.NewUnaryHandler(\n\t\tUserServiceDeleteUserNotificationProcedure,\n\t\tsvc.DeleteUserNotification,\n\t\tconnect.WithSchema(userServiceMethods.ByName(\"DeleteUserNotification\")),\n\t\tconnect.WithHandlerOptions(opts...),\n\t)\n\treturn \"/memos.api.v1.UserService/\", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tswitch r.URL.Path {\n\t\tcase UserServiceListUsersProcedure:\n\t\t\tuserServiceListUsersHandler.ServeHTTP(w, r)\n\t\tcase UserServiceGetUserProcedure:\n\t\t\tuserServiceGetUserHandler.ServeHTTP(w, r)\n\t\tcase UserServiceCreateUserProcedure:\n\t\t\tuserServiceCreateUserHandler.ServeHTTP(w, r)\n\t\tcase UserServiceUpdateUserProcedure:\n\t\t\tuserServiceUpdateUserHandler.ServeHTTP(w, r)\n\t\tcase UserServiceDeleteUserProcedure:\n\t\t\tuserServiceDeleteUserHandler.ServeHTTP(w, r)\n\t\tcase UserServiceListAllUserStatsProcedure:\n\t\t\tuserServiceListAllUserStatsHandler.ServeHTTP(w, r)\n\t\tcase UserServiceGetUserStatsProcedure:\n\t\t\tuserServiceGetUserStatsHandler.ServeHTTP(w, r)\n\t\tcase UserServiceGetUserSettingProcedure:\n\t\t\tuserServiceGetUserSettingHandler.ServeHTTP(w, r)\n\t\tcase UserServiceUpdateUserSettingProcedure:\n\t\t\tuserServiceUpdateUserSettingHandler.ServeHTTP(w, r)\n\t\tcase UserServiceListUserSettingsProcedure:\n\t\t\tuserServiceListUserSettingsHandler.ServeHTTP(w, r)\n\t\tcase UserServiceListPersonalAccessTokensProcedure:\n\t\t\tuserServiceListPersonalAccessTokensHandler.ServeHTTP(w, r)\n\t\tcase UserServiceCreatePersonalAccessTokenProcedure:\n\t\t\tuserServiceCreatePersonalAccessTokenHandler.ServeHTTP(w, r)\n\t\tcase UserServiceDeletePersonalAccessTokenProcedure:\n\t\t\tuserServiceDeletePersonalAccessTokenHandler.ServeHTTP(w, r)\n\t\tcase UserServiceListUserWebhooksProcedure:\n\t\t\tuserServiceListUserWebhooksHandler.ServeHTTP(w, r)\n\t\tcase UserServiceCreateUserWebhookProcedure:\n\t\t\tuserServiceCreateUserWebhookHandler.ServeHTTP(w, r)\n\t\tcase UserServiceUpdateUserWebhookProcedure:\n\t\t\tuserServiceUpdateUserWebhookHandler.ServeHTTP(w, r)\n\t\tcase UserServiceDeleteUserWebhookProcedure:\n\t\t\tuserServiceDeleteUserWebhookHandler.ServeHTTP(w, r)\n\t\tcase UserServiceListUserNotificationsProcedure:\n\t\t\tuserServiceListUserNotificationsHandler.ServeHTTP(w, r)\n\t\tcase UserServiceUpdateUserNotificationProcedure:\n\t\t\tuserServiceUpdateUserNotificationHandler.ServeHTTP(w, r)\n\t\tcase UserServiceDeleteUserNotificationProcedure:\n\t\t\tuserServiceDeleteUserNotificationHandler.ServeHTTP(w, r)\n\t\tdefault:\n\t\t\thttp.NotFound(w, r)\n\t\t}\n\t})\n}\n\n// UnimplementedUserServiceHandler returns CodeUnimplemented from all methods.\ntype UnimplementedUserServiceHandler struct{}\n\nfunc (UnimplementedUserServiceHandler) ListUsers(context.Context, *connect.Request[v1.ListUsersRequest]) (*connect.Response[v1.ListUsersResponse], error) {\n\treturn nil, connect.NewError(connect.CodeUnimplemented, errors.New(\"memos.api.v1.UserService.ListUsers is not implemented\"))\n}\n\nfunc (UnimplementedUserServiceHandler) GetUser(context.Context, *connect.Request[v1.GetUserRequest]) (*connect.Response[v1.User], error) {\n\treturn nil, connect.NewError(connect.CodeUnimplemented, errors.New(\"memos.api.v1.UserService.GetUser is not implemented\"))\n}\n\nfunc (UnimplementedUserServiceHandler) CreateUser(context.Context, *connect.Request[v1.CreateUserRequest]) (*connect.Response[v1.User], error) {\n\treturn nil, connect.NewError(connect.CodeUnimplemented, errors.New(\"memos.api.v1.UserService.CreateUser is not implemented\"))\n}\n\nfunc (UnimplementedUserServiceHandler) UpdateUser(context.Context, *connect.Request[v1.UpdateUserRequest]) (*connect.Response[v1.User], error) {\n\treturn nil, connect.NewError(connect.CodeUnimplemented, errors.New(\"memos.api.v1.UserService.UpdateUser is not implemented\"))\n}\n\nfunc (UnimplementedUserServiceHandler) DeleteUser(context.Context, *connect.Request[v1.DeleteUserRequest]) (*connect.Response[emptypb.Empty], error) {\n\treturn nil, connect.NewError(connect.CodeUnimplemented, errors.New(\"memos.api.v1.UserService.DeleteUser is not implemented\"))\n}\n\nfunc (UnimplementedUserServiceHandler) ListAllUserStats(context.Context, *connect.Request[v1.ListAllUserStatsRequest]) (*connect.Response[v1.ListAllUserStatsResponse], error) {\n\treturn nil, connect.NewError(connect.CodeUnimplemented, errors.New(\"memos.api.v1.UserService.ListAllUserStats is not implemented\"))\n}\n\nfunc (UnimplementedUserServiceHandler) GetUserStats(context.Context, *connect.Request[v1.GetUserStatsRequest]) (*connect.Response[v1.UserStats], error) {\n\treturn nil, connect.NewError(connect.CodeUnimplemented, errors.New(\"memos.api.v1.UserService.GetUserStats is not implemented\"))\n}\n\nfunc (UnimplementedUserServiceHandler) GetUserSetting(context.Context, *connect.Request[v1.GetUserSettingRequest]) (*connect.Response[v1.UserSetting], error) {\n\treturn nil, connect.NewError(connect.CodeUnimplemented, errors.New(\"memos.api.v1.UserService.GetUserSetting is not implemented\"))\n}\n\nfunc (UnimplementedUserServiceHandler) UpdateUserSetting(context.Context, *connect.Request[v1.UpdateUserSettingRequest]) (*connect.Response[v1.UserSetting], error) {\n\treturn nil, connect.NewError(connect.CodeUnimplemented, errors.New(\"memos.api.v1.UserService.UpdateUserSetting is not implemented\"))\n}\n\nfunc (UnimplementedUserServiceHandler) ListUserSettings(context.Context, *connect.Request[v1.ListUserSettingsRequest]) (*connect.Response[v1.ListUserSettingsResponse], error) {\n\treturn nil, connect.NewError(connect.CodeUnimplemented, errors.New(\"memos.api.v1.UserService.ListUserSettings is not implemented\"))\n}\n\nfunc (UnimplementedUserServiceHandler) ListPersonalAccessTokens(context.Context, *connect.Request[v1.ListPersonalAccessTokensRequest]) (*connect.Response[v1.ListPersonalAccessTokensResponse], error) {\n\treturn nil, connect.NewError(connect.CodeUnimplemented, errors.New(\"memos.api.v1.UserService.ListPersonalAccessTokens is not implemented\"))\n}\n\nfunc (UnimplementedUserServiceHandler) CreatePersonalAccessToken(context.Context, *connect.Request[v1.CreatePersonalAccessTokenRequest]) (*connect.Response[v1.CreatePersonalAccessTokenResponse], error) {\n\treturn nil, connect.NewError(connect.CodeUnimplemented, errors.New(\"memos.api.v1.UserService.CreatePersonalAccessToken is not implemented\"))\n}\n\nfunc (UnimplementedUserServiceHandler) DeletePersonalAccessToken(context.Context, *connect.Request[v1.DeletePersonalAccessTokenRequest]) (*connect.Response[emptypb.Empty], error) {\n\treturn nil, connect.NewError(connect.CodeUnimplemented, errors.New(\"memos.api.v1.UserService.DeletePersonalAccessToken is not implemented\"))\n}\n\nfunc (UnimplementedUserServiceHandler) ListUserWebhooks(context.Context, *connect.Request[v1.ListUserWebhooksRequest]) (*connect.Response[v1.ListUserWebhooksResponse], error) {\n\treturn nil, connect.NewError(connect.CodeUnimplemented, errors.New(\"memos.api.v1.UserService.ListUserWebhooks is not implemented\"))\n}\n\nfunc (UnimplementedUserServiceHandler) CreateUserWebhook(context.Context, *connect.Request[v1.CreateUserWebhookRequest]) (*connect.Response[v1.UserWebhook], error) {\n\treturn nil, connect.NewError(connect.CodeUnimplemented, errors.New(\"memos.api.v1.UserService.CreateUserWebhook is not implemented\"))\n}\n\nfunc (UnimplementedUserServiceHandler) UpdateUserWebhook(context.Context, *connect.Request[v1.UpdateUserWebhookRequest]) (*connect.Response[v1.UserWebhook], error) {\n\treturn nil, connect.NewError(connect.CodeUnimplemented, errors.New(\"memos.api.v1.UserService.UpdateUserWebhook is not implemented\"))\n}\n\nfunc (UnimplementedUserServiceHandler) DeleteUserWebhook(context.Context, *connect.Request[v1.DeleteUserWebhookRequest]) (*connect.Response[emptypb.Empty], error) {\n\treturn nil, connect.NewError(connect.CodeUnimplemented, errors.New(\"memos.api.v1.UserService.DeleteUserWebhook is not implemented\"))\n}\n\nfunc (UnimplementedUserServiceHandler) ListUserNotifications(context.Context, *connect.Request[v1.ListUserNotificationsRequest]) (*connect.Response[v1.ListUserNotificationsResponse], error) {\n\treturn nil, connect.NewError(connect.CodeUnimplemented, errors.New(\"memos.api.v1.UserService.ListUserNotifications is not implemented\"))\n}\n\nfunc (UnimplementedUserServiceHandler) UpdateUserNotification(context.Context, *connect.Request[v1.UpdateUserNotificationRequest]) (*connect.Response[v1.UserNotification], error) {\n\treturn nil, connect.NewError(connect.CodeUnimplemented, errors.New(\"memos.api.v1.UserService.UpdateUserNotification is not implemented\"))\n}\n\nfunc (UnimplementedUserServiceHandler) DeleteUserNotification(context.Context, *connect.Request[v1.DeleteUserNotificationRequest]) (*connect.Response[emptypb.Empty], error) {\n\treturn nil, connect.NewError(connect.CodeUnimplemented, errors.New(\"memos.api.v1.UserService.DeleteUserNotification is not implemented\"))\n}\n"
  },
  {
    "path": "proto/gen/api/v1/attachment_service.pb.go",
    "content": "// Code generated by protoc-gen-go. DO NOT EDIT.\n// versions:\n// \tprotoc-gen-go v1.36.11\n// \tprotoc        (unknown)\n// source: api/v1/attachment_service.proto\n\npackage apiv1\n\nimport (\n\t_ \"google.golang.org/genproto/googleapis/api/annotations\"\n\tprotoreflect \"google.golang.org/protobuf/reflect/protoreflect\"\n\tprotoimpl \"google.golang.org/protobuf/runtime/protoimpl\"\n\temptypb \"google.golang.org/protobuf/types/known/emptypb\"\n\tfieldmaskpb \"google.golang.org/protobuf/types/known/fieldmaskpb\"\n\ttimestamppb \"google.golang.org/protobuf/types/known/timestamppb\"\n\treflect \"reflect\"\n\tsync \"sync\"\n\tunsafe \"unsafe\"\n)\n\nconst (\n\t// Verify that this generated code is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)\n\t// Verify that runtime/protoimpl is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)\n)\n\ntype Attachment struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// The name of the attachment.\n\t// Format: attachments/{attachment}\n\tName string `protobuf:\"bytes,1,opt,name=name,proto3\" json:\"name,omitempty\"`\n\t// Output only. The creation timestamp.\n\tCreateTime *timestamppb.Timestamp `protobuf:\"bytes,2,opt,name=create_time,json=createTime,proto3\" json:\"create_time,omitempty\"`\n\t// The filename of the attachment.\n\tFilename string `protobuf:\"bytes,3,opt,name=filename,proto3\" json:\"filename,omitempty\"`\n\t// Input only. The content of the attachment.\n\tContent []byte `protobuf:\"bytes,4,opt,name=content,proto3\" json:\"content,omitempty\"`\n\t// Optional. The external link of the attachment.\n\tExternalLink string `protobuf:\"bytes,5,opt,name=external_link,json=externalLink,proto3\" json:\"external_link,omitempty\"`\n\t// The MIME type of the attachment.\n\tType string `protobuf:\"bytes,6,opt,name=type,proto3\" json:\"type,omitempty\"`\n\t// Output only. The size of the attachment in bytes.\n\tSize int64 `protobuf:\"varint,7,opt,name=size,proto3\" json:\"size,omitempty\"`\n\t// Optional. The related memo. Refer to `Memo.name`.\n\t// Format: memos/{memo}\n\tMemo          *string `protobuf:\"bytes,8,opt,name=memo,proto3,oneof\" json:\"memo,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *Attachment) Reset() {\n\t*x = Attachment{}\n\tmi := &file_api_v1_attachment_service_proto_msgTypes[0]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *Attachment) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*Attachment) ProtoMessage() {}\n\nfunc (x *Attachment) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_attachment_service_proto_msgTypes[0]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use Attachment.ProtoReflect.Descriptor instead.\nfunc (*Attachment) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_attachment_service_proto_rawDescGZIP(), []int{0}\n}\n\nfunc (x *Attachment) GetName() string {\n\tif x != nil {\n\t\treturn x.Name\n\t}\n\treturn \"\"\n}\n\nfunc (x *Attachment) GetCreateTime() *timestamppb.Timestamp {\n\tif x != nil {\n\t\treturn x.CreateTime\n\t}\n\treturn nil\n}\n\nfunc (x *Attachment) GetFilename() string {\n\tif x != nil {\n\t\treturn x.Filename\n\t}\n\treturn \"\"\n}\n\nfunc (x *Attachment) GetContent() []byte {\n\tif x != nil {\n\t\treturn x.Content\n\t}\n\treturn nil\n}\n\nfunc (x *Attachment) GetExternalLink() string {\n\tif x != nil {\n\t\treturn x.ExternalLink\n\t}\n\treturn \"\"\n}\n\nfunc (x *Attachment) GetType() string {\n\tif x != nil {\n\t\treturn x.Type\n\t}\n\treturn \"\"\n}\n\nfunc (x *Attachment) GetSize() int64 {\n\tif x != nil {\n\t\treturn x.Size\n\t}\n\treturn 0\n}\n\nfunc (x *Attachment) GetMemo() string {\n\tif x != nil && x.Memo != nil {\n\t\treturn *x.Memo\n\t}\n\treturn \"\"\n}\n\ntype CreateAttachmentRequest struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// Required. The attachment to create.\n\tAttachment *Attachment `protobuf:\"bytes,1,opt,name=attachment,proto3\" json:\"attachment,omitempty\"`\n\t// Optional. The attachment ID to use for this attachment.\n\t// If empty, a unique ID will be generated.\n\tAttachmentId  string `protobuf:\"bytes,2,opt,name=attachment_id,json=attachmentId,proto3\" json:\"attachment_id,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *CreateAttachmentRequest) Reset() {\n\t*x = CreateAttachmentRequest{}\n\tmi := &file_api_v1_attachment_service_proto_msgTypes[1]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *CreateAttachmentRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*CreateAttachmentRequest) ProtoMessage() {}\n\nfunc (x *CreateAttachmentRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_attachment_service_proto_msgTypes[1]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use CreateAttachmentRequest.ProtoReflect.Descriptor instead.\nfunc (*CreateAttachmentRequest) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_attachment_service_proto_rawDescGZIP(), []int{1}\n}\n\nfunc (x *CreateAttachmentRequest) GetAttachment() *Attachment {\n\tif x != nil {\n\t\treturn x.Attachment\n\t}\n\treturn nil\n}\n\nfunc (x *CreateAttachmentRequest) GetAttachmentId() string {\n\tif x != nil {\n\t\treturn x.AttachmentId\n\t}\n\treturn \"\"\n}\n\ntype ListAttachmentsRequest struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// Optional. The maximum number of attachments to return.\n\t// The service may return fewer than this value.\n\t// If unspecified, at most 50 attachments will be returned.\n\t// The maximum value is 1000; values above 1000 will be coerced to 1000.\n\tPageSize int32 `protobuf:\"varint,1,opt,name=page_size,json=pageSize,proto3\" json:\"page_size,omitempty\"`\n\t// Optional. A page token, received from a previous `ListAttachments` call.\n\t// Provide this to retrieve the subsequent page.\n\tPageToken string `protobuf:\"bytes,2,opt,name=page_token,json=pageToken,proto3\" json:\"page_token,omitempty\"`\n\t// Optional. Filter to apply to the list results.\n\t// Example: \"mime_type==\\\"image/png\\\"\" or \"filename.contains(\\\"test\\\")\"\n\t// Supported operators: =, !=, <, <=, >, >=, : (contains), in\n\t// Supported fields: filename, mime_type, create_time, memo\n\tFilter string `protobuf:\"bytes,3,opt,name=filter,proto3\" json:\"filter,omitempty\"`\n\t// Optional. The order to sort results by.\n\t// Example: \"create_time desc\" or \"filename asc\"\n\tOrderBy       string `protobuf:\"bytes,4,opt,name=order_by,json=orderBy,proto3\" json:\"order_by,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *ListAttachmentsRequest) Reset() {\n\t*x = ListAttachmentsRequest{}\n\tmi := &file_api_v1_attachment_service_proto_msgTypes[2]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *ListAttachmentsRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*ListAttachmentsRequest) ProtoMessage() {}\n\nfunc (x *ListAttachmentsRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_attachment_service_proto_msgTypes[2]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use ListAttachmentsRequest.ProtoReflect.Descriptor instead.\nfunc (*ListAttachmentsRequest) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_attachment_service_proto_rawDescGZIP(), []int{2}\n}\n\nfunc (x *ListAttachmentsRequest) GetPageSize() int32 {\n\tif x != nil {\n\t\treturn x.PageSize\n\t}\n\treturn 0\n}\n\nfunc (x *ListAttachmentsRequest) GetPageToken() string {\n\tif x != nil {\n\t\treturn x.PageToken\n\t}\n\treturn \"\"\n}\n\nfunc (x *ListAttachmentsRequest) GetFilter() string {\n\tif x != nil {\n\t\treturn x.Filter\n\t}\n\treturn \"\"\n}\n\nfunc (x *ListAttachmentsRequest) GetOrderBy() string {\n\tif x != nil {\n\t\treturn x.OrderBy\n\t}\n\treturn \"\"\n}\n\ntype ListAttachmentsResponse struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// The list of attachments.\n\tAttachments []*Attachment `protobuf:\"bytes,1,rep,name=attachments,proto3\" json:\"attachments,omitempty\"`\n\t// A token that can be sent as `page_token` to retrieve the next page.\n\t// If this field is omitted, there are no subsequent pages.\n\tNextPageToken string `protobuf:\"bytes,2,opt,name=next_page_token,json=nextPageToken,proto3\" json:\"next_page_token,omitempty\"`\n\t// The total count of attachments (may be approximate).\n\tTotalSize     int32 `protobuf:\"varint,3,opt,name=total_size,json=totalSize,proto3\" json:\"total_size,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *ListAttachmentsResponse) Reset() {\n\t*x = ListAttachmentsResponse{}\n\tmi := &file_api_v1_attachment_service_proto_msgTypes[3]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *ListAttachmentsResponse) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*ListAttachmentsResponse) ProtoMessage() {}\n\nfunc (x *ListAttachmentsResponse) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_attachment_service_proto_msgTypes[3]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use ListAttachmentsResponse.ProtoReflect.Descriptor instead.\nfunc (*ListAttachmentsResponse) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_attachment_service_proto_rawDescGZIP(), []int{3}\n}\n\nfunc (x *ListAttachmentsResponse) GetAttachments() []*Attachment {\n\tif x != nil {\n\t\treturn x.Attachments\n\t}\n\treturn nil\n}\n\nfunc (x *ListAttachmentsResponse) GetNextPageToken() string {\n\tif x != nil {\n\t\treturn x.NextPageToken\n\t}\n\treturn \"\"\n}\n\nfunc (x *ListAttachmentsResponse) GetTotalSize() int32 {\n\tif x != nil {\n\t\treturn x.TotalSize\n\t}\n\treturn 0\n}\n\ntype GetAttachmentRequest struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// Required. The attachment name of the attachment to retrieve.\n\t// Format: attachments/{attachment}\n\tName          string `protobuf:\"bytes,1,opt,name=name,proto3\" json:\"name,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *GetAttachmentRequest) Reset() {\n\t*x = GetAttachmentRequest{}\n\tmi := &file_api_v1_attachment_service_proto_msgTypes[4]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *GetAttachmentRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*GetAttachmentRequest) ProtoMessage() {}\n\nfunc (x *GetAttachmentRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_attachment_service_proto_msgTypes[4]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use GetAttachmentRequest.ProtoReflect.Descriptor instead.\nfunc (*GetAttachmentRequest) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_attachment_service_proto_rawDescGZIP(), []int{4}\n}\n\nfunc (x *GetAttachmentRequest) GetName() string {\n\tif x != nil {\n\t\treturn x.Name\n\t}\n\treturn \"\"\n}\n\ntype UpdateAttachmentRequest struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// Required. The attachment which replaces the attachment on the server.\n\tAttachment *Attachment `protobuf:\"bytes,1,opt,name=attachment,proto3\" json:\"attachment,omitempty\"`\n\t// Required. The list of fields to update.\n\tUpdateMask    *fieldmaskpb.FieldMask `protobuf:\"bytes,2,opt,name=update_mask,json=updateMask,proto3\" json:\"update_mask,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *UpdateAttachmentRequest) Reset() {\n\t*x = UpdateAttachmentRequest{}\n\tmi := &file_api_v1_attachment_service_proto_msgTypes[5]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *UpdateAttachmentRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*UpdateAttachmentRequest) ProtoMessage() {}\n\nfunc (x *UpdateAttachmentRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_attachment_service_proto_msgTypes[5]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use UpdateAttachmentRequest.ProtoReflect.Descriptor instead.\nfunc (*UpdateAttachmentRequest) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_attachment_service_proto_rawDescGZIP(), []int{5}\n}\n\nfunc (x *UpdateAttachmentRequest) GetAttachment() *Attachment {\n\tif x != nil {\n\t\treturn x.Attachment\n\t}\n\treturn nil\n}\n\nfunc (x *UpdateAttachmentRequest) GetUpdateMask() *fieldmaskpb.FieldMask {\n\tif x != nil {\n\t\treturn x.UpdateMask\n\t}\n\treturn nil\n}\n\ntype DeleteAttachmentRequest struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// Required. The attachment name of the attachment to delete.\n\t// Format: attachments/{attachment}\n\tName          string `protobuf:\"bytes,1,opt,name=name,proto3\" json:\"name,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *DeleteAttachmentRequest) Reset() {\n\t*x = DeleteAttachmentRequest{}\n\tmi := &file_api_v1_attachment_service_proto_msgTypes[6]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *DeleteAttachmentRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*DeleteAttachmentRequest) ProtoMessage() {}\n\nfunc (x *DeleteAttachmentRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_attachment_service_proto_msgTypes[6]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use DeleteAttachmentRequest.ProtoReflect.Descriptor instead.\nfunc (*DeleteAttachmentRequest) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_attachment_service_proto_rawDescGZIP(), []int{6}\n}\n\nfunc (x *DeleteAttachmentRequest) GetName() string {\n\tif x != nil {\n\t\treturn x.Name\n\t}\n\treturn \"\"\n}\n\nvar File_api_v1_attachment_service_proto protoreflect.FileDescriptor\n\nconst file_api_v1_attachment_service_proto_rawDesc = \"\" +\n\t\"\\n\" +\n\t\"\\x1fapi/v1/attachment_service.proto\\x12\\fmemos.api.v1\\x1a\\x1cgoogle/api/annotations.proto\\x1a\\x17google/api/client.proto\\x1a\\x1fgoogle/api/field_behavior.proto\\x1a\\x19google/api/resource.proto\\x1a\\x1bgoogle/protobuf/empty.proto\\x1a google/protobuf/field_mask.proto\\x1a\\x1fgoogle/protobuf/timestamp.proto\\\"\\xfb\\x02\\n\" +\n\t\"\\n\" +\n\t\"Attachment\\x12\\x17\\n\" +\n\t\"\\x04name\\x18\\x01 \\x01(\\tB\\x03\\xe0A\\bR\\x04name\\x12@\\n\" +\n\t\"\\vcreate_time\\x18\\x02 \\x01(\\v2\\x1a.google.protobuf.TimestampB\\x03\\xe0A\\x03R\\n\" +\n\t\"createTime\\x12\\x1f\\n\" +\n\t\"\\bfilename\\x18\\x03 \\x01(\\tB\\x03\\xe0A\\x02R\\bfilename\\x12\\x1d\\n\" +\n\t\"\\acontent\\x18\\x04 \\x01(\\fB\\x03\\xe0A\\x04R\\acontent\\x12(\\n\" +\n\t\"\\rexternal_link\\x18\\x05 \\x01(\\tB\\x03\\xe0A\\x01R\\fexternalLink\\x12\\x17\\n\" +\n\t\"\\x04type\\x18\\x06 \\x01(\\tB\\x03\\xe0A\\x02R\\x04type\\x12\\x17\\n\" +\n\t\"\\x04size\\x18\\a \\x01(\\x03B\\x03\\xe0A\\x03R\\x04size\\x12\\x1c\\n\" +\n\t\"\\x04memo\\x18\\b \\x01(\\tB\\x03\\xe0A\\x01H\\x00R\\x04memo\\x88\\x01\\x01:O\\xeaAL\\n\" +\n\t\"\\x17memos.api.v1/Attachment\\x12\\x18attachments/{attachment}*\\vattachments2\\n\" +\n\t\"attachmentB\\a\\n\" +\n\t\"\\x05_memo\\\"\\x82\\x01\\n\" +\n\t\"\\x17CreateAttachmentRequest\\x12=\\n\" +\n\t\"\\n\" +\n\t\"attachment\\x18\\x01 \\x01(\\v2\\x18.memos.api.v1.AttachmentB\\x03\\xe0A\\x02R\\n\" +\n\t\"attachment\\x12(\\n\" +\n\t\"\\rattachment_id\\x18\\x02 \\x01(\\tB\\x03\\xe0A\\x01R\\fattachmentId\\\"\\x9b\\x01\\n\" +\n\t\"\\x16ListAttachmentsRequest\\x12 \\n\" +\n\t\"\\tpage_size\\x18\\x01 \\x01(\\x05B\\x03\\xe0A\\x01R\\bpageSize\\x12\\\"\\n\" +\n\t\"\\n\" +\n\t\"page_token\\x18\\x02 \\x01(\\tB\\x03\\xe0A\\x01R\\tpageToken\\x12\\x1b\\n\" +\n\t\"\\x06filter\\x18\\x03 \\x01(\\tB\\x03\\xe0A\\x01R\\x06filter\\x12\\x1e\\n\" +\n\t\"\\border_by\\x18\\x04 \\x01(\\tB\\x03\\xe0A\\x01R\\aorderBy\\\"\\x9c\\x01\\n\" +\n\t\"\\x17ListAttachmentsResponse\\x12:\\n\" +\n\t\"\\vattachments\\x18\\x01 \\x03(\\v2\\x18.memos.api.v1.AttachmentR\\vattachments\\x12&\\n\" +\n\t\"\\x0fnext_page_token\\x18\\x02 \\x01(\\tR\\rnextPageToken\\x12\\x1d\\n\" +\n\t\"\\n\" +\n\t\"total_size\\x18\\x03 \\x01(\\x05R\\ttotalSize\\\"K\\n\" +\n\t\"\\x14GetAttachmentRequest\\x123\\n\" +\n\t\"\\x04name\\x18\\x01 \\x01(\\tB\\x1f\\xe0A\\x02\\xfaA\\x19\\n\" +\n\t\"\\x17memos.api.v1/AttachmentR\\x04name\\\"\\x9a\\x01\\n\" +\n\t\"\\x17UpdateAttachmentRequest\\x12=\\n\" +\n\t\"\\n\" +\n\t\"attachment\\x18\\x01 \\x01(\\v2\\x18.memos.api.v1.AttachmentB\\x03\\xe0A\\x02R\\n\" +\n\t\"attachment\\x12@\\n\" +\n\t\"\\vupdate_mask\\x18\\x02 \\x01(\\v2\\x1a.google.protobuf.FieldMaskB\\x03\\xe0A\\x02R\\n\" +\n\t\"updateMask\\\"N\\n\" +\n\t\"\\x17DeleteAttachmentRequest\\x123\\n\" +\n\t\"\\x04name\\x18\\x01 \\x01(\\tB\\x1f\\xe0A\\x02\\xfaA\\x19\\n\" +\n\t\"\\x17memos.api.v1/AttachmentR\\x04name2\\xc4\\x05\\n\" +\n\t\"\\x11AttachmentService\\x12\\x89\\x01\\n\" +\n\t\"\\x10CreateAttachment\\x12%.memos.api.v1.CreateAttachmentRequest\\x1a\\x18.memos.api.v1.Attachment\\\"4\\xdaA\\n\" +\n\t\"attachment\\x82\\xd3\\xe4\\x93\\x02!:\\n\" +\n\t\"attachment\\\"\\x13/api/v1/attachments\\x12{\\n\" +\n\t\"\\x0fListAttachments\\x12$.memos.api.v1.ListAttachmentsRequest\\x1a%.memos.api.v1.ListAttachmentsResponse\\\"\\x1b\\x82\\xd3\\xe4\\x93\\x02\\x15\\x12\\x13/api/v1/attachments\\x12z\\n\" +\n\t\"\\rGetAttachment\\x12\\\".memos.api.v1.GetAttachmentRequest\\x1a\\x18.memos.api.v1.Attachment\\\"+\\xdaA\\x04name\\x82\\xd3\\xe4\\x93\\x02\\x1e\\x12\\x1c/api/v1/{name=attachments/*}\\x12\\xa9\\x01\\n\" +\n\t\"\\x10UpdateAttachment\\x12%.memos.api.v1.UpdateAttachmentRequest\\x1a\\x18.memos.api.v1.Attachment\\\"T\\xdaA\\x16attachment,update_mask\\x82\\xd3\\xe4\\x93\\x025:\\n\" +\n\t\"attachment2'/api/v1/{attachment.name=attachments/*}\\x12~\\n\" +\n\t\"\\x10DeleteAttachment\\x12%.memos.api.v1.DeleteAttachmentRequest\\x1a\\x16.google.protobuf.Empty\\\"+\\xdaA\\x04name\\x82\\xd3\\xe4\\x93\\x02\\x1e*\\x1c/api/v1/{name=attachments/*}B\\xae\\x01\\n\" +\n\t\"\\x10com.memos.api.v1B\\x16AttachmentServiceProtoP\\x01Z0github.com/usememos/memos/proto/gen/api/v1;apiv1\\xa2\\x02\\x03MAX\\xaa\\x02\\fMemos.Api.V1\\xca\\x02\\fMemos\\\\Api\\\\V1\\xe2\\x02\\x18Memos\\\\Api\\\\V1\\\\GPBMetadata\\xea\\x02\\x0eMemos::Api::V1b\\x06proto3\"\n\nvar (\n\tfile_api_v1_attachment_service_proto_rawDescOnce sync.Once\n\tfile_api_v1_attachment_service_proto_rawDescData []byte\n)\n\nfunc file_api_v1_attachment_service_proto_rawDescGZIP() []byte {\n\tfile_api_v1_attachment_service_proto_rawDescOnce.Do(func() {\n\t\tfile_api_v1_attachment_service_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_api_v1_attachment_service_proto_rawDesc), len(file_api_v1_attachment_service_proto_rawDesc)))\n\t})\n\treturn file_api_v1_attachment_service_proto_rawDescData\n}\n\nvar file_api_v1_attachment_service_proto_msgTypes = make([]protoimpl.MessageInfo, 7)\nvar file_api_v1_attachment_service_proto_goTypes = []any{\n\t(*Attachment)(nil),              // 0: memos.api.v1.Attachment\n\t(*CreateAttachmentRequest)(nil), // 1: memos.api.v1.CreateAttachmentRequest\n\t(*ListAttachmentsRequest)(nil),  // 2: memos.api.v1.ListAttachmentsRequest\n\t(*ListAttachmentsResponse)(nil), // 3: memos.api.v1.ListAttachmentsResponse\n\t(*GetAttachmentRequest)(nil),    // 4: memos.api.v1.GetAttachmentRequest\n\t(*UpdateAttachmentRequest)(nil), // 5: memos.api.v1.UpdateAttachmentRequest\n\t(*DeleteAttachmentRequest)(nil), // 6: memos.api.v1.DeleteAttachmentRequest\n\t(*timestamppb.Timestamp)(nil),   // 7: google.protobuf.Timestamp\n\t(*fieldmaskpb.FieldMask)(nil),   // 8: google.protobuf.FieldMask\n\t(*emptypb.Empty)(nil),           // 9: google.protobuf.Empty\n}\nvar file_api_v1_attachment_service_proto_depIdxs = []int32{\n\t7,  // 0: memos.api.v1.Attachment.create_time:type_name -> google.protobuf.Timestamp\n\t0,  // 1: memos.api.v1.CreateAttachmentRequest.attachment:type_name -> memos.api.v1.Attachment\n\t0,  // 2: memos.api.v1.ListAttachmentsResponse.attachments:type_name -> memos.api.v1.Attachment\n\t0,  // 3: memos.api.v1.UpdateAttachmentRequest.attachment:type_name -> memos.api.v1.Attachment\n\t8,  // 4: memos.api.v1.UpdateAttachmentRequest.update_mask:type_name -> google.protobuf.FieldMask\n\t1,  // 5: memos.api.v1.AttachmentService.CreateAttachment:input_type -> memos.api.v1.CreateAttachmentRequest\n\t2,  // 6: memos.api.v1.AttachmentService.ListAttachments:input_type -> memos.api.v1.ListAttachmentsRequest\n\t4,  // 7: memos.api.v1.AttachmentService.GetAttachment:input_type -> memos.api.v1.GetAttachmentRequest\n\t5,  // 8: memos.api.v1.AttachmentService.UpdateAttachment:input_type -> memos.api.v1.UpdateAttachmentRequest\n\t6,  // 9: memos.api.v1.AttachmentService.DeleteAttachment:input_type -> memos.api.v1.DeleteAttachmentRequest\n\t0,  // 10: memos.api.v1.AttachmentService.CreateAttachment:output_type -> memos.api.v1.Attachment\n\t3,  // 11: memos.api.v1.AttachmentService.ListAttachments:output_type -> memos.api.v1.ListAttachmentsResponse\n\t0,  // 12: memos.api.v1.AttachmentService.GetAttachment:output_type -> memos.api.v1.Attachment\n\t0,  // 13: memos.api.v1.AttachmentService.UpdateAttachment:output_type -> memos.api.v1.Attachment\n\t9,  // 14: memos.api.v1.AttachmentService.DeleteAttachment:output_type -> google.protobuf.Empty\n\t10, // [10:15] is the sub-list for method output_type\n\t5,  // [5:10] is the sub-list for method input_type\n\t5,  // [5:5] is the sub-list for extension type_name\n\t5,  // [5:5] is the sub-list for extension extendee\n\t0,  // [0:5] is the sub-list for field type_name\n}\n\nfunc init() { file_api_v1_attachment_service_proto_init() }\nfunc file_api_v1_attachment_service_proto_init() {\n\tif File_api_v1_attachment_service_proto != nil {\n\t\treturn\n\t}\n\tfile_api_v1_attachment_service_proto_msgTypes[0].OneofWrappers = []any{}\n\ttype x struct{}\n\tout := protoimpl.TypeBuilder{\n\t\tFile: protoimpl.DescBuilder{\n\t\t\tGoPackagePath: reflect.TypeOf(x{}).PkgPath(),\n\t\t\tRawDescriptor: unsafe.Slice(unsafe.StringData(file_api_v1_attachment_service_proto_rawDesc), len(file_api_v1_attachment_service_proto_rawDesc)),\n\t\t\tNumEnums:      0,\n\t\t\tNumMessages:   7,\n\t\t\tNumExtensions: 0,\n\t\t\tNumServices:   1,\n\t\t},\n\t\tGoTypes:           file_api_v1_attachment_service_proto_goTypes,\n\t\tDependencyIndexes: file_api_v1_attachment_service_proto_depIdxs,\n\t\tMessageInfos:      file_api_v1_attachment_service_proto_msgTypes,\n\t}.Build()\n\tFile_api_v1_attachment_service_proto = out.File\n\tfile_api_v1_attachment_service_proto_goTypes = nil\n\tfile_api_v1_attachment_service_proto_depIdxs = nil\n}\n"
  },
  {
    "path": "proto/gen/api/v1/attachment_service.pb.gw.go",
    "content": "// Code generated by protoc-gen-grpc-gateway. DO NOT EDIT.\n// source: api/v1/attachment_service.proto\n\n/*\nPackage apiv1 is a reverse proxy.\n\nIt translates gRPC into RESTful JSON APIs.\n*/\npackage apiv1\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"io\"\n\t\"net/http\"\n\n\t\"github.com/grpc-ecosystem/grpc-gateway/v2/runtime\"\n\t\"github.com/grpc-ecosystem/grpc-gateway/v2/utilities\"\n\t\"google.golang.org/grpc\"\n\t\"google.golang.org/grpc/codes\"\n\t\"google.golang.org/grpc/grpclog\"\n\t\"google.golang.org/grpc/metadata\"\n\t\"google.golang.org/grpc/status\"\n\t\"google.golang.org/protobuf/proto\"\n)\n\n// Suppress \"imported and not used\" errors\nvar (\n\t_ codes.Code\n\t_ io.Reader\n\t_ status.Status\n\t_ = errors.New\n\t_ = runtime.String\n\t_ = utilities.NewDoubleArray\n\t_ = metadata.Join\n)\n\nvar filter_AttachmentService_CreateAttachment_0 = &utilities.DoubleArray{Encoding: map[string]int{\"attachment\": 0}, Base: []int{1, 1, 0}, Check: []int{0, 1, 2}}\n\nfunc request_AttachmentService_CreateAttachment_0(ctx context.Context, marshaler runtime.Marshaler, client AttachmentServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq CreateAttachmentRequest\n\t\tmetadata runtime.ServerMetadata\n\t)\n\tif err := marshaler.NewDecoder(req.Body).Decode(&protoReq.Attachment); err != nil && !errors.Is(err, io.EOF) {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tif req.Body != nil {\n\t\t_, _ = io.Copy(io.Discard, req.Body)\n\t}\n\tif err := req.ParseForm(); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tif err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_AttachmentService_CreateAttachment_0); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tmsg, err := client.CreateAttachment(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))\n\treturn msg, metadata, err\n}\n\nfunc local_request_AttachmentService_CreateAttachment_0(ctx context.Context, marshaler runtime.Marshaler, server AttachmentServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq CreateAttachmentRequest\n\t\tmetadata runtime.ServerMetadata\n\t)\n\tif err := marshaler.NewDecoder(req.Body).Decode(&protoReq.Attachment); err != nil && !errors.Is(err, io.EOF) {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tif err := req.ParseForm(); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tif err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_AttachmentService_CreateAttachment_0); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tmsg, err := server.CreateAttachment(ctx, &protoReq)\n\treturn msg, metadata, err\n}\n\nvar filter_AttachmentService_ListAttachments_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)}\n\nfunc request_AttachmentService_ListAttachments_0(ctx context.Context, marshaler runtime.Marshaler, client AttachmentServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq ListAttachmentsRequest\n\t\tmetadata runtime.ServerMetadata\n\t)\n\tif req.Body != nil {\n\t\t_, _ = io.Copy(io.Discard, req.Body)\n\t}\n\tif err := req.ParseForm(); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tif err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_AttachmentService_ListAttachments_0); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tmsg, err := client.ListAttachments(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))\n\treturn msg, metadata, err\n}\n\nfunc local_request_AttachmentService_ListAttachments_0(ctx context.Context, marshaler runtime.Marshaler, server AttachmentServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq ListAttachmentsRequest\n\t\tmetadata runtime.ServerMetadata\n\t)\n\tif err := req.ParseForm(); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tif err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_AttachmentService_ListAttachments_0); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tmsg, err := server.ListAttachments(ctx, &protoReq)\n\treturn msg, metadata, err\n}\n\nfunc request_AttachmentService_GetAttachment_0(ctx context.Context, marshaler runtime.Marshaler, client AttachmentServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq GetAttachmentRequest\n\t\tmetadata runtime.ServerMetadata\n\t\terr      error\n\t)\n\tif req.Body != nil {\n\t\t_, _ = io.Copy(io.Discard, req.Body)\n\t}\n\tval, ok := pathParams[\"name\"]\n\tif !ok {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"missing parameter %s\", \"name\")\n\t}\n\tprotoReq.Name, err = runtime.String(val)\n\tif err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"type mismatch, parameter: %s, error: %v\", \"name\", err)\n\t}\n\tmsg, err := client.GetAttachment(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))\n\treturn msg, metadata, err\n}\n\nfunc local_request_AttachmentService_GetAttachment_0(ctx context.Context, marshaler runtime.Marshaler, server AttachmentServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq GetAttachmentRequest\n\t\tmetadata runtime.ServerMetadata\n\t\terr      error\n\t)\n\tval, ok := pathParams[\"name\"]\n\tif !ok {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"missing parameter %s\", \"name\")\n\t}\n\tprotoReq.Name, err = runtime.String(val)\n\tif err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"type mismatch, parameter: %s, error: %v\", \"name\", err)\n\t}\n\tmsg, err := server.GetAttachment(ctx, &protoReq)\n\treturn msg, metadata, err\n}\n\nvar filter_AttachmentService_UpdateAttachment_0 = &utilities.DoubleArray{Encoding: map[string]int{\"attachment\": 0, \"name\": 1}, Base: []int{1, 2, 1, 0, 0}, Check: []int{0, 1, 2, 3, 2}}\n\nfunc request_AttachmentService_UpdateAttachment_0(ctx context.Context, marshaler runtime.Marshaler, client AttachmentServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq UpdateAttachmentRequest\n\t\tmetadata runtime.ServerMetadata\n\t\terr      error\n\t)\n\tnewReader, berr := utilities.IOReaderFactory(req.Body)\n\tif berr != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", berr)\n\t}\n\tif err := marshaler.NewDecoder(newReader()).Decode(&protoReq.Attachment); err != nil && !errors.Is(err, io.EOF) {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tif req.Body != nil {\n\t\t_, _ = io.Copy(io.Discard, req.Body)\n\t}\n\tif protoReq.UpdateMask == nil || len(protoReq.UpdateMask.GetPaths()) == 0 {\n\t\tif fieldMask, err := runtime.FieldMaskFromRequestBody(newReader(), protoReq.Attachment); err != nil {\n\t\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t\t} else {\n\t\t\tprotoReq.UpdateMask = fieldMask\n\t\t}\n\t}\n\tval, ok := pathParams[\"attachment.name\"]\n\tif !ok {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"missing parameter %s\", \"attachment.name\")\n\t}\n\terr = runtime.PopulateFieldFromPath(&protoReq, \"attachment.name\", val)\n\tif err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"type mismatch, parameter: %s, error: %v\", \"attachment.name\", err)\n\t}\n\tif err := req.ParseForm(); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tif err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_AttachmentService_UpdateAttachment_0); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tmsg, err := client.UpdateAttachment(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))\n\treturn msg, metadata, err\n}\n\nfunc local_request_AttachmentService_UpdateAttachment_0(ctx context.Context, marshaler runtime.Marshaler, server AttachmentServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq UpdateAttachmentRequest\n\t\tmetadata runtime.ServerMetadata\n\t\terr      error\n\t)\n\tnewReader, berr := utilities.IOReaderFactory(req.Body)\n\tif berr != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", berr)\n\t}\n\tif err := marshaler.NewDecoder(newReader()).Decode(&protoReq.Attachment); err != nil && !errors.Is(err, io.EOF) {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tif protoReq.UpdateMask == nil || len(protoReq.UpdateMask.GetPaths()) == 0 {\n\t\tif fieldMask, err := runtime.FieldMaskFromRequestBody(newReader(), protoReq.Attachment); err != nil {\n\t\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t\t} else {\n\t\t\tprotoReq.UpdateMask = fieldMask\n\t\t}\n\t}\n\tval, ok := pathParams[\"attachment.name\"]\n\tif !ok {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"missing parameter %s\", \"attachment.name\")\n\t}\n\terr = runtime.PopulateFieldFromPath(&protoReq, \"attachment.name\", val)\n\tif err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"type mismatch, parameter: %s, error: %v\", \"attachment.name\", err)\n\t}\n\tif err := req.ParseForm(); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tif err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_AttachmentService_UpdateAttachment_0); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tmsg, err := server.UpdateAttachment(ctx, &protoReq)\n\treturn msg, metadata, err\n}\n\nfunc request_AttachmentService_DeleteAttachment_0(ctx context.Context, marshaler runtime.Marshaler, client AttachmentServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq DeleteAttachmentRequest\n\t\tmetadata runtime.ServerMetadata\n\t\terr      error\n\t)\n\tif req.Body != nil {\n\t\t_, _ = io.Copy(io.Discard, req.Body)\n\t}\n\tval, ok := pathParams[\"name\"]\n\tif !ok {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"missing parameter %s\", \"name\")\n\t}\n\tprotoReq.Name, err = runtime.String(val)\n\tif err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"type mismatch, parameter: %s, error: %v\", \"name\", err)\n\t}\n\tmsg, err := client.DeleteAttachment(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))\n\treturn msg, metadata, err\n}\n\nfunc local_request_AttachmentService_DeleteAttachment_0(ctx context.Context, marshaler runtime.Marshaler, server AttachmentServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq DeleteAttachmentRequest\n\t\tmetadata runtime.ServerMetadata\n\t\terr      error\n\t)\n\tval, ok := pathParams[\"name\"]\n\tif !ok {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"missing parameter %s\", \"name\")\n\t}\n\tprotoReq.Name, err = runtime.String(val)\n\tif err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"type mismatch, parameter: %s, error: %v\", \"name\", err)\n\t}\n\tmsg, err := server.DeleteAttachment(ctx, &protoReq)\n\treturn msg, metadata, err\n}\n\n// RegisterAttachmentServiceHandlerServer registers the http handlers for service AttachmentService to \"mux\".\n// UnaryRPC     :call AttachmentServiceServer directly.\n// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906.\n// Note that using this registration option will cause many gRPC library features to stop working. Consider using RegisterAttachmentServiceHandlerFromEndpoint instead.\n// GRPC interceptors will not work for this type of registration. To use interceptors, you must use the \"runtime.WithMiddlewares\" option in the \"runtime.NewServeMux\" call.\nfunc RegisterAttachmentServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux, server AttachmentServiceServer) error {\n\tmux.Handle(http.MethodPost, pattern_AttachmentService_CreateAttachment_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tvar stream runtime.ServerTransportStream\n\t\tctx = grpc.NewContextWithServerTransportStream(ctx, &stream)\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, \"/memos.api.v1.AttachmentService/CreateAttachment\", runtime.WithHTTPPathPattern(\"/api/v1/attachments\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := local_request_AttachmentService_CreateAttachment_0(annotatedContext, inboundMarshaler, server, req, pathParams)\n\t\tmd.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_AttachmentService_CreateAttachment_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodGet, pattern_AttachmentService_ListAttachments_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tvar stream runtime.ServerTransportStream\n\t\tctx = grpc.NewContextWithServerTransportStream(ctx, &stream)\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, \"/memos.api.v1.AttachmentService/ListAttachments\", runtime.WithHTTPPathPattern(\"/api/v1/attachments\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := local_request_AttachmentService_ListAttachments_0(annotatedContext, inboundMarshaler, server, req, pathParams)\n\t\tmd.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_AttachmentService_ListAttachments_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodGet, pattern_AttachmentService_GetAttachment_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tvar stream runtime.ServerTransportStream\n\t\tctx = grpc.NewContextWithServerTransportStream(ctx, &stream)\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, \"/memos.api.v1.AttachmentService/GetAttachment\", runtime.WithHTTPPathPattern(\"/api/v1/{name=attachments/*}\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := local_request_AttachmentService_GetAttachment_0(annotatedContext, inboundMarshaler, server, req, pathParams)\n\t\tmd.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_AttachmentService_GetAttachment_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodPatch, pattern_AttachmentService_UpdateAttachment_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tvar stream runtime.ServerTransportStream\n\t\tctx = grpc.NewContextWithServerTransportStream(ctx, &stream)\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, \"/memos.api.v1.AttachmentService/UpdateAttachment\", runtime.WithHTTPPathPattern(\"/api/v1/{attachment.name=attachments/*}\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := local_request_AttachmentService_UpdateAttachment_0(annotatedContext, inboundMarshaler, server, req, pathParams)\n\t\tmd.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_AttachmentService_UpdateAttachment_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodDelete, pattern_AttachmentService_DeleteAttachment_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tvar stream runtime.ServerTransportStream\n\t\tctx = grpc.NewContextWithServerTransportStream(ctx, &stream)\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, \"/memos.api.v1.AttachmentService/DeleteAttachment\", runtime.WithHTTPPathPattern(\"/api/v1/{name=attachments/*}\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := local_request_AttachmentService_DeleteAttachment_0(annotatedContext, inboundMarshaler, server, req, pathParams)\n\t\tmd.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_AttachmentService_DeleteAttachment_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\n\treturn nil\n}\n\n// RegisterAttachmentServiceHandlerFromEndpoint is same as RegisterAttachmentServiceHandler but\n// automatically dials to \"endpoint\" and closes the connection when \"ctx\" gets done.\nfunc RegisterAttachmentServiceHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) {\n\tconn, err := grpc.NewClient(endpoint, opts...)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer func() {\n\t\tif err != nil {\n\t\t\tif cerr := conn.Close(); cerr != nil {\n\t\t\t\tgrpclog.Errorf(\"Failed to close conn to %s: %v\", endpoint, cerr)\n\t\t\t}\n\t\t\treturn\n\t\t}\n\t\tgo func() {\n\t\t\t<-ctx.Done()\n\t\t\tif cerr := conn.Close(); cerr != nil {\n\t\t\t\tgrpclog.Errorf(\"Failed to close conn to %s: %v\", endpoint, cerr)\n\t\t\t}\n\t\t}()\n\t}()\n\treturn RegisterAttachmentServiceHandler(ctx, mux, conn)\n}\n\n// RegisterAttachmentServiceHandler registers the http handlers for service AttachmentService to \"mux\".\n// The handlers forward requests to the grpc endpoint over \"conn\".\nfunc RegisterAttachmentServiceHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error {\n\treturn RegisterAttachmentServiceHandlerClient(ctx, mux, NewAttachmentServiceClient(conn))\n}\n\n// RegisterAttachmentServiceHandlerClient registers the http handlers for service AttachmentService\n// to \"mux\". The handlers forward requests to the grpc endpoint over the given implementation of \"AttachmentServiceClient\".\n// Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in \"AttachmentServiceClient\"\n// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in\n// \"AttachmentServiceClient\" to call the correct interceptors. This client ignores the HTTP middlewares.\nfunc RegisterAttachmentServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux, client AttachmentServiceClient) error {\n\tmux.Handle(http.MethodPost, pattern_AttachmentService_CreateAttachment_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateContext(ctx, mux, req, \"/memos.api.v1.AttachmentService/CreateAttachment\", runtime.WithHTTPPathPattern(\"/api/v1/attachments\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := request_AttachmentService_CreateAttachment_0(annotatedContext, inboundMarshaler, client, req, pathParams)\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_AttachmentService_CreateAttachment_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodGet, pattern_AttachmentService_ListAttachments_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateContext(ctx, mux, req, \"/memos.api.v1.AttachmentService/ListAttachments\", runtime.WithHTTPPathPattern(\"/api/v1/attachments\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := request_AttachmentService_ListAttachments_0(annotatedContext, inboundMarshaler, client, req, pathParams)\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_AttachmentService_ListAttachments_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodGet, pattern_AttachmentService_GetAttachment_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateContext(ctx, mux, req, \"/memos.api.v1.AttachmentService/GetAttachment\", runtime.WithHTTPPathPattern(\"/api/v1/{name=attachments/*}\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := request_AttachmentService_GetAttachment_0(annotatedContext, inboundMarshaler, client, req, pathParams)\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_AttachmentService_GetAttachment_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodPatch, pattern_AttachmentService_UpdateAttachment_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateContext(ctx, mux, req, \"/memos.api.v1.AttachmentService/UpdateAttachment\", runtime.WithHTTPPathPattern(\"/api/v1/{attachment.name=attachments/*}\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := request_AttachmentService_UpdateAttachment_0(annotatedContext, inboundMarshaler, client, req, pathParams)\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_AttachmentService_UpdateAttachment_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodDelete, pattern_AttachmentService_DeleteAttachment_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateContext(ctx, mux, req, \"/memos.api.v1.AttachmentService/DeleteAttachment\", runtime.WithHTTPPathPattern(\"/api/v1/{name=attachments/*}\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := request_AttachmentService_DeleteAttachment_0(annotatedContext, inboundMarshaler, client, req, pathParams)\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_AttachmentService_DeleteAttachment_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\treturn nil\n}\n\nvar (\n\tpattern_AttachmentService_CreateAttachment_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{\"api\", \"v1\", \"attachments\"}, \"\"))\n\tpattern_AttachmentService_ListAttachments_0  = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{\"api\", \"v1\", \"attachments\"}, \"\"))\n\tpattern_AttachmentService_GetAttachment_0    = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3}, []string{\"api\", \"v1\", \"attachments\", \"name\"}, \"\"))\n\tpattern_AttachmentService_UpdateAttachment_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3}, []string{\"api\", \"v1\", \"attachments\", \"attachment.name\"}, \"\"))\n\tpattern_AttachmentService_DeleteAttachment_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3}, []string{\"api\", \"v1\", \"attachments\", \"name\"}, \"\"))\n)\n\nvar (\n\tforward_AttachmentService_CreateAttachment_0 = runtime.ForwardResponseMessage\n\tforward_AttachmentService_ListAttachments_0  = runtime.ForwardResponseMessage\n\tforward_AttachmentService_GetAttachment_0    = runtime.ForwardResponseMessage\n\tforward_AttachmentService_UpdateAttachment_0 = runtime.ForwardResponseMessage\n\tforward_AttachmentService_DeleteAttachment_0 = runtime.ForwardResponseMessage\n)\n"
  },
  {
    "path": "proto/gen/api/v1/attachment_service_grpc.pb.go",
    "content": "// Code generated by protoc-gen-go-grpc. DO NOT EDIT.\n// versions:\n// - protoc-gen-go-grpc v1.6.1\n// - protoc             (unknown)\n// source: api/v1/attachment_service.proto\n\npackage apiv1\n\nimport (\n\tcontext \"context\"\n\tgrpc \"google.golang.org/grpc\"\n\tcodes \"google.golang.org/grpc/codes\"\n\tstatus \"google.golang.org/grpc/status\"\n\temptypb \"google.golang.org/protobuf/types/known/emptypb\"\n)\n\n// This is a compile-time assertion to ensure that this generated file\n// is compatible with the grpc package it is being compiled against.\n// Requires gRPC-Go v1.64.0 or later.\nconst _ = grpc.SupportPackageIsVersion9\n\nconst (\n\tAttachmentService_CreateAttachment_FullMethodName = \"/memos.api.v1.AttachmentService/CreateAttachment\"\n\tAttachmentService_ListAttachments_FullMethodName  = \"/memos.api.v1.AttachmentService/ListAttachments\"\n\tAttachmentService_GetAttachment_FullMethodName    = \"/memos.api.v1.AttachmentService/GetAttachment\"\n\tAttachmentService_UpdateAttachment_FullMethodName = \"/memos.api.v1.AttachmentService/UpdateAttachment\"\n\tAttachmentService_DeleteAttachment_FullMethodName = \"/memos.api.v1.AttachmentService/DeleteAttachment\"\n)\n\n// AttachmentServiceClient is the client API for AttachmentService service.\n//\n// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.\ntype AttachmentServiceClient interface {\n\t// CreateAttachment creates a new attachment.\n\tCreateAttachment(ctx context.Context, in *CreateAttachmentRequest, opts ...grpc.CallOption) (*Attachment, error)\n\t// ListAttachments lists all attachments.\n\tListAttachments(ctx context.Context, in *ListAttachmentsRequest, opts ...grpc.CallOption) (*ListAttachmentsResponse, error)\n\t// GetAttachment returns an attachment by name.\n\tGetAttachment(ctx context.Context, in *GetAttachmentRequest, opts ...grpc.CallOption) (*Attachment, error)\n\t// UpdateAttachment updates an attachment.\n\tUpdateAttachment(ctx context.Context, in *UpdateAttachmentRequest, opts ...grpc.CallOption) (*Attachment, error)\n\t// DeleteAttachment deletes an attachment by name.\n\tDeleteAttachment(ctx context.Context, in *DeleteAttachmentRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)\n}\n\ntype attachmentServiceClient struct {\n\tcc grpc.ClientConnInterface\n}\n\nfunc NewAttachmentServiceClient(cc grpc.ClientConnInterface) AttachmentServiceClient {\n\treturn &attachmentServiceClient{cc}\n}\n\nfunc (c *attachmentServiceClient) CreateAttachment(ctx context.Context, in *CreateAttachmentRequest, opts ...grpc.CallOption) (*Attachment, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(Attachment)\n\terr := c.cc.Invoke(ctx, AttachmentService_CreateAttachment_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *attachmentServiceClient) ListAttachments(ctx context.Context, in *ListAttachmentsRequest, opts ...grpc.CallOption) (*ListAttachmentsResponse, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(ListAttachmentsResponse)\n\terr := c.cc.Invoke(ctx, AttachmentService_ListAttachments_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *attachmentServiceClient) GetAttachment(ctx context.Context, in *GetAttachmentRequest, opts ...grpc.CallOption) (*Attachment, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(Attachment)\n\terr := c.cc.Invoke(ctx, AttachmentService_GetAttachment_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *attachmentServiceClient) UpdateAttachment(ctx context.Context, in *UpdateAttachmentRequest, opts ...grpc.CallOption) (*Attachment, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(Attachment)\n\terr := c.cc.Invoke(ctx, AttachmentService_UpdateAttachment_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *attachmentServiceClient) DeleteAttachment(ctx context.Context, in *DeleteAttachmentRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(emptypb.Empty)\n\terr := c.cc.Invoke(ctx, AttachmentService_DeleteAttachment_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\n// AttachmentServiceServer is the server API for AttachmentService service.\n// All implementations must embed UnimplementedAttachmentServiceServer\n// for forward compatibility.\ntype AttachmentServiceServer interface {\n\t// CreateAttachment creates a new attachment.\n\tCreateAttachment(context.Context, *CreateAttachmentRequest) (*Attachment, error)\n\t// ListAttachments lists all attachments.\n\tListAttachments(context.Context, *ListAttachmentsRequest) (*ListAttachmentsResponse, error)\n\t// GetAttachment returns an attachment by name.\n\tGetAttachment(context.Context, *GetAttachmentRequest) (*Attachment, error)\n\t// UpdateAttachment updates an attachment.\n\tUpdateAttachment(context.Context, *UpdateAttachmentRequest) (*Attachment, error)\n\t// DeleteAttachment deletes an attachment by name.\n\tDeleteAttachment(context.Context, *DeleteAttachmentRequest) (*emptypb.Empty, error)\n\tmustEmbedUnimplementedAttachmentServiceServer()\n}\n\n// UnimplementedAttachmentServiceServer must be embedded to have\n// forward compatible implementations.\n//\n// NOTE: this should be embedded by value instead of pointer to avoid a nil\n// pointer dereference when methods are called.\ntype UnimplementedAttachmentServiceServer struct{}\n\nfunc (UnimplementedAttachmentServiceServer) CreateAttachment(context.Context, *CreateAttachmentRequest) (*Attachment, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method CreateAttachment not implemented\")\n}\nfunc (UnimplementedAttachmentServiceServer) ListAttachments(context.Context, *ListAttachmentsRequest) (*ListAttachmentsResponse, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method ListAttachments not implemented\")\n}\nfunc (UnimplementedAttachmentServiceServer) GetAttachment(context.Context, *GetAttachmentRequest) (*Attachment, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method GetAttachment not implemented\")\n}\nfunc (UnimplementedAttachmentServiceServer) UpdateAttachment(context.Context, *UpdateAttachmentRequest) (*Attachment, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method UpdateAttachment not implemented\")\n}\nfunc (UnimplementedAttachmentServiceServer) DeleteAttachment(context.Context, *DeleteAttachmentRequest) (*emptypb.Empty, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method DeleteAttachment not implemented\")\n}\nfunc (UnimplementedAttachmentServiceServer) mustEmbedUnimplementedAttachmentServiceServer() {}\nfunc (UnimplementedAttachmentServiceServer) testEmbeddedByValue()                           {}\n\n// UnsafeAttachmentServiceServer may be embedded to opt out of forward compatibility for this service.\n// Use of this interface is not recommended, as added methods to AttachmentServiceServer will\n// result in compilation errors.\ntype UnsafeAttachmentServiceServer interface {\n\tmustEmbedUnimplementedAttachmentServiceServer()\n}\n\nfunc RegisterAttachmentServiceServer(s grpc.ServiceRegistrar, srv AttachmentServiceServer) {\n\t// If the following call panics, it indicates UnimplementedAttachmentServiceServer was\n\t// embedded by pointer and is nil.  This will cause panics if an\n\t// unimplemented method is ever invoked, so we test this at initialization\n\t// time to prevent it from happening at runtime later due to I/O.\n\tif t, ok := srv.(interface{ testEmbeddedByValue() }); ok {\n\t\tt.testEmbeddedByValue()\n\t}\n\ts.RegisterService(&AttachmentService_ServiceDesc, srv)\n}\n\nfunc _AttachmentService_CreateAttachment_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(CreateAttachmentRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(AttachmentServiceServer).CreateAttachment(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: AttachmentService_CreateAttachment_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(AttachmentServiceServer).CreateAttachment(ctx, req.(*CreateAttachmentRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _AttachmentService_ListAttachments_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(ListAttachmentsRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(AttachmentServiceServer).ListAttachments(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: AttachmentService_ListAttachments_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(AttachmentServiceServer).ListAttachments(ctx, req.(*ListAttachmentsRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _AttachmentService_GetAttachment_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(GetAttachmentRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(AttachmentServiceServer).GetAttachment(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: AttachmentService_GetAttachment_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(AttachmentServiceServer).GetAttachment(ctx, req.(*GetAttachmentRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _AttachmentService_UpdateAttachment_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(UpdateAttachmentRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(AttachmentServiceServer).UpdateAttachment(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: AttachmentService_UpdateAttachment_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(AttachmentServiceServer).UpdateAttachment(ctx, req.(*UpdateAttachmentRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _AttachmentService_DeleteAttachment_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(DeleteAttachmentRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(AttachmentServiceServer).DeleteAttachment(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: AttachmentService_DeleteAttachment_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(AttachmentServiceServer).DeleteAttachment(ctx, req.(*DeleteAttachmentRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\n// AttachmentService_ServiceDesc is the grpc.ServiceDesc for AttachmentService service.\n// It's only intended for direct use with grpc.RegisterService,\n// and not to be introspected or modified (even as a copy)\nvar AttachmentService_ServiceDesc = grpc.ServiceDesc{\n\tServiceName: \"memos.api.v1.AttachmentService\",\n\tHandlerType: (*AttachmentServiceServer)(nil),\n\tMethods: []grpc.MethodDesc{\n\t\t{\n\t\t\tMethodName: \"CreateAttachment\",\n\t\t\tHandler:    _AttachmentService_CreateAttachment_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"ListAttachments\",\n\t\t\tHandler:    _AttachmentService_ListAttachments_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"GetAttachment\",\n\t\t\tHandler:    _AttachmentService_GetAttachment_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"UpdateAttachment\",\n\t\t\tHandler:    _AttachmentService_UpdateAttachment_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"DeleteAttachment\",\n\t\t\tHandler:    _AttachmentService_DeleteAttachment_Handler,\n\t\t},\n\t},\n\tStreams:  []grpc.StreamDesc{},\n\tMetadata: \"api/v1/attachment_service.proto\",\n}\n"
  },
  {
    "path": "proto/gen/api/v1/auth_service.pb.go",
    "content": "// Code generated by protoc-gen-go. DO NOT EDIT.\n// versions:\n// \tprotoc-gen-go v1.36.11\n// \tprotoc        (unknown)\n// source: api/v1/auth_service.proto\n\npackage apiv1\n\nimport (\n\t_ \"google.golang.org/genproto/googleapis/api/annotations\"\n\tprotoreflect \"google.golang.org/protobuf/reflect/protoreflect\"\n\tprotoimpl \"google.golang.org/protobuf/runtime/protoimpl\"\n\temptypb \"google.golang.org/protobuf/types/known/emptypb\"\n\ttimestamppb \"google.golang.org/protobuf/types/known/timestamppb\"\n\treflect \"reflect\"\n\tsync \"sync\"\n\tunsafe \"unsafe\"\n)\n\nconst (\n\t// Verify that this generated code is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)\n\t// Verify that runtime/protoimpl is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)\n)\n\ntype GetCurrentUserRequest struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *GetCurrentUserRequest) Reset() {\n\t*x = GetCurrentUserRequest{}\n\tmi := &file_api_v1_auth_service_proto_msgTypes[0]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *GetCurrentUserRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*GetCurrentUserRequest) ProtoMessage() {}\n\nfunc (x *GetCurrentUserRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_auth_service_proto_msgTypes[0]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use GetCurrentUserRequest.ProtoReflect.Descriptor instead.\nfunc (*GetCurrentUserRequest) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_auth_service_proto_rawDescGZIP(), []int{0}\n}\n\ntype GetCurrentUserResponse struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// The authenticated user's information.\n\tUser          *User `protobuf:\"bytes,1,opt,name=user,proto3\" json:\"user,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *GetCurrentUserResponse) Reset() {\n\t*x = GetCurrentUserResponse{}\n\tmi := &file_api_v1_auth_service_proto_msgTypes[1]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *GetCurrentUserResponse) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*GetCurrentUserResponse) ProtoMessage() {}\n\nfunc (x *GetCurrentUserResponse) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_auth_service_proto_msgTypes[1]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use GetCurrentUserResponse.ProtoReflect.Descriptor instead.\nfunc (*GetCurrentUserResponse) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_auth_service_proto_rawDescGZIP(), []int{1}\n}\n\nfunc (x *GetCurrentUserResponse) GetUser() *User {\n\tif x != nil {\n\t\treturn x.User\n\t}\n\treturn nil\n}\n\ntype SignInRequest struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// Authentication credentials. Provide one method.\n\t//\n\t// Types that are valid to be assigned to Credentials:\n\t//\n\t//\t*SignInRequest_PasswordCredentials_\n\t//\t*SignInRequest_SsoCredentials\n\tCredentials   isSignInRequest_Credentials `protobuf_oneof:\"credentials\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *SignInRequest) Reset() {\n\t*x = SignInRequest{}\n\tmi := &file_api_v1_auth_service_proto_msgTypes[2]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *SignInRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*SignInRequest) ProtoMessage() {}\n\nfunc (x *SignInRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_auth_service_proto_msgTypes[2]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use SignInRequest.ProtoReflect.Descriptor instead.\nfunc (*SignInRequest) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_auth_service_proto_rawDescGZIP(), []int{2}\n}\n\nfunc (x *SignInRequest) GetCredentials() isSignInRequest_Credentials {\n\tif x != nil {\n\t\treturn x.Credentials\n\t}\n\treturn nil\n}\n\nfunc (x *SignInRequest) GetPasswordCredentials() *SignInRequest_PasswordCredentials {\n\tif x != nil {\n\t\tif x, ok := x.Credentials.(*SignInRequest_PasswordCredentials_); ok {\n\t\t\treturn x.PasswordCredentials\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *SignInRequest) GetSsoCredentials() *SignInRequest_SSOCredentials {\n\tif x != nil {\n\t\tif x, ok := x.Credentials.(*SignInRequest_SsoCredentials); ok {\n\t\t\treturn x.SsoCredentials\n\t\t}\n\t}\n\treturn nil\n}\n\ntype isSignInRequest_Credentials interface {\n\tisSignInRequest_Credentials()\n}\n\ntype SignInRequest_PasswordCredentials_ struct {\n\t// Username and password authentication.\n\tPasswordCredentials *SignInRequest_PasswordCredentials `protobuf:\"bytes,1,opt,name=password_credentials,json=passwordCredentials,proto3,oneof\"`\n}\n\ntype SignInRequest_SsoCredentials struct {\n\t// SSO provider authentication.\n\tSsoCredentials *SignInRequest_SSOCredentials `protobuf:\"bytes,2,opt,name=sso_credentials,json=ssoCredentials,proto3,oneof\"`\n}\n\nfunc (*SignInRequest_PasswordCredentials_) isSignInRequest_Credentials() {}\n\nfunc (*SignInRequest_SsoCredentials) isSignInRequest_Credentials() {}\n\ntype SignInResponse struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// The authenticated user's information.\n\tUser *User `protobuf:\"bytes,1,opt,name=user,proto3\" json:\"user,omitempty\"`\n\t// The short-lived access token for API requests.\n\t// Store in memory only, not in localStorage.\n\tAccessToken string `protobuf:\"bytes,2,opt,name=access_token,json=accessToken,proto3\" json:\"access_token,omitempty\"`\n\t// When the access token expires.\n\t// Client should call RefreshToken before this time.\n\tAccessTokenExpiresAt *timestamppb.Timestamp `protobuf:\"bytes,3,opt,name=access_token_expires_at,json=accessTokenExpiresAt,proto3\" json:\"access_token_expires_at,omitempty\"`\n\tunknownFields        protoimpl.UnknownFields\n\tsizeCache            protoimpl.SizeCache\n}\n\nfunc (x *SignInResponse) Reset() {\n\t*x = SignInResponse{}\n\tmi := &file_api_v1_auth_service_proto_msgTypes[3]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *SignInResponse) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*SignInResponse) ProtoMessage() {}\n\nfunc (x *SignInResponse) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_auth_service_proto_msgTypes[3]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use SignInResponse.ProtoReflect.Descriptor instead.\nfunc (*SignInResponse) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_auth_service_proto_rawDescGZIP(), []int{3}\n}\n\nfunc (x *SignInResponse) GetUser() *User {\n\tif x != nil {\n\t\treturn x.User\n\t}\n\treturn nil\n}\n\nfunc (x *SignInResponse) GetAccessToken() string {\n\tif x != nil {\n\t\treturn x.AccessToken\n\t}\n\treturn \"\"\n}\n\nfunc (x *SignInResponse) GetAccessTokenExpiresAt() *timestamppb.Timestamp {\n\tif x != nil {\n\t\treturn x.AccessTokenExpiresAt\n\t}\n\treturn nil\n}\n\ntype SignOutRequest struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *SignOutRequest) Reset() {\n\t*x = SignOutRequest{}\n\tmi := &file_api_v1_auth_service_proto_msgTypes[4]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *SignOutRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*SignOutRequest) ProtoMessage() {}\n\nfunc (x *SignOutRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_auth_service_proto_msgTypes[4]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use SignOutRequest.ProtoReflect.Descriptor instead.\nfunc (*SignOutRequest) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_auth_service_proto_rawDescGZIP(), []int{4}\n}\n\ntype RefreshTokenRequest struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *RefreshTokenRequest) Reset() {\n\t*x = RefreshTokenRequest{}\n\tmi := &file_api_v1_auth_service_proto_msgTypes[5]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *RefreshTokenRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*RefreshTokenRequest) ProtoMessage() {}\n\nfunc (x *RefreshTokenRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_auth_service_proto_msgTypes[5]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use RefreshTokenRequest.ProtoReflect.Descriptor instead.\nfunc (*RefreshTokenRequest) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_auth_service_proto_rawDescGZIP(), []int{5}\n}\n\ntype RefreshTokenResponse struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// The new short-lived access token.\n\tAccessToken string `protobuf:\"bytes,1,opt,name=access_token,json=accessToken,proto3\" json:\"access_token,omitempty\"`\n\t// When the access token expires.\n\tExpiresAt     *timestamppb.Timestamp `protobuf:\"bytes,2,opt,name=expires_at,json=expiresAt,proto3\" json:\"expires_at,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *RefreshTokenResponse) Reset() {\n\t*x = RefreshTokenResponse{}\n\tmi := &file_api_v1_auth_service_proto_msgTypes[6]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *RefreshTokenResponse) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*RefreshTokenResponse) ProtoMessage() {}\n\nfunc (x *RefreshTokenResponse) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_auth_service_proto_msgTypes[6]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use RefreshTokenResponse.ProtoReflect.Descriptor instead.\nfunc (*RefreshTokenResponse) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_auth_service_proto_rawDescGZIP(), []int{6}\n}\n\nfunc (x *RefreshTokenResponse) GetAccessToken() string {\n\tif x != nil {\n\t\treturn x.AccessToken\n\t}\n\treturn \"\"\n}\n\nfunc (x *RefreshTokenResponse) GetExpiresAt() *timestamppb.Timestamp {\n\tif x != nil {\n\t\treturn x.ExpiresAt\n\t}\n\treturn nil\n}\n\n// Nested message for password-based authentication credentials.\ntype SignInRequest_PasswordCredentials struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// The username to sign in with.\n\tUsername string `protobuf:\"bytes,1,opt,name=username,proto3\" json:\"username,omitempty\"`\n\t// The password to sign in with.\n\tPassword      string `protobuf:\"bytes,2,opt,name=password,proto3\" json:\"password,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *SignInRequest_PasswordCredentials) Reset() {\n\t*x = SignInRequest_PasswordCredentials{}\n\tmi := &file_api_v1_auth_service_proto_msgTypes[7]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *SignInRequest_PasswordCredentials) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*SignInRequest_PasswordCredentials) ProtoMessage() {}\n\nfunc (x *SignInRequest_PasswordCredentials) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_auth_service_proto_msgTypes[7]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use SignInRequest_PasswordCredentials.ProtoReflect.Descriptor instead.\nfunc (*SignInRequest_PasswordCredentials) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_auth_service_proto_rawDescGZIP(), []int{2, 0}\n}\n\nfunc (x *SignInRequest_PasswordCredentials) GetUsername() string {\n\tif x != nil {\n\t\treturn x.Username\n\t}\n\treturn \"\"\n}\n\nfunc (x *SignInRequest_PasswordCredentials) GetPassword() string {\n\tif x != nil {\n\t\treturn x.Password\n\t}\n\treturn \"\"\n}\n\n// Nested message for SSO authentication credentials.\ntype SignInRequest_SSOCredentials struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// The resource name of the SSO provider.\n\t// Format: identity-providers/{uid}\n\tIdpName string `protobuf:\"bytes,1,opt,name=idp_name,json=idpName,proto3\" json:\"idp_name,omitempty\"`\n\t// The authorization code from the SSO provider.\n\tCode string `protobuf:\"bytes,2,opt,name=code,proto3\" json:\"code,omitempty\"`\n\t// The redirect URI used in the SSO flow.\n\tRedirectUri string `protobuf:\"bytes,3,opt,name=redirect_uri,json=redirectUri,proto3\" json:\"redirect_uri,omitempty\"`\n\t// The PKCE code verifier for enhanced security (RFC 7636).\n\t// Optional - enables PKCE flow protection against authorization code interception.\n\tCodeVerifier  string `protobuf:\"bytes,4,opt,name=code_verifier,json=codeVerifier,proto3\" json:\"code_verifier,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *SignInRequest_SSOCredentials) Reset() {\n\t*x = SignInRequest_SSOCredentials{}\n\tmi := &file_api_v1_auth_service_proto_msgTypes[8]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *SignInRequest_SSOCredentials) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*SignInRequest_SSOCredentials) ProtoMessage() {}\n\nfunc (x *SignInRequest_SSOCredentials) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_auth_service_proto_msgTypes[8]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use SignInRequest_SSOCredentials.ProtoReflect.Descriptor instead.\nfunc (*SignInRequest_SSOCredentials) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_auth_service_proto_rawDescGZIP(), []int{2, 1}\n}\n\nfunc (x *SignInRequest_SSOCredentials) GetIdpName() string {\n\tif x != nil {\n\t\treturn x.IdpName\n\t}\n\treturn \"\"\n}\n\nfunc (x *SignInRequest_SSOCredentials) GetCode() string {\n\tif x != nil {\n\t\treturn x.Code\n\t}\n\treturn \"\"\n}\n\nfunc (x *SignInRequest_SSOCredentials) GetRedirectUri() string {\n\tif x != nil {\n\t\treturn x.RedirectUri\n\t}\n\treturn \"\"\n}\n\nfunc (x *SignInRequest_SSOCredentials) GetCodeVerifier() string {\n\tif x != nil {\n\t\treturn x.CodeVerifier\n\t}\n\treturn \"\"\n}\n\nvar File_api_v1_auth_service_proto protoreflect.FileDescriptor\n\nconst file_api_v1_auth_service_proto_rawDesc = \"\" +\n\t\"\\n\" +\n\t\"\\x19api/v1/auth_service.proto\\x12\\fmemos.api.v1\\x1a\\x19api/v1/user_service.proto\\x1a\\x1cgoogle/api/annotations.proto\\x1a\\x1fgoogle/api/field_behavior.proto\\x1a\\x1bgoogle/protobuf/empty.proto\\x1a\\x1fgoogle/protobuf/timestamp.proto\\\"\\x17\\n\" +\n\t\"\\x15GetCurrentUserRequest\\\"@\\n\" +\n\t\"\\x16GetCurrentUserResponse\\x12&\\n\" +\n\t\"\\x04user\\x18\\x01 \\x01(\\v2\\x12.memos.api.v1.UserR\\x04user\\\"\\xd2\\x03\\n\" +\n\t\"\\rSignInRequest\\x12d\\n\" +\n\t\"\\x14password_credentials\\x18\\x01 \\x01(\\v2/.memos.api.v1.SignInRequest.PasswordCredentialsH\\x00R\\x13passwordCredentials\\x12U\\n\" +\n\t\"\\x0fsso_credentials\\x18\\x02 \\x01(\\v2*.memos.api.v1.SignInRequest.SSOCredentialsH\\x00R\\x0essoCredentials\\x1aW\\n\" +\n\t\"\\x13PasswordCredentials\\x12\\x1f\\n\" +\n\t\"\\busername\\x18\\x01 \\x01(\\tB\\x03\\xe0A\\x02R\\busername\\x12\\x1f\\n\" +\n\t\"\\bpassword\\x18\\x02 \\x01(\\tB\\x03\\xe0A\\x02R\\bpassword\\x1a\\x9b\\x01\\n\" +\n\t\"\\x0eSSOCredentials\\x12\\x1e\\n\" +\n\t\"\\bidp_name\\x18\\x01 \\x01(\\tB\\x03\\xe0A\\x02R\\aidpName\\x12\\x17\\n\" +\n\t\"\\x04code\\x18\\x02 \\x01(\\tB\\x03\\xe0A\\x02R\\x04code\\x12&\\n\" +\n\t\"\\fredirect_uri\\x18\\x03 \\x01(\\tB\\x03\\xe0A\\x02R\\vredirectUri\\x12(\\n\" +\n\t\"\\rcode_verifier\\x18\\x04 \\x01(\\tB\\x03\\xe0A\\x01R\\fcodeVerifierB\\r\\n\" +\n\t\"\\vcredentials\\\"\\xae\\x01\\n\" +\n\t\"\\x0eSignInResponse\\x12&\\n\" +\n\t\"\\x04user\\x18\\x01 \\x01(\\v2\\x12.memos.api.v1.UserR\\x04user\\x12!\\n\" +\n\t\"\\faccess_token\\x18\\x02 \\x01(\\tR\\vaccessToken\\x12Q\\n\" +\n\t\"\\x17access_token_expires_at\\x18\\x03 \\x01(\\v2\\x1a.google.protobuf.TimestampR\\x14accessTokenExpiresAt\\\"\\x10\\n\" +\n\t\"\\x0eSignOutRequest\\\"\\x15\\n\" +\n\t\"\\x13RefreshTokenRequest\\\"t\\n\" +\n\t\"\\x14RefreshTokenResponse\\x12!\\n\" +\n\t\"\\faccess_token\\x18\\x01 \\x01(\\tR\\vaccessToken\\x129\\n\" +\n\t\"\\n\" +\n\t\"expires_at\\x18\\x02 \\x01(\\v2\\x1a.google.protobuf.TimestampR\\texpiresAt2\\xbf\\x03\\n\" +\n\t\"\\vAuthService\\x12t\\n\" +\n\t\"\\x0eGetCurrentUser\\x12#.memos.api.v1.GetCurrentUserRequest\\x1a$.memos.api.v1.GetCurrentUserResponse\\\"\\x17\\x82\\xd3\\xe4\\x93\\x02\\x11\\x12\\x0f/api/v1/auth/me\\x12c\\n\" +\n\t\"\\x06SignIn\\x12\\x1b.memos.api.v1.SignInRequest\\x1a\\x1c.memos.api.v1.SignInResponse\\\"\\x1e\\x82\\xd3\\xe4\\x93\\x02\\x18:\\x01*\\\"\\x13/api/v1/auth/signin\\x12]\\n\" +\n\t\"\\aSignOut\\x12\\x1c.memos.api.v1.SignOutRequest\\x1a\\x16.google.protobuf.Empty\\\"\\x1c\\x82\\xd3\\xe4\\x93\\x02\\x16\\\"\\x14/api/v1/auth/signout\\x12v\\n\" +\n\t\"\\fRefreshToken\\x12!.memos.api.v1.RefreshTokenRequest\\x1a\\\".memos.api.v1.RefreshTokenResponse\\\"\\x1f\\x82\\xd3\\xe4\\x93\\x02\\x19:\\x01*\\\"\\x14/api/v1/auth/refreshB\\xa8\\x01\\n\" +\n\t\"\\x10com.memos.api.v1B\\x10AuthServiceProtoP\\x01Z0github.com/usememos/memos/proto/gen/api/v1;apiv1\\xa2\\x02\\x03MAX\\xaa\\x02\\fMemos.Api.V1\\xca\\x02\\fMemos\\\\Api\\\\V1\\xe2\\x02\\x18Memos\\\\Api\\\\V1\\\\GPBMetadata\\xea\\x02\\x0eMemos::Api::V1b\\x06proto3\"\n\nvar (\n\tfile_api_v1_auth_service_proto_rawDescOnce sync.Once\n\tfile_api_v1_auth_service_proto_rawDescData []byte\n)\n\nfunc file_api_v1_auth_service_proto_rawDescGZIP() []byte {\n\tfile_api_v1_auth_service_proto_rawDescOnce.Do(func() {\n\t\tfile_api_v1_auth_service_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_api_v1_auth_service_proto_rawDesc), len(file_api_v1_auth_service_proto_rawDesc)))\n\t})\n\treturn file_api_v1_auth_service_proto_rawDescData\n}\n\nvar file_api_v1_auth_service_proto_msgTypes = make([]protoimpl.MessageInfo, 9)\nvar file_api_v1_auth_service_proto_goTypes = []any{\n\t(*GetCurrentUserRequest)(nil),             // 0: memos.api.v1.GetCurrentUserRequest\n\t(*GetCurrentUserResponse)(nil),            // 1: memos.api.v1.GetCurrentUserResponse\n\t(*SignInRequest)(nil),                     // 2: memos.api.v1.SignInRequest\n\t(*SignInResponse)(nil),                    // 3: memos.api.v1.SignInResponse\n\t(*SignOutRequest)(nil),                    // 4: memos.api.v1.SignOutRequest\n\t(*RefreshTokenRequest)(nil),               // 5: memos.api.v1.RefreshTokenRequest\n\t(*RefreshTokenResponse)(nil),              // 6: memos.api.v1.RefreshTokenResponse\n\t(*SignInRequest_PasswordCredentials)(nil), // 7: memos.api.v1.SignInRequest.PasswordCredentials\n\t(*SignInRequest_SSOCredentials)(nil),      // 8: memos.api.v1.SignInRequest.SSOCredentials\n\t(*User)(nil),                              // 9: memos.api.v1.User\n\t(*timestamppb.Timestamp)(nil),             // 10: google.protobuf.Timestamp\n\t(*emptypb.Empty)(nil),                     // 11: google.protobuf.Empty\n}\nvar file_api_v1_auth_service_proto_depIdxs = []int32{\n\t9,  // 0: memos.api.v1.GetCurrentUserResponse.user:type_name -> memos.api.v1.User\n\t7,  // 1: memos.api.v1.SignInRequest.password_credentials:type_name -> memos.api.v1.SignInRequest.PasswordCredentials\n\t8,  // 2: memos.api.v1.SignInRequest.sso_credentials:type_name -> memos.api.v1.SignInRequest.SSOCredentials\n\t9,  // 3: memos.api.v1.SignInResponse.user:type_name -> memos.api.v1.User\n\t10, // 4: memos.api.v1.SignInResponse.access_token_expires_at:type_name -> google.protobuf.Timestamp\n\t10, // 5: memos.api.v1.RefreshTokenResponse.expires_at:type_name -> google.protobuf.Timestamp\n\t0,  // 6: memos.api.v1.AuthService.GetCurrentUser:input_type -> memos.api.v1.GetCurrentUserRequest\n\t2,  // 7: memos.api.v1.AuthService.SignIn:input_type -> memos.api.v1.SignInRequest\n\t4,  // 8: memos.api.v1.AuthService.SignOut:input_type -> memos.api.v1.SignOutRequest\n\t5,  // 9: memos.api.v1.AuthService.RefreshToken:input_type -> memos.api.v1.RefreshTokenRequest\n\t1,  // 10: memos.api.v1.AuthService.GetCurrentUser:output_type -> memos.api.v1.GetCurrentUserResponse\n\t3,  // 11: memos.api.v1.AuthService.SignIn:output_type -> memos.api.v1.SignInResponse\n\t11, // 12: memos.api.v1.AuthService.SignOut:output_type -> google.protobuf.Empty\n\t6,  // 13: memos.api.v1.AuthService.RefreshToken:output_type -> memos.api.v1.RefreshTokenResponse\n\t10, // [10:14] is the sub-list for method output_type\n\t6,  // [6:10] is the sub-list for method input_type\n\t6,  // [6:6] is the sub-list for extension type_name\n\t6,  // [6:6] is the sub-list for extension extendee\n\t0,  // [0:6] is the sub-list for field type_name\n}\n\nfunc init() { file_api_v1_auth_service_proto_init() }\nfunc file_api_v1_auth_service_proto_init() {\n\tif File_api_v1_auth_service_proto != nil {\n\t\treturn\n\t}\n\tfile_api_v1_user_service_proto_init()\n\tfile_api_v1_auth_service_proto_msgTypes[2].OneofWrappers = []any{\n\t\t(*SignInRequest_PasswordCredentials_)(nil),\n\t\t(*SignInRequest_SsoCredentials)(nil),\n\t}\n\ttype x struct{}\n\tout := protoimpl.TypeBuilder{\n\t\tFile: protoimpl.DescBuilder{\n\t\t\tGoPackagePath: reflect.TypeOf(x{}).PkgPath(),\n\t\t\tRawDescriptor: unsafe.Slice(unsafe.StringData(file_api_v1_auth_service_proto_rawDesc), len(file_api_v1_auth_service_proto_rawDesc)),\n\t\t\tNumEnums:      0,\n\t\t\tNumMessages:   9,\n\t\t\tNumExtensions: 0,\n\t\t\tNumServices:   1,\n\t\t},\n\t\tGoTypes:           file_api_v1_auth_service_proto_goTypes,\n\t\tDependencyIndexes: file_api_v1_auth_service_proto_depIdxs,\n\t\tMessageInfos:      file_api_v1_auth_service_proto_msgTypes,\n\t}.Build()\n\tFile_api_v1_auth_service_proto = out.File\n\tfile_api_v1_auth_service_proto_goTypes = nil\n\tfile_api_v1_auth_service_proto_depIdxs = nil\n}\n"
  },
  {
    "path": "proto/gen/api/v1/auth_service.pb.gw.go",
    "content": "// Code generated by protoc-gen-grpc-gateway. DO NOT EDIT.\n// source: api/v1/auth_service.proto\n\n/*\nPackage apiv1 is a reverse proxy.\n\nIt translates gRPC into RESTful JSON APIs.\n*/\npackage apiv1\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"io\"\n\t\"net/http\"\n\n\t\"github.com/grpc-ecosystem/grpc-gateway/v2/runtime\"\n\t\"github.com/grpc-ecosystem/grpc-gateway/v2/utilities\"\n\t\"google.golang.org/grpc\"\n\t\"google.golang.org/grpc/codes\"\n\t\"google.golang.org/grpc/grpclog\"\n\t\"google.golang.org/grpc/metadata\"\n\t\"google.golang.org/grpc/status\"\n\t\"google.golang.org/protobuf/proto\"\n)\n\n// Suppress \"imported and not used\" errors\nvar (\n\t_ codes.Code\n\t_ io.Reader\n\t_ status.Status\n\t_ = errors.New\n\t_ = runtime.String\n\t_ = utilities.NewDoubleArray\n\t_ = metadata.Join\n)\n\nfunc request_AuthService_GetCurrentUser_0(ctx context.Context, marshaler runtime.Marshaler, client AuthServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq GetCurrentUserRequest\n\t\tmetadata runtime.ServerMetadata\n\t)\n\tif req.Body != nil {\n\t\t_, _ = io.Copy(io.Discard, req.Body)\n\t}\n\tmsg, err := client.GetCurrentUser(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))\n\treturn msg, metadata, err\n}\n\nfunc local_request_AuthService_GetCurrentUser_0(ctx context.Context, marshaler runtime.Marshaler, server AuthServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq GetCurrentUserRequest\n\t\tmetadata runtime.ServerMetadata\n\t)\n\tmsg, err := server.GetCurrentUser(ctx, &protoReq)\n\treturn msg, metadata, err\n}\n\nfunc request_AuthService_SignIn_0(ctx context.Context, marshaler runtime.Marshaler, client AuthServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq SignInRequest\n\t\tmetadata runtime.ServerMetadata\n\t)\n\tif err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tif req.Body != nil {\n\t\t_, _ = io.Copy(io.Discard, req.Body)\n\t}\n\tmsg, err := client.SignIn(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))\n\treturn msg, metadata, err\n}\n\nfunc local_request_AuthService_SignIn_0(ctx context.Context, marshaler runtime.Marshaler, server AuthServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq SignInRequest\n\t\tmetadata runtime.ServerMetadata\n\t)\n\tif err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tmsg, err := server.SignIn(ctx, &protoReq)\n\treturn msg, metadata, err\n}\n\nfunc request_AuthService_SignOut_0(ctx context.Context, marshaler runtime.Marshaler, client AuthServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq SignOutRequest\n\t\tmetadata runtime.ServerMetadata\n\t)\n\tif req.Body != nil {\n\t\t_, _ = io.Copy(io.Discard, req.Body)\n\t}\n\tmsg, err := client.SignOut(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))\n\treturn msg, metadata, err\n}\n\nfunc local_request_AuthService_SignOut_0(ctx context.Context, marshaler runtime.Marshaler, server AuthServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq SignOutRequest\n\t\tmetadata runtime.ServerMetadata\n\t)\n\tmsg, err := server.SignOut(ctx, &protoReq)\n\treturn msg, metadata, err\n}\n\nfunc request_AuthService_RefreshToken_0(ctx context.Context, marshaler runtime.Marshaler, client AuthServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq RefreshTokenRequest\n\t\tmetadata runtime.ServerMetadata\n\t)\n\tif err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tif req.Body != nil {\n\t\t_, _ = io.Copy(io.Discard, req.Body)\n\t}\n\tmsg, err := client.RefreshToken(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))\n\treturn msg, metadata, err\n}\n\nfunc local_request_AuthService_RefreshToken_0(ctx context.Context, marshaler runtime.Marshaler, server AuthServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq RefreshTokenRequest\n\t\tmetadata runtime.ServerMetadata\n\t)\n\tif err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tmsg, err := server.RefreshToken(ctx, &protoReq)\n\treturn msg, metadata, err\n}\n\n// RegisterAuthServiceHandlerServer registers the http handlers for service AuthService to \"mux\".\n// UnaryRPC     :call AuthServiceServer directly.\n// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906.\n// Note that using this registration option will cause many gRPC library features to stop working. Consider using RegisterAuthServiceHandlerFromEndpoint instead.\n// GRPC interceptors will not work for this type of registration. To use interceptors, you must use the \"runtime.WithMiddlewares\" option in the \"runtime.NewServeMux\" call.\nfunc RegisterAuthServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux, server AuthServiceServer) error {\n\tmux.Handle(http.MethodGet, pattern_AuthService_GetCurrentUser_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tvar stream runtime.ServerTransportStream\n\t\tctx = grpc.NewContextWithServerTransportStream(ctx, &stream)\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, \"/memos.api.v1.AuthService/GetCurrentUser\", runtime.WithHTTPPathPattern(\"/api/v1/auth/me\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := local_request_AuthService_GetCurrentUser_0(annotatedContext, inboundMarshaler, server, req, pathParams)\n\t\tmd.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_AuthService_GetCurrentUser_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodPost, pattern_AuthService_SignIn_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tvar stream runtime.ServerTransportStream\n\t\tctx = grpc.NewContextWithServerTransportStream(ctx, &stream)\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, \"/memos.api.v1.AuthService/SignIn\", runtime.WithHTTPPathPattern(\"/api/v1/auth/signin\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := local_request_AuthService_SignIn_0(annotatedContext, inboundMarshaler, server, req, pathParams)\n\t\tmd.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_AuthService_SignIn_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodPost, pattern_AuthService_SignOut_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tvar stream runtime.ServerTransportStream\n\t\tctx = grpc.NewContextWithServerTransportStream(ctx, &stream)\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, \"/memos.api.v1.AuthService/SignOut\", runtime.WithHTTPPathPattern(\"/api/v1/auth/signout\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := local_request_AuthService_SignOut_0(annotatedContext, inboundMarshaler, server, req, pathParams)\n\t\tmd.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_AuthService_SignOut_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodPost, pattern_AuthService_RefreshToken_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tvar stream runtime.ServerTransportStream\n\t\tctx = grpc.NewContextWithServerTransportStream(ctx, &stream)\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, \"/memos.api.v1.AuthService/RefreshToken\", runtime.WithHTTPPathPattern(\"/api/v1/auth/refresh\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := local_request_AuthService_RefreshToken_0(annotatedContext, inboundMarshaler, server, req, pathParams)\n\t\tmd.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_AuthService_RefreshToken_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\n\treturn nil\n}\n\n// RegisterAuthServiceHandlerFromEndpoint is same as RegisterAuthServiceHandler but\n// automatically dials to \"endpoint\" and closes the connection when \"ctx\" gets done.\nfunc RegisterAuthServiceHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) {\n\tconn, err := grpc.NewClient(endpoint, opts...)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer func() {\n\t\tif err != nil {\n\t\t\tif cerr := conn.Close(); cerr != nil {\n\t\t\t\tgrpclog.Errorf(\"Failed to close conn to %s: %v\", endpoint, cerr)\n\t\t\t}\n\t\t\treturn\n\t\t}\n\t\tgo func() {\n\t\t\t<-ctx.Done()\n\t\t\tif cerr := conn.Close(); cerr != nil {\n\t\t\t\tgrpclog.Errorf(\"Failed to close conn to %s: %v\", endpoint, cerr)\n\t\t\t}\n\t\t}()\n\t}()\n\treturn RegisterAuthServiceHandler(ctx, mux, conn)\n}\n\n// RegisterAuthServiceHandler registers the http handlers for service AuthService to \"mux\".\n// The handlers forward requests to the grpc endpoint over \"conn\".\nfunc RegisterAuthServiceHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error {\n\treturn RegisterAuthServiceHandlerClient(ctx, mux, NewAuthServiceClient(conn))\n}\n\n// RegisterAuthServiceHandlerClient registers the http handlers for service AuthService\n// to \"mux\". The handlers forward requests to the grpc endpoint over the given implementation of \"AuthServiceClient\".\n// Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in \"AuthServiceClient\"\n// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in\n// \"AuthServiceClient\" to call the correct interceptors. This client ignores the HTTP middlewares.\nfunc RegisterAuthServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux, client AuthServiceClient) error {\n\tmux.Handle(http.MethodGet, pattern_AuthService_GetCurrentUser_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateContext(ctx, mux, req, \"/memos.api.v1.AuthService/GetCurrentUser\", runtime.WithHTTPPathPattern(\"/api/v1/auth/me\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := request_AuthService_GetCurrentUser_0(annotatedContext, inboundMarshaler, client, req, pathParams)\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_AuthService_GetCurrentUser_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodPost, pattern_AuthService_SignIn_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateContext(ctx, mux, req, \"/memos.api.v1.AuthService/SignIn\", runtime.WithHTTPPathPattern(\"/api/v1/auth/signin\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := request_AuthService_SignIn_0(annotatedContext, inboundMarshaler, client, req, pathParams)\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_AuthService_SignIn_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodPost, pattern_AuthService_SignOut_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateContext(ctx, mux, req, \"/memos.api.v1.AuthService/SignOut\", runtime.WithHTTPPathPattern(\"/api/v1/auth/signout\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := request_AuthService_SignOut_0(annotatedContext, inboundMarshaler, client, req, pathParams)\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_AuthService_SignOut_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodPost, pattern_AuthService_RefreshToken_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateContext(ctx, mux, req, \"/memos.api.v1.AuthService/RefreshToken\", runtime.WithHTTPPathPattern(\"/api/v1/auth/refresh\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := request_AuthService_RefreshToken_0(annotatedContext, inboundMarshaler, client, req, pathParams)\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_AuthService_RefreshToken_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\treturn nil\n}\n\nvar (\n\tpattern_AuthService_GetCurrentUser_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{\"api\", \"v1\", \"auth\", \"me\"}, \"\"))\n\tpattern_AuthService_SignIn_0         = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{\"api\", \"v1\", \"auth\", \"signin\"}, \"\"))\n\tpattern_AuthService_SignOut_0        = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{\"api\", \"v1\", \"auth\", \"signout\"}, \"\"))\n\tpattern_AuthService_RefreshToken_0   = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{\"api\", \"v1\", \"auth\", \"refresh\"}, \"\"))\n)\n\nvar (\n\tforward_AuthService_GetCurrentUser_0 = runtime.ForwardResponseMessage\n\tforward_AuthService_SignIn_0         = runtime.ForwardResponseMessage\n\tforward_AuthService_SignOut_0        = runtime.ForwardResponseMessage\n\tforward_AuthService_RefreshToken_0   = runtime.ForwardResponseMessage\n)\n"
  },
  {
    "path": "proto/gen/api/v1/auth_service_grpc.pb.go",
    "content": "// Code generated by protoc-gen-go-grpc. DO NOT EDIT.\n// versions:\n// - protoc-gen-go-grpc v1.6.1\n// - protoc             (unknown)\n// source: api/v1/auth_service.proto\n\npackage apiv1\n\nimport (\n\tcontext \"context\"\n\tgrpc \"google.golang.org/grpc\"\n\tcodes \"google.golang.org/grpc/codes\"\n\tstatus \"google.golang.org/grpc/status\"\n\temptypb \"google.golang.org/protobuf/types/known/emptypb\"\n)\n\n// This is a compile-time assertion to ensure that this generated file\n// is compatible with the grpc package it is being compiled against.\n// Requires gRPC-Go v1.64.0 or later.\nconst _ = grpc.SupportPackageIsVersion9\n\nconst (\n\tAuthService_GetCurrentUser_FullMethodName = \"/memos.api.v1.AuthService/GetCurrentUser\"\n\tAuthService_SignIn_FullMethodName         = \"/memos.api.v1.AuthService/SignIn\"\n\tAuthService_SignOut_FullMethodName        = \"/memos.api.v1.AuthService/SignOut\"\n\tAuthService_RefreshToken_FullMethodName   = \"/memos.api.v1.AuthService/RefreshToken\"\n)\n\n// AuthServiceClient is the client API for AuthService service.\n//\n// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.\ntype AuthServiceClient interface {\n\t// GetCurrentUser returns the authenticated user's information.\n\t// Validates the access token and returns user details.\n\t// Similar to OIDC's /userinfo endpoint.\n\tGetCurrentUser(ctx context.Context, in *GetCurrentUserRequest, opts ...grpc.CallOption) (*GetCurrentUserResponse, error)\n\t// SignIn authenticates a user with credentials and returns tokens.\n\t// On success, returns an access token and sets a refresh token cookie.\n\t// Supports password-based and SSO authentication methods.\n\tSignIn(ctx context.Context, in *SignInRequest, opts ...grpc.CallOption) (*SignInResponse, error)\n\t// SignOut terminates the user's authentication.\n\t// Revokes the refresh token and clears the authentication cookie.\n\tSignOut(ctx context.Context, in *SignOutRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)\n\t// RefreshToken exchanges a valid refresh token for a new access token.\n\t// The refresh token is read from the HttpOnly cookie.\n\t// Returns a new short-lived access token.\n\tRefreshToken(ctx context.Context, in *RefreshTokenRequest, opts ...grpc.CallOption) (*RefreshTokenResponse, error)\n}\n\ntype authServiceClient struct {\n\tcc grpc.ClientConnInterface\n}\n\nfunc NewAuthServiceClient(cc grpc.ClientConnInterface) AuthServiceClient {\n\treturn &authServiceClient{cc}\n}\n\nfunc (c *authServiceClient) GetCurrentUser(ctx context.Context, in *GetCurrentUserRequest, opts ...grpc.CallOption) (*GetCurrentUserResponse, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(GetCurrentUserResponse)\n\terr := c.cc.Invoke(ctx, AuthService_GetCurrentUser_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *authServiceClient) SignIn(ctx context.Context, in *SignInRequest, opts ...grpc.CallOption) (*SignInResponse, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(SignInResponse)\n\terr := c.cc.Invoke(ctx, AuthService_SignIn_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *authServiceClient) SignOut(ctx context.Context, in *SignOutRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(emptypb.Empty)\n\terr := c.cc.Invoke(ctx, AuthService_SignOut_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *authServiceClient) RefreshToken(ctx context.Context, in *RefreshTokenRequest, opts ...grpc.CallOption) (*RefreshTokenResponse, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(RefreshTokenResponse)\n\terr := c.cc.Invoke(ctx, AuthService_RefreshToken_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\n// AuthServiceServer is the server API for AuthService service.\n// All implementations must embed UnimplementedAuthServiceServer\n// for forward compatibility.\ntype AuthServiceServer interface {\n\t// GetCurrentUser returns the authenticated user's information.\n\t// Validates the access token and returns user details.\n\t// Similar to OIDC's /userinfo endpoint.\n\tGetCurrentUser(context.Context, *GetCurrentUserRequest) (*GetCurrentUserResponse, error)\n\t// SignIn authenticates a user with credentials and returns tokens.\n\t// On success, returns an access token and sets a refresh token cookie.\n\t// Supports password-based and SSO authentication methods.\n\tSignIn(context.Context, *SignInRequest) (*SignInResponse, error)\n\t// SignOut terminates the user's authentication.\n\t// Revokes the refresh token and clears the authentication cookie.\n\tSignOut(context.Context, *SignOutRequest) (*emptypb.Empty, error)\n\t// RefreshToken exchanges a valid refresh token for a new access token.\n\t// The refresh token is read from the HttpOnly cookie.\n\t// Returns a new short-lived access token.\n\tRefreshToken(context.Context, *RefreshTokenRequest) (*RefreshTokenResponse, error)\n\tmustEmbedUnimplementedAuthServiceServer()\n}\n\n// UnimplementedAuthServiceServer must be embedded to have\n// forward compatible implementations.\n//\n// NOTE: this should be embedded by value instead of pointer to avoid a nil\n// pointer dereference when methods are called.\ntype UnimplementedAuthServiceServer struct{}\n\nfunc (UnimplementedAuthServiceServer) GetCurrentUser(context.Context, *GetCurrentUserRequest) (*GetCurrentUserResponse, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method GetCurrentUser not implemented\")\n}\nfunc (UnimplementedAuthServiceServer) SignIn(context.Context, *SignInRequest) (*SignInResponse, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method SignIn not implemented\")\n}\nfunc (UnimplementedAuthServiceServer) SignOut(context.Context, *SignOutRequest) (*emptypb.Empty, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method SignOut not implemented\")\n}\nfunc (UnimplementedAuthServiceServer) RefreshToken(context.Context, *RefreshTokenRequest) (*RefreshTokenResponse, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method RefreshToken not implemented\")\n}\nfunc (UnimplementedAuthServiceServer) mustEmbedUnimplementedAuthServiceServer() {}\nfunc (UnimplementedAuthServiceServer) testEmbeddedByValue()                     {}\n\n// UnsafeAuthServiceServer may be embedded to opt out of forward compatibility for this service.\n// Use of this interface is not recommended, as added methods to AuthServiceServer will\n// result in compilation errors.\ntype UnsafeAuthServiceServer interface {\n\tmustEmbedUnimplementedAuthServiceServer()\n}\n\nfunc RegisterAuthServiceServer(s grpc.ServiceRegistrar, srv AuthServiceServer) {\n\t// If the following call panics, it indicates UnimplementedAuthServiceServer was\n\t// embedded by pointer and is nil.  This will cause panics if an\n\t// unimplemented method is ever invoked, so we test this at initialization\n\t// time to prevent it from happening at runtime later due to I/O.\n\tif t, ok := srv.(interface{ testEmbeddedByValue() }); ok {\n\t\tt.testEmbeddedByValue()\n\t}\n\ts.RegisterService(&AuthService_ServiceDesc, srv)\n}\n\nfunc _AuthService_GetCurrentUser_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(GetCurrentUserRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(AuthServiceServer).GetCurrentUser(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: AuthService_GetCurrentUser_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(AuthServiceServer).GetCurrentUser(ctx, req.(*GetCurrentUserRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _AuthService_SignIn_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(SignInRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(AuthServiceServer).SignIn(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: AuthService_SignIn_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(AuthServiceServer).SignIn(ctx, req.(*SignInRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _AuthService_SignOut_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(SignOutRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(AuthServiceServer).SignOut(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: AuthService_SignOut_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(AuthServiceServer).SignOut(ctx, req.(*SignOutRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _AuthService_RefreshToken_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(RefreshTokenRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(AuthServiceServer).RefreshToken(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: AuthService_RefreshToken_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(AuthServiceServer).RefreshToken(ctx, req.(*RefreshTokenRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\n// AuthService_ServiceDesc is the grpc.ServiceDesc for AuthService service.\n// It's only intended for direct use with grpc.RegisterService,\n// and not to be introspected or modified (even as a copy)\nvar AuthService_ServiceDesc = grpc.ServiceDesc{\n\tServiceName: \"memos.api.v1.AuthService\",\n\tHandlerType: (*AuthServiceServer)(nil),\n\tMethods: []grpc.MethodDesc{\n\t\t{\n\t\t\tMethodName: \"GetCurrentUser\",\n\t\t\tHandler:    _AuthService_GetCurrentUser_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"SignIn\",\n\t\t\tHandler:    _AuthService_SignIn_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"SignOut\",\n\t\t\tHandler:    _AuthService_SignOut_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"RefreshToken\",\n\t\t\tHandler:    _AuthService_RefreshToken_Handler,\n\t\t},\n\t},\n\tStreams:  []grpc.StreamDesc{},\n\tMetadata: \"api/v1/auth_service.proto\",\n}\n"
  },
  {
    "path": "proto/gen/api/v1/common.pb.go",
    "content": "// Code generated by protoc-gen-go. DO NOT EDIT.\n// versions:\n// \tprotoc-gen-go v1.36.11\n// \tprotoc        (unknown)\n// source: api/v1/common.proto\n\npackage apiv1\n\nimport (\n\tprotoreflect \"google.golang.org/protobuf/reflect/protoreflect\"\n\tprotoimpl \"google.golang.org/protobuf/runtime/protoimpl\"\n\treflect \"reflect\"\n\tsync \"sync\"\n\tunsafe \"unsafe\"\n)\n\nconst (\n\t// Verify that this generated code is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)\n\t// Verify that runtime/protoimpl is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)\n)\n\ntype State int32\n\nconst (\n\tState_STATE_UNSPECIFIED State = 0\n\tState_NORMAL            State = 1\n\tState_ARCHIVED          State = 2\n)\n\n// Enum value maps for State.\nvar (\n\tState_name = map[int32]string{\n\t\t0: \"STATE_UNSPECIFIED\",\n\t\t1: \"NORMAL\",\n\t\t2: \"ARCHIVED\",\n\t}\n\tState_value = map[string]int32{\n\t\t\"STATE_UNSPECIFIED\": 0,\n\t\t\"NORMAL\":            1,\n\t\t\"ARCHIVED\":          2,\n\t}\n)\n\nfunc (x State) Enum() *State {\n\tp := new(State)\n\t*p = x\n\treturn p\n}\n\nfunc (x State) String() string {\n\treturn protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))\n}\n\nfunc (State) Descriptor() protoreflect.EnumDescriptor {\n\treturn file_api_v1_common_proto_enumTypes[0].Descriptor()\n}\n\nfunc (State) Type() protoreflect.EnumType {\n\treturn &file_api_v1_common_proto_enumTypes[0]\n}\n\nfunc (x State) Number() protoreflect.EnumNumber {\n\treturn protoreflect.EnumNumber(x)\n}\n\n// Deprecated: Use State.Descriptor instead.\nfunc (State) EnumDescriptor() ([]byte, []int) {\n\treturn file_api_v1_common_proto_rawDescGZIP(), []int{0}\n}\n\ntype Direction int32\n\nconst (\n\tDirection_DIRECTION_UNSPECIFIED Direction = 0\n\tDirection_ASC                   Direction = 1\n\tDirection_DESC                  Direction = 2\n)\n\n// Enum value maps for Direction.\nvar (\n\tDirection_name = map[int32]string{\n\t\t0: \"DIRECTION_UNSPECIFIED\",\n\t\t1: \"ASC\",\n\t\t2: \"DESC\",\n\t}\n\tDirection_value = map[string]int32{\n\t\t\"DIRECTION_UNSPECIFIED\": 0,\n\t\t\"ASC\":                   1,\n\t\t\"DESC\":                  2,\n\t}\n)\n\nfunc (x Direction) Enum() *Direction {\n\tp := new(Direction)\n\t*p = x\n\treturn p\n}\n\nfunc (x Direction) String() string {\n\treturn protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))\n}\n\nfunc (Direction) Descriptor() protoreflect.EnumDescriptor {\n\treturn file_api_v1_common_proto_enumTypes[1].Descriptor()\n}\n\nfunc (Direction) Type() protoreflect.EnumType {\n\treturn &file_api_v1_common_proto_enumTypes[1]\n}\n\nfunc (x Direction) Number() protoreflect.EnumNumber {\n\treturn protoreflect.EnumNumber(x)\n}\n\n// Deprecated: Use Direction.Descriptor instead.\nfunc (Direction) EnumDescriptor() ([]byte, []int) {\n\treturn file_api_v1_common_proto_rawDescGZIP(), []int{1}\n}\n\n// Used internally for obfuscating the page token.\ntype PageToken struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tLimit         int32                  `protobuf:\"varint,1,opt,name=limit,proto3\" json:\"limit,omitempty\"`\n\tOffset        int32                  `protobuf:\"varint,2,opt,name=offset,proto3\" json:\"offset,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *PageToken) Reset() {\n\t*x = PageToken{}\n\tmi := &file_api_v1_common_proto_msgTypes[0]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *PageToken) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*PageToken) ProtoMessage() {}\n\nfunc (x *PageToken) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_common_proto_msgTypes[0]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use PageToken.ProtoReflect.Descriptor instead.\nfunc (*PageToken) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_common_proto_rawDescGZIP(), []int{0}\n}\n\nfunc (x *PageToken) GetLimit() int32 {\n\tif x != nil {\n\t\treturn x.Limit\n\t}\n\treturn 0\n}\n\nfunc (x *PageToken) GetOffset() int32 {\n\tif x != nil {\n\t\treturn x.Offset\n\t}\n\treturn 0\n}\n\nvar File_api_v1_common_proto protoreflect.FileDescriptor\n\nconst file_api_v1_common_proto_rawDesc = \"\" +\n\t\"\\n\" +\n\t\"\\x13api/v1/common.proto\\x12\\fmemos.api.v1\\\"9\\n\" +\n\t\"\\tPageToken\\x12\\x14\\n\" +\n\t\"\\x05limit\\x18\\x01 \\x01(\\x05R\\x05limit\\x12\\x16\\n\" +\n\t\"\\x06offset\\x18\\x02 \\x01(\\x05R\\x06offset*8\\n\" +\n\t\"\\x05State\\x12\\x15\\n\" +\n\t\"\\x11STATE_UNSPECIFIED\\x10\\x00\\x12\\n\" +\n\t\"\\n\" +\n\t\"\\x06NORMAL\\x10\\x01\\x12\\f\\n\" +\n\t\"\\bARCHIVED\\x10\\x02*9\\n\" +\n\t\"\\tDirection\\x12\\x19\\n\" +\n\t\"\\x15DIRECTION_UNSPECIFIED\\x10\\x00\\x12\\a\\n\" +\n\t\"\\x03ASC\\x10\\x01\\x12\\b\\n\" +\n\t\"\\x04DESC\\x10\\x02B\\xa3\\x01\\n\" +\n\t\"\\x10com.memos.api.v1B\\vCommonProtoP\\x01Z0github.com/usememos/memos/proto/gen/api/v1;apiv1\\xa2\\x02\\x03MAX\\xaa\\x02\\fMemos.Api.V1\\xca\\x02\\fMemos\\\\Api\\\\V1\\xe2\\x02\\x18Memos\\\\Api\\\\V1\\\\GPBMetadata\\xea\\x02\\x0eMemos::Api::V1b\\x06proto3\"\n\nvar (\n\tfile_api_v1_common_proto_rawDescOnce sync.Once\n\tfile_api_v1_common_proto_rawDescData []byte\n)\n\nfunc file_api_v1_common_proto_rawDescGZIP() []byte {\n\tfile_api_v1_common_proto_rawDescOnce.Do(func() {\n\t\tfile_api_v1_common_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_api_v1_common_proto_rawDesc), len(file_api_v1_common_proto_rawDesc)))\n\t})\n\treturn file_api_v1_common_proto_rawDescData\n}\n\nvar file_api_v1_common_proto_enumTypes = make([]protoimpl.EnumInfo, 2)\nvar file_api_v1_common_proto_msgTypes = make([]protoimpl.MessageInfo, 1)\nvar file_api_v1_common_proto_goTypes = []any{\n\t(State)(0),        // 0: memos.api.v1.State\n\t(Direction)(0),    // 1: memos.api.v1.Direction\n\t(*PageToken)(nil), // 2: memos.api.v1.PageToken\n}\nvar file_api_v1_common_proto_depIdxs = []int32{\n\t0, // [0:0] is the sub-list for method output_type\n\t0, // [0:0] is the sub-list for method input_type\n\t0, // [0:0] is the sub-list for extension type_name\n\t0, // [0:0] is the sub-list for extension extendee\n\t0, // [0:0] is the sub-list for field type_name\n}\n\nfunc init() { file_api_v1_common_proto_init() }\nfunc file_api_v1_common_proto_init() {\n\tif File_api_v1_common_proto != nil {\n\t\treturn\n\t}\n\ttype x struct{}\n\tout := protoimpl.TypeBuilder{\n\t\tFile: protoimpl.DescBuilder{\n\t\t\tGoPackagePath: reflect.TypeOf(x{}).PkgPath(),\n\t\t\tRawDescriptor: unsafe.Slice(unsafe.StringData(file_api_v1_common_proto_rawDesc), len(file_api_v1_common_proto_rawDesc)),\n\t\t\tNumEnums:      2,\n\t\t\tNumMessages:   1,\n\t\t\tNumExtensions: 0,\n\t\t\tNumServices:   0,\n\t\t},\n\t\tGoTypes:           file_api_v1_common_proto_goTypes,\n\t\tDependencyIndexes: file_api_v1_common_proto_depIdxs,\n\t\tEnumInfos:         file_api_v1_common_proto_enumTypes,\n\t\tMessageInfos:      file_api_v1_common_proto_msgTypes,\n\t}.Build()\n\tFile_api_v1_common_proto = out.File\n\tfile_api_v1_common_proto_goTypes = nil\n\tfile_api_v1_common_proto_depIdxs = nil\n}\n"
  },
  {
    "path": "proto/gen/api/v1/idp_service.pb.go",
    "content": "// Code generated by protoc-gen-go. DO NOT EDIT.\n// versions:\n// \tprotoc-gen-go v1.36.11\n// \tprotoc        (unknown)\n// source: api/v1/idp_service.proto\n\npackage apiv1\n\nimport (\n\t_ \"google.golang.org/genproto/googleapis/api/annotations\"\n\tprotoreflect \"google.golang.org/protobuf/reflect/protoreflect\"\n\tprotoimpl \"google.golang.org/protobuf/runtime/protoimpl\"\n\temptypb \"google.golang.org/protobuf/types/known/emptypb\"\n\tfieldmaskpb \"google.golang.org/protobuf/types/known/fieldmaskpb\"\n\treflect \"reflect\"\n\tsync \"sync\"\n\tunsafe \"unsafe\"\n)\n\nconst (\n\t// Verify that this generated code is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)\n\t// Verify that runtime/protoimpl is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)\n)\n\ntype IdentityProvider_Type int32\n\nconst (\n\tIdentityProvider_TYPE_UNSPECIFIED IdentityProvider_Type = 0\n\t// OAuth2 identity provider.\n\tIdentityProvider_OAUTH2 IdentityProvider_Type = 1\n)\n\n// Enum value maps for IdentityProvider_Type.\nvar (\n\tIdentityProvider_Type_name = map[int32]string{\n\t\t0: \"TYPE_UNSPECIFIED\",\n\t\t1: \"OAUTH2\",\n\t}\n\tIdentityProvider_Type_value = map[string]int32{\n\t\t\"TYPE_UNSPECIFIED\": 0,\n\t\t\"OAUTH2\":           1,\n\t}\n)\n\nfunc (x IdentityProvider_Type) Enum() *IdentityProvider_Type {\n\tp := new(IdentityProvider_Type)\n\t*p = x\n\treturn p\n}\n\nfunc (x IdentityProvider_Type) String() string {\n\treturn protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))\n}\n\nfunc (IdentityProvider_Type) Descriptor() protoreflect.EnumDescriptor {\n\treturn file_api_v1_idp_service_proto_enumTypes[0].Descriptor()\n}\n\nfunc (IdentityProvider_Type) Type() protoreflect.EnumType {\n\treturn &file_api_v1_idp_service_proto_enumTypes[0]\n}\n\nfunc (x IdentityProvider_Type) Number() protoreflect.EnumNumber {\n\treturn protoreflect.EnumNumber(x)\n}\n\n// Deprecated: Use IdentityProvider_Type.Descriptor instead.\nfunc (IdentityProvider_Type) EnumDescriptor() ([]byte, []int) {\n\treturn file_api_v1_idp_service_proto_rawDescGZIP(), []int{0, 0}\n}\n\ntype IdentityProvider struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// The resource name of the identity provider.\n\t// Format: identity-providers/{idp}\n\tName string `protobuf:\"bytes,1,opt,name=name,proto3\" json:\"name,omitempty\"`\n\t// Required. The type of the identity provider.\n\tType IdentityProvider_Type `protobuf:\"varint,2,opt,name=type,proto3,enum=memos.api.v1.IdentityProvider_Type\" json:\"type,omitempty\"`\n\t// Required. The display title of the identity provider.\n\tTitle string `protobuf:\"bytes,3,opt,name=title,proto3\" json:\"title,omitempty\"`\n\t// Optional. Filter applied to user identifiers.\n\tIdentifierFilter string `protobuf:\"bytes,4,opt,name=identifier_filter,json=identifierFilter,proto3\" json:\"identifier_filter,omitempty\"`\n\t// Required. Configuration for the identity provider.\n\tConfig        *IdentityProviderConfig `protobuf:\"bytes,5,opt,name=config,proto3\" json:\"config,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *IdentityProvider) Reset() {\n\t*x = IdentityProvider{}\n\tmi := &file_api_v1_idp_service_proto_msgTypes[0]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *IdentityProvider) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*IdentityProvider) ProtoMessage() {}\n\nfunc (x *IdentityProvider) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_idp_service_proto_msgTypes[0]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use IdentityProvider.ProtoReflect.Descriptor instead.\nfunc (*IdentityProvider) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_idp_service_proto_rawDescGZIP(), []int{0}\n}\n\nfunc (x *IdentityProvider) GetName() string {\n\tif x != nil {\n\t\treturn x.Name\n\t}\n\treturn \"\"\n}\n\nfunc (x *IdentityProvider) GetType() IdentityProvider_Type {\n\tif x != nil {\n\t\treturn x.Type\n\t}\n\treturn IdentityProvider_TYPE_UNSPECIFIED\n}\n\nfunc (x *IdentityProvider) GetTitle() string {\n\tif x != nil {\n\t\treturn x.Title\n\t}\n\treturn \"\"\n}\n\nfunc (x *IdentityProvider) GetIdentifierFilter() string {\n\tif x != nil {\n\t\treturn x.IdentifierFilter\n\t}\n\treturn \"\"\n}\n\nfunc (x *IdentityProvider) GetConfig() *IdentityProviderConfig {\n\tif x != nil {\n\t\treturn x.Config\n\t}\n\treturn nil\n}\n\ntype IdentityProviderConfig struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// Types that are valid to be assigned to Config:\n\t//\n\t//\t*IdentityProviderConfig_Oauth2Config\n\tConfig        isIdentityProviderConfig_Config `protobuf_oneof:\"config\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *IdentityProviderConfig) Reset() {\n\t*x = IdentityProviderConfig{}\n\tmi := &file_api_v1_idp_service_proto_msgTypes[1]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *IdentityProviderConfig) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*IdentityProviderConfig) ProtoMessage() {}\n\nfunc (x *IdentityProviderConfig) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_idp_service_proto_msgTypes[1]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use IdentityProviderConfig.ProtoReflect.Descriptor instead.\nfunc (*IdentityProviderConfig) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_idp_service_proto_rawDescGZIP(), []int{1}\n}\n\nfunc (x *IdentityProviderConfig) GetConfig() isIdentityProviderConfig_Config {\n\tif x != nil {\n\t\treturn x.Config\n\t}\n\treturn nil\n}\n\nfunc (x *IdentityProviderConfig) GetOauth2Config() *OAuth2Config {\n\tif x != nil {\n\t\tif x, ok := x.Config.(*IdentityProviderConfig_Oauth2Config); ok {\n\t\t\treturn x.Oauth2Config\n\t\t}\n\t}\n\treturn nil\n}\n\ntype isIdentityProviderConfig_Config interface {\n\tisIdentityProviderConfig_Config()\n}\n\ntype IdentityProviderConfig_Oauth2Config struct {\n\tOauth2Config *OAuth2Config `protobuf:\"bytes,1,opt,name=oauth2_config,json=oauth2Config,proto3,oneof\"`\n}\n\nfunc (*IdentityProviderConfig_Oauth2Config) isIdentityProviderConfig_Config() {}\n\ntype FieldMapping struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tIdentifier    string                 `protobuf:\"bytes,1,opt,name=identifier,proto3\" json:\"identifier,omitempty\"`\n\tDisplayName   string                 `protobuf:\"bytes,2,opt,name=display_name,json=displayName,proto3\" json:\"display_name,omitempty\"`\n\tEmail         string                 `protobuf:\"bytes,3,opt,name=email,proto3\" json:\"email,omitempty\"`\n\tAvatarUrl     string                 `protobuf:\"bytes,4,opt,name=avatar_url,json=avatarUrl,proto3\" json:\"avatar_url,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *FieldMapping) Reset() {\n\t*x = FieldMapping{}\n\tmi := &file_api_v1_idp_service_proto_msgTypes[2]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *FieldMapping) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*FieldMapping) ProtoMessage() {}\n\nfunc (x *FieldMapping) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_idp_service_proto_msgTypes[2]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use FieldMapping.ProtoReflect.Descriptor instead.\nfunc (*FieldMapping) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_idp_service_proto_rawDescGZIP(), []int{2}\n}\n\nfunc (x *FieldMapping) GetIdentifier() string {\n\tif x != nil {\n\t\treturn x.Identifier\n\t}\n\treturn \"\"\n}\n\nfunc (x *FieldMapping) GetDisplayName() string {\n\tif x != nil {\n\t\treturn x.DisplayName\n\t}\n\treturn \"\"\n}\n\nfunc (x *FieldMapping) GetEmail() string {\n\tif x != nil {\n\t\treturn x.Email\n\t}\n\treturn \"\"\n}\n\nfunc (x *FieldMapping) GetAvatarUrl() string {\n\tif x != nil {\n\t\treturn x.AvatarUrl\n\t}\n\treturn \"\"\n}\n\ntype OAuth2Config struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tClientId      string                 `protobuf:\"bytes,1,opt,name=client_id,json=clientId,proto3\" json:\"client_id,omitempty\"`\n\tClientSecret  string                 `protobuf:\"bytes,2,opt,name=client_secret,json=clientSecret,proto3\" json:\"client_secret,omitempty\"`\n\tAuthUrl       string                 `protobuf:\"bytes,3,opt,name=auth_url,json=authUrl,proto3\" json:\"auth_url,omitempty\"`\n\tTokenUrl      string                 `protobuf:\"bytes,4,opt,name=token_url,json=tokenUrl,proto3\" json:\"token_url,omitempty\"`\n\tUserInfoUrl   string                 `protobuf:\"bytes,5,opt,name=user_info_url,json=userInfoUrl,proto3\" json:\"user_info_url,omitempty\"`\n\tScopes        []string               `protobuf:\"bytes,6,rep,name=scopes,proto3\" json:\"scopes,omitempty\"`\n\tFieldMapping  *FieldMapping          `protobuf:\"bytes,7,opt,name=field_mapping,json=fieldMapping,proto3\" json:\"field_mapping,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *OAuth2Config) Reset() {\n\t*x = OAuth2Config{}\n\tmi := &file_api_v1_idp_service_proto_msgTypes[3]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *OAuth2Config) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*OAuth2Config) ProtoMessage() {}\n\nfunc (x *OAuth2Config) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_idp_service_proto_msgTypes[3]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use OAuth2Config.ProtoReflect.Descriptor instead.\nfunc (*OAuth2Config) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_idp_service_proto_rawDescGZIP(), []int{3}\n}\n\nfunc (x *OAuth2Config) GetClientId() string {\n\tif x != nil {\n\t\treturn x.ClientId\n\t}\n\treturn \"\"\n}\n\nfunc (x *OAuth2Config) GetClientSecret() string {\n\tif x != nil {\n\t\treturn x.ClientSecret\n\t}\n\treturn \"\"\n}\n\nfunc (x *OAuth2Config) GetAuthUrl() string {\n\tif x != nil {\n\t\treturn x.AuthUrl\n\t}\n\treturn \"\"\n}\n\nfunc (x *OAuth2Config) GetTokenUrl() string {\n\tif x != nil {\n\t\treturn x.TokenUrl\n\t}\n\treturn \"\"\n}\n\nfunc (x *OAuth2Config) GetUserInfoUrl() string {\n\tif x != nil {\n\t\treturn x.UserInfoUrl\n\t}\n\treturn \"\"\n}\n\nfunc (x *OAuth2Config) GetScopes() []string {\n\tif x != nil {\n\t\treturn x.Scopes\n\t}\n\treturn nil\n}\n\nfunc (x *OAuth2Config) GetFieldMapping() *FieldMapping {\n\tif x != nil {\n\t\treturn x.FieldMapping\n\t}\n\treturn nil\n}\n\ntype ListIdentityProvidersRequest struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *ListIdentityProvidersRequest) Reset() {\n\t*x = ListIdentityProvidersRequest{}\n\tmi := &file_api_v1_idp_service_proto_msgTypes[4]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *ListIdentityProvidersRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*ListIdentityProvidersRequest) ProtoMessage() {}\n\nfunc (x *ListIdentityProvidersRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_idp_service_proto_msgTypes[4]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use ListIdentityProvidersRequest.ProtoReflect.Descriptor instead.\nfunc (*ListIdentityProvidersRequest) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_idp_service_proto_rawDescGZIP(), []int{4}\n}\n\ntype ListIdentityProvidersResponse struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// The list of identity providers.\n\tIdentityProviders []*IdentityProvider `protobuf:\"bytes,1,rep,name=identity_providers,json=identityProviders,proto3\" json:\"identity_providers,omitempty\"`\n\tunknownFields     protoimpl.UnknownFields\n\tsizeCache         protoimpl.SizeCache\n}\n\nfunc (x *ListIdentityProvidersResponse) Reset() {\n\t*x = ListIdentityProvidersResponse{}\n\tmi := &file_api_v1_idp_service_proto_msgTypes[5]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *ListIdentityProvidersResponse) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*ListIdentityProvidersResponse) ProtoMessage() {}\n\nfunc (x *ListIdentityProvidersResponse) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_idp_service_proto_msgTypes[5]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use ListIdentityProvidersResponse.ProtoReflect.Descriptor instead.\nfunc (*ListIdentityProvidersResponse) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_idp_service_proto_rawDescGZIP(), []int{5}\n}\n\nfunc (x *ListIdentityProvidersResponse) GetIdentityProviders() []*IdentityProvider {\n\tif x != nil {\n\t\treturn x.IdentityProviders\n\t}\n\treturn nil\n}\n\ntype GetIdentityProviderRequest struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// Required. The resource name of the identity provider to get.\n\t// Format: identity-providers/{idp}\n\tName          string `protobuf:\"bytes,1,opt,name=name,proto3\" json:\"name,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *GetIdentityProviderRequest) Reset() {\n\t*x = GetIdentityProviderRequest{}\n\tmi := &file_api_v1_idp_service_proto_msgTypes[6]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *GetIdentityProviderRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*GetIdentityProviderRequest) ProtoMessage() {}\n\nfunc (x *GetIdentityProviderRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_idp_service_proto_msgTypes[6]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use GetIdentityProviderRequest.ProtoReflect.Descriptor instead.\nfunc (*GetIdentityProviderRequest) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_idp_service_proto_rawDescGZIP(), []int{6}\n}\n\nfunc (x *GetIdentityProviderRequest) GetName() string {\n\tif x != nil {\n\t\treturn x.Name\n\t}\n\treturn \"\"\n}\n\ntype CreateIdentityProviderRequest struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// Required. The identity provider to create.\n\tIdentityProvider *IdentityProvider `protobuf:\"bytes,1,opt,name=identity_provider,json=identityProvider,proto3\" json:\"identity_provider,omitempty\"`\n\t// Optional. The ID to use for the identity provider, which will become the final component of the resource name.\n\t// If not provided, the system will generate one.\n\tIdentityProviderId string `protobuf:\"bytes,2,opt,name=identity_provider_id,json=identityProviderId,proto3\" json:\"identity_provider_id,omitempty\"`\n\tunknownFields      protoimpl.UnknownFields\n\tsizeCache          protoimpl.SizeCache\n}\n\nfunc (x *CreateIdentityProviderRequest) Reset() {\n\t*x = CreateIdentityProviderRequest{}\n\tmi := &file_api_v1_idp_service_proto_msgTypes[7]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *CreateIdentityProviderRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*CreateIdentityProviderRequest) ProtoMessage() {}\n\nfunc (x *CreateIdentityProviderRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_idp_service_proto_msgTypes[7]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use CreateIdentityProviderRequest.ProtoReflect.Descriptor instead.\nfunc (*CreateIdentityProviderRequest) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_idp_service_proto_rawDescGZIP(), []int{7}\n}\n\nfunc (x *CreateIdentityProviderRequest) GetIdentityProvider() *IdentityProvider {\n\tif x != nil {\n\t\treturn x.IdentityProvider\n\t}\n\treturn nil\n}\n\nfunc (x *CreateIdentityProviderRequest) GetIdentityProviderId() string {\n\tif x != nil {\n\t\treturn x.IdentityProviderId\n\t}\n\treturn \"\"\n}\n\ntype UpdateIdentityProviderRequest struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// Required. The identity provider to update.\n\tIdentityProvider *IdentityProvider `protobuf:\"bytes,1,opt,name=identity_provider,json=identityProvider,proto3\" json:\"identity_provider,omitempty\"`\n\t// Required. The update mask applies to the resource. Only the top level fields of\n\t// IdentityProvider are supported.\n\tUpdateMask    *fieldmaskpb.FieldMask `protobuf:\"bytes,2,opt,name=update_mask,json=updateMask,proto3\" json:\"update_mask,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *UpdateIdentityProviderRequest) Reset() {\n\t*x = UpdateIdentityProviderRequest{}\n\tmi := &file_api_v1_idp_service_proto_msgTypes[8]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *UpdateIdentityProviderRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*UpdateIdentityProviderRequest) ProtoMessage() {}\n\nfunc (x *UpdateIdentityProviderRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_idp_service_proto_msgTypes[8]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use UpdateIdentityProviderRequest.ProtoReflect.Descriptor instead.\nfunc (*UpdateIdentityProviderRequest) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_idp_service_proto_rawDescGZIP(), []int{8}\n}\n\nfunc (x *UpdateIdentityProviderRequest) GetIdentityProvider() *IdentityProvider {\n\tif x != nil {\n\t\treturn x.IdentityProvider\n\t}\n\treturn nil\n}\n\nfunc (x *UpdateIdentityProviderRequest) GetUpdateMask() *fieldmaskpb.FieldMask {\n\tif x != nil {\n\t\treturn x.UpdateMask\n\t}\n\treturn nil\n}\n\ntype DeleteIdentityProviderRequest struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// Required. The resource name of the identity provider to delete.\n\t// Format: identity-providers/{idp}\n\tName          string `protobuf:\"bytes,1,opt,name=name,proto3\" json:\"name,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *DeleteIdentityProviderRequest) Reset() {\n\t*x = DeleteIdentityProviderRequest{}\n\tmi := &file_api_v1_idp_service_proto_msgTypes[9]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *DeleteIdentityProviderRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*DeleteIdentityProviderRequest) ProtoMessage() {}\n\nfunc (x *DeleteIdentityProviderRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_idp_service_proto_msgTypes[9]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use DeleteIdentityProviderRequest.ProtoReflect.Descriptor instead.\nfunc (*DeleteIdentityProviderRequest) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_idp_service_proto_rawDescGZIP(), []int{9}\n}\n\nfunc (x *DeleteIdentityProviderRequest) GetName() string {\n\tif x != nil {\n\t\treturn x.Name\n\t}\n\treturn \"\"\n}\n\nvar File_api_v1_idp_service_proto protoreflect.FileDescriptor\n\nconst file_api_v1_idp_service_proto_rawDesc = \"\" +\n\t\"\\n\" +\n\t\"\\x18api/v1/idp_service.proto\\x12\\fmemos.api.v1\\x1a\\x1cgoogle/api/annotations.proto\\x1a\\x17google/api/client.proto\\x1a\\x1fgoogle/api/field_behavior.proto\\x1a\\x19google/api/resource.proto\\x1a\\x1bgoogle/protobuf/empty.proto\\x1a google/protobuf/field_mask.proto\\\"\\x8c\\x03\\n\" +\n\t\"\\x10IdentityProvider\\x12\\x17\\n\" +\n\t\"\\x04name\\x18\\x01 \\x01(\\tB\\x03\\xe0A\\bR\\x04name\\x12<\\n\" +\n\t\"\\x04type\\x18\\x02 \\x01(\\x0e2#.memos.api.v1.IdentityProvider.TypeB\\x03\\xe0A\\x02R\\x04type\\x12\\x19\\n\" +\n\t\"\\x05title\\x18\\x03 \\x01(\\tB\\x03\\xe0A\\x02R\\x05title\\x120\\n\" +\n\t\"\\x11identifier_filter\\x18\\x04 \\x01(\\tB\\x03\\xe0A\\x01R\\x10identifierFilter\\x12A\\n\" +\n\t\"\\x06config\\x18\\x05 \\x01(\\v2$.memos.api.v1.IdentityProviderConfigB\\x03\\xe0A\\x02R\\x06config\\\"(\\n\" +\n\t\"\\x04Type\\x12\\x14\\n\" +\n\t\"\\x10TYPE_UNSPECIFIED\\x10\\x00\\x12\\n\" +\n\t\"\\n\" +\n\t\"\\x06OAUTH2\\x10\\x01:g\\xeaAd\\n\" +\n\t\"\\x1dmemos.api.v1/IdentityProvider\\x12\\x18identity-providers/{idp}\\x1a\\x04name*\\x11identityProviders2\\x10identityProvider\\\"e\\n\" +\n\t\"\\x16IdentityProviderConfig\\x12A\\n\" +\n\t\"\\roauth2_config\\x18\\x01 \\x01(\\v2\\x1a.memos.api.v1.OAuth2ConfigH\\x00R\\foauth2ConfigB\\b\\n\" +\n\t\"\\x06config\\\"\\x86\\x01\\n\" +\n\t\"\\fFieldMapping\\x12\\x1e\\n\" +\n\t\"\\n\" +\n\t\"identifier\\x18\\x01 \\x01(\\tR\\n\" +\n\t\"identifier\\x12!\\n\" +\n\t\"\\fdisplay_name\\x18\\x02 \\x01(\\tR\\vdisplayName\\x12\\x14\\n\" +\n\t\"\\x05email\\x18\\x03 \\x01(\\tR\\x05email\\x12\\x1d\\n\" +\n\t\"\\n\" +\n\t\"avatar_url\\x18\\x04 \\x01(\\tR\\tavatarUrl\\\"\\x85\\x02\\n\" +\n\t\"\\fOAuth2Config\\x12\\x1b\\n\" +\n\t\"\\tclient_id\\x18\\x01 \\x01(\\tR\\bclientId\\x12#\\n\" +\n\t\"\\rclient_secret\\x18\\x02 \\x01(\\tR\\fclientSecret\\x12\\x19\\n\" +\n\t\"\\bauth_url\\x18\\x03 \\x01(\\tR\\aauthUrl\\x12\\x1b\\n\" +\n\t\"\\ttoken_url\\x18\\x04 \\x01(\\tR\\btokenUrl\\x12\\\"\\n\" +\n\t\"\\ruser_info_url\\x18\\x05 \\x01(\\tR\\vuserInfoUrl\\x12\\x16\\n\" +\n\t\"\\x06scopes\\x18\\x06 \\x03(\\tR\\x06scopes\\x12?\\n\" +\n\t\"\\rfield_mapping\\x18\\a \\x01(\\v2\\x1a.memos.api.v1.FieldMappingR\\ffieldMapping\\\"\\x1e\\n\" +\n\t\"\\x1cListIdentityProvidersRequest\\\"n\\n\" +\n\t\"\\x1dListIdentityProvidersResponse\\x12M\\n\" +\n\t\"\\x12identity_providers\\x18\\x01 \\x03(\\v2\\x1e.memos.api.v1.IdentityProviderR\\x11identityProviders\\\"W\\n\" +\n\t\"\\x1aGetIdentityProviderRequest\\x129\\n\" +\n\t\"\\x04name\\x18\\x01 \\x01(\\tB%\\xe0A\\x02\\xfaA\\x1f\\n\" +\n\t\"\\x1dmemos.api.v1/IdentityProviderR\\x04name\\\"\\xa8\\x01\\n\" +\n\t\"\\x1dCreateIdentityProviderRequest\\x12P\\n\" +\n\t\"\\x11identity_provider\\x18\\x01 \\x01(\\v2\\x1e.memos.api.v1.IdentityProviderB\\x03\\xe0A\\x02R\\x10identityProvider\\x125\\n\" +\n\t\"\\x14identity_provider_id\\x18\\x02 \\x01(\\tB\\x03\\xe0A\\x01R\\x12identityProviderId\\\"\\xb3\\x01\\n\" +\n\t\"\\x1dUpdateIdentityProviderRequest\\x12P\\n\" +\n\t\"\\x11identity_provider\\x18\\x01 \\x01(\\v2\\x1e.memos.api.v1.IdentityProviderB\\x03\\xe0A\\x02R\\x10identityProvider\\x12@\\n\" +\n\t\"\\vupdate_mask\\x18\\x02 \\x01(\\v2\\x1a.google.protobuf.FieldMaskB\\x03\\xe0A\\x02R\\n\" +\n\t\"updateMask\\\"Z\\n\" +\n\t\"\\x1dDeleteIdentityProviderRequest\\x129\\n\" +\n\t\"\\x04name\\x18\\x01 \\x01(\\tB%\\xe0A\\x02\\xfaA\\x1f\\n\" +\n\t\"\\x1dmemos.api.v1/IdentityProviderR\\x04name2\\xe7\\x06\\n\" +\n\t\"\\x17IdentityProviderService\\x12\\x94\\x01\\n\" +\n\t\"\\x15ListIdentityProviders\\x12*.memos.api.v1.ListIdentityProvidersRequest\\x1a+.memos.api.v1.ListIdentityProvidersResponse\\\"\\\"\\x82\\xd3\\xe4\\x93\\x02\\x1c\\x12\\x1a/api/v1/identity-providers\\x12\\x93\\x01\\n\" +\n\t\"\\x13GetIdentityProvider\\x12(.memos.api.v1.GetIdentityProviderRequest\\x1a\\x1e.memos.api.v1.IdentityProvider\\\"2\\xdaA\\x04name\\x82\\xd3\\xe4\\x93\\x02%\\x12#/api/v1/{name=identity-providers/*}\\x12\\xb0\\x01\\n\" +\n\t\"\\x16CreateIdentityProvider\\x12+.memos.api.v1.CreateIdentityProviderRequest\\x1a\\x1e.memos.api.v1.IdentityProvider\\\"I\\xdaA\\x11identity_provider\\x82\\xd3\\xe4\\x93\\x02/:\\x11identity_provider\\\"\\x1a/api/v1/identity-providers\\x12\\xd7\\x01\\n\" +\n\t\"\\x16UpdateIdentityProvider\\x12+.memos.api.v1.UpdateIdentityProviderRequest\\x1a\\x1e.memos.api.v1.IdentityProvider\\\"p\\xdaA\\x1didentity_provider,update_mask\\x82\\xd3\\xe4\\x93\\x02J:\\x11identity_provider25/api/v1/{identity_provider.name=identity-providers/*}\\x12\\x91\\x01\\n\" +\n\t\"\\x16DeleteIdentityProvider\\x12+.memos.api.v1.DeleteIdentityProviderRequest\\x1a\\x16.google.protobuf.Empty\\\"2\\xdaA\\x04name\\x82\\xd3\\xe4\\x93\\x02%*#/api/v1/{name=identity-providers/*}B\\xa7\\x01\\n\" +\n\t\"\\x10com.memos.api.v1B\\x0fIdpServiceProtoP\\x01Z0github.com/usememos/memos/proto/gen/api/v1;apiv1\\xa2\\x02\\x03MAX\\xaa\\x02\\fMemos.Api.V1\\xca\\x02\\fMemos\\\\Api\\\\V1\\xe2\\x02\\x18Memos\\\\Api\\\\V1\\\\GPBMetadata\\xea\\x02\\x0eMemos::Api::V1b\\x06proto3\"\n\nvar (\n\tfile_api_v1_idp_service_proto_rawDescOnce sync.Once\n\tfile_api_v1_idp_service_proto_rawDescData []byte\n)\n\nfunc file_api_v1_idp_service_proto_rawDescGZIP() []byte {\n\tfile_api_v1_idp_service_proto_rawDescOnce.Do(func() {\n\t\tfile_api_v1_idp_service_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_api_v1_idp_service_proto_rawDesc), len(file_api_v1_idp_service_proto_rawDesc)))\n\t})\n\treturn file_api_v1_idp_service_proto_rawDescData\n}\n\nvar file_api_v1_idp_service_proto_enumTypes = make([]protoimpl.EnumInfo, 1)\nvar file_api_v1_idp_service_proto_msgTypes = make([]protoimpl.MessageInfo, 10)\nvar file_api_v1_idp_service_proto_goTypes = []any{\n\t(IdentityProvider_Type)(0),            // 0: memos.api.v1.IdentityProvider.Type\n\t(*IdentityProvider)(nil),              // 1: memos.api.v1.IdentityProvider\n\t(*IdentityProviderConfig)(nil),        // 2: memos.api.v1.IdentityProviderConfig\n\t(*FieldMapping)(nil),                  // 3: memos.api.v1.FieldMapping\n\t(*OAuth2Config)(nil),                  // 4: memos.api.v1.OAuth2Config\n\t(*ListIdentityProvidersRequest)(nil),  // 5: memos.api.v1.ListIdentityProvidersRequest\n\t(*ListIdentityProvidersResponse)(nil), // 6: memos.api.v1.ListIdentityProvidersResponse\n\t(*GetIdentityProviderRequest)(nil),    // 7: memos.api.v1.GetIdentityProviderRequest\n\t(*CreateIdentityProviderRequest)(nil), // 8: memos.api.v1.CreateIdentityProviderRequest\n\t(*UpdateIdentityProviderRequest)(nil), // 9: memos.api.v1.UpdateIdentityProviderRequest\n\t(*DeleteIdentityProviderRequest)(nil), // 10: memos.api.v1.DeleteIdentityProviderRequest\n\t(*fieldmaskpb.FieldMask)(nil),         // 11: google.protobuf.FieldMask\n\t(*emptypb.Empty)(nil),                 // 12: google.protobuf.Empty\n}\nvar file_api_v1_idp_service_proto_depIdxs = []int32{\n\t0,  // 0: memos.api.v1.IdentityProvider.type:type_name -> memos.api.v1.IdentityProvider.Type\n\t2,  // 1: memos.api.v1.IdentityProvider.config:type_name -> memos.api.v1.IdentityProviderConfig\n\t4,  // 2: memos.api.v1.IdentityProviderConfig.oauth2_config:type_name -> memos.api.v1.OAuth2Config\n\t3,  // 3: memos.api.v1.OAuth2Config.field_mapping:type_name -> memos.api.v1.FieldMapping\n\t1,  // 4: memos.api.v1.ListIdentityProvidersResponse.identity_providers:type_name -> memos.api.v1.IdentityProvider\n\t1,  // 5: memos.api.v1.CreateIdentityProviderRequest.identity_provider:type_name -> memos.api.v1.IdentityProvider\n\t1,  // 6: memos.api.v1.UpdateIdentityProviderRequest.identity_provider:type_name -> memos.api.v1.IdentityProvider\n\t11, // 7: memos.api.v1.UpdateIdentityProviderRequest.update_mask:type_name -> google.protobuf.FieldMask\n\t5,  // 8: memos.api.v1.IdentityProviderService.ListIdentityProviders:input_type -> memos.api.v1.ListIdentityProvidersRequest\n\t7,  // 9: memos.api.v1.IdentityProviderService.GetIdentityProvider:input_type -> memos.api.v1.GetIdentityProviderRequest\n\t8,  // 10: memos.api.v1.IdentityProviderService.CreateIdentityProvider:input_type -> memos.api.v1.CreateIdentityProviderRequest\n\t9,  // 11: memos.api.v1.IdentityProviderService.UpdateIdentityProvider:input_type -> memos.api.v1.UpdateIdentityProviderRequest\n\t10, // 12: memos.api.v1.IdentityProviderService.DeleteIdentityProvider:input_type -> memos.api.v1.DeleteIdentityProviderRequest\n\t6,  // 13: memos.api.v1.IdentityProviderService.ListIdentityProviders:output_type -> memos.api.v1.ListIdentityProvidersResponse\n\t1,  // 14: memos.api.v1.IdentityProviderService.GetIdentityProvider:output_type -> memos.api.v1.IdentityProvider\n\t1,  // 15: memos.api.v1.IdentityProviderService.CreateIdentityProvider:output_type -> memos.api.v1.IdentityProvider\n\t1,  // 16: memos.api.v1.IdentityProviderService.UpdateIdentityProvider:output_type -> memos.api.v1.IdentityProvider\n\t12, // 17: memos.api.v1.IdentityProviderService.DeleteIdentityProvider:output_type -> google.protobuf.Empty\n\t13, // [13:18] is the sub-list for method output_type\n\t8,  // [8:13] is the sub-list for method input_type\n\t8,  // [8:8] is the sub-list for extension type_name\n\t8,  // [8:8] is the sub-list for extension extendee\n\t0,  // [0:8] is the sub-list for field type_name\n}\n\nfunc init() { file_api_v1_idp_service_proto_init() }\nfunc file_api_v1_idp_service_proto_init() {\n\tif File_api_v1_idp_service_proto != nil {\n\t\treturn\n\t}\n\tfile_api_v1_idp_service_proto_msgTypes[1].OneofWrappers = []any{\n\t\t(*IdentityProviderConfig_Oauth2Config)(nil),\n\t}\n\ttype x struct{}\n\tout := protoimpl.TypeBuilder{\n\t\tFile: protoimpl.DescBuilder{\n\t\t\tGoPackagePath: reflect.TypeOf(x{}).PkgPath(),\n\t\t\tRawDescriptor: unsafe.Slice(unsafe.StringData(file_api_v1_idp_service_proto_rawDesc), len(file_api_v1_idp_service_proto_rawDesc)),\n\t\t\tNumEnums:      1,\n\t\t\tNumMessages:   10,\n\t\t\tNumExtensions: 0,\n\t\t\tNumServices:   1,\n\t\t},\n\t\tGoTypes:           file_api_v1_idp_service_proto_goTypes,\n\t\tDependencyIndexes: file_api_v1_idp_service_proto_depIdxs,\n\t\tEnumInfos:         file_api_v1_idp_service_proto_enumTypes,\n\t\tMessageInfos:      file_api_v1_idp_service_proto_msgTypes,\n\t}.Build()\n\tFile_api_v1_idp_service_proto = out.File\n\tfile_api_v1_idp_service_proto_goTypes = nil\n\tfile_api_v1_idp_service_proto_depIdxs = nil\n}\n"
  },
  {
    "path": "proto/gen/api/v1/idp_service.pb.gw.go",
    "content": "// Code generated by protoc-gen-grpc-gateway. DO NOT EDIT.\n// source: api/v1/idp_service.proto\n\n/*\nPackage apiv1 is a reverse proxy.\n\nIt translates gRPC into RESTful JSON APIs.\n*/\npackage apiv1\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"io\"\n\t\"net/http\"\n\n\t\"github.com/grpc-ecosystem/grpc-gateway/v2/runtime\"\n\t\"github.com/grpc-ecosystem/grpc-gateway/v2/utilities\"\n\t\"google.golang.org/grpc\"\n\t\"google.golang.org/grpc/codes\"\n\t\"google.golang.org/grpc/grpclog\"\n\t\"google.golang.org/grpc/metadata\"\n\t\"google.golang.org/grpc/status\"\n\t\"google.golang.org/protobuf/proto\"\n)\n\n// Suppress \"imported and not used\" errors\nvar (\n\t_ codes.Code\n\t_ io.Reader\n\t_ status.Status\n\t_ = errors.New\n\t_ = runtime.String\n\t_ = utilities.NewDoubleArray\n\t_ = metadata.Join\n)\n\nfunc request_IdentityProviderService_ListIdentityProviders_0(ctx context.Context, marshaler runtime.Marshaler, client IdentityProviderServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq ListIdentityProvidersRequest\n\t\tmetadata runtime.ServerMetadata\n\t)\n\tif req.Body != nil {\n\t\t_, _ = io.Copy(io.Discard, req.Body)\n\t}\n\tmsg, err := client.ListIdentityProviders(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))\n\treturn msg, metadata, err\n}\n\nfunc local_request_IdentityProviderService_ListIdentityProviders_0(ctx context.Context, marshaler runtime.Marshaler, server IdentityProviderServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq ListIdentityProvidersRequest\n\t\tmetadata runtime.ServerMetadata\n\t)\n\tmsg, err := server.ListIdentityProviders(ctx, &protoReq)\n\treturn msg, metadata, err\n}\n\nfunc request_IdentityProviderService_GetIdentityProvider_0(ctx context.Context, marshaler runtime.Marshaler, client IdentityProviderServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq GetIdentityProviderRequest\n\t\tmetadata runtime.ServerMetadata\n\t\terr      error\n\t)\n\tif req.Body != nil {\n\t\t_, _ = io.Copy(io.Discard, req.Body)\n\t}\n\tval, ok := pathParams[\"name\"]\n\tif !ok {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"missing parameter %s\", \"name\")\n\t}\n\tprotoReq.Name, err = runtime.String(val)\n\tif err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"type mismatch, parameter: %s, error: %v\", \"name\", err)\n\t}\n\tmsg, err := client.GetIdentityProvider(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))\n\treturn msg, metadata, err\n}\n\nfunc local_request_IdentityProviderService_GetIdentityProvider_0(ctx context.Context, marshaler runtime.Marshaler, server IdentityProviderServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq GetIdentityProviderRequest\n\t\tmetadata runtime.ServerMetadata\n\t\terr      error\n\t)\n\tval, ok := pathParams[\"name\"]\n\tif !ok {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"missing parameter %s\", \"name\")\n\t}\n\tprotoReq.Name, err = runtime.String(val)\n\tif err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"type mismatch, parameter: %s, error: %v\", \"name\", err)\n\t}\n\tmsg, err := server.GetIdentityProvider(ctx, &protoReq)\n\treturn msg, metadata, err\n}\n\nvar filter_IdentityProviderService_CreateIdentityProvider_0 = &utilities.DoubleArray{Encoding: map[string]int{\"identity_provider\": 0}, Base: []int{1, 1, 0}, Check: []int{0, 1, 2}}\n\nfunc request_IdentityProviderService_CreateIdentityProvider_0(ctx context.Context, marshaler runtime.Marshaler, client IdentityProviderServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq CreateIdentityProviderRequest\n\t\tmetadata runtime.ServerMetadata\n\t)\n\tif err := marshaler.NewDecoder(req.Body).Decode(&protoReq.IdentityProvider); err != nil && !errors.Is(err, io.EOF) {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tif req.Body != nil {\n\t\t_, _ = io.Copy(io.Discard, req.Body)\n\t}\n\tif err := req.ParseForm(); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tif err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_IdentityProviderService_CreateIdentityProvider_0); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tmsg, err := client.CreateIdentityProvider(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))\n\treturn msg, metadata, err\n}\n\nfunc local_request_IdentityProviderService_CreateIdentityProvider_0(ctx context.Context, marshaler runtime.Marshaler, server IdentityProviderServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq CreateIdentityProviderRequest\n\t\tmetadata runtime.ServerMetadata\n\t)\n\tif err := marshaler.NewDecoder(req.Body).Decode(&protoReq.IdentityProvider); err != nil && !errors.Is(err, io.EOF) {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tif err := req.ParseForm(); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tif err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_IdentityProviderService_CreateIdentityProvider_0); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tmsg, err := server.CreateIdentityProvider(ctx, &protoReq)\n\treturn msg, metadata, err\n}\n\nvar filter_IdentityProviderService_UpdateIdentityProvider_0 = &utilities.DoubleArray{Encoding: map[string]int{\"identity_provider\": 0, \"name\": 1}, Base: []int{1, 2, 1, 0, 0}, Check: []int{0, 1, 2, 3, 2}}\n\nfunc request_IdentityProviderService_UpdateIdentityProvider_0(ctx context.Context, marshaler runtime.Marshaler, client IdentityProviderServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq UpdateIdentityProviderRequest\n\t\tmetadata runtime.ServerMetadata\n\t\terr      error\n\t)\n\tnewReader, berr := utilities.IOReaderFactory(req.Body)\n\tif berr != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", berr)\n\t}\n\tif err := marshaler.NewDecoder(newReader()).Decode(&protoReq.IdentityProvider); err != nil && !errors.Is(err, io.EOF) {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tif req.Body != nil {\n\t\t_, _ = io.Copy(io.Discard, req.Body)\n\t}\n\tif protoReq.UpdateMask == nil || len(protoReq.UpdateMask.GetPaths()) == 0 {\n\t\tif fieldMask, err := runtime.FieldMaskFromRequestBody(newReader(), protoReq.IdentityProvider); err != nil {\n\t\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t\t} else {\n\t\t\tprotoReq.UpdateMask = fieldMask\n\t\t}\n\t}\n\tval, ok := pathParams[\"identity_provider.name\"]\n\tif !ok {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"missing parameter %s\", \"identity_provider.name\")\n\t}\n\terr = runtime.PopulateFieldFromPath(&protoReq, \"identity_provider.name\", val)\n\tif err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"type mismatch, parameter: %s, error: %v\", \"identity_provider.name\", err)\n\t}\n\tif err := req.ParseForm(); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tif err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_IdentityProviderService_UpdateIdentityProvider_0); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tmsg, err := client.UpdateIdentityProvider(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))\n\treturn msg, metadata, err\n}\n\nfunc local_request_IdentityProviderService_UpdateIdentityProvider_0(ctx context.Context, marshaler runtime.Marshaler, server IdentityProviderServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq UpdateIdentityProviderRequest\n\t\tmetadata runtime.ServerMetadata\n\t\terr      error\n\t)\n\tnewReader, berr := utilities.IOReaderFactory(req.Body)\n\tif berr != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", berr)\n\t}\n\tif err := marshaler.NewDecoder(newReader()).Decode(&protoReq.IdentityProvider); err != nil && !errors.Is(err, io.EOF) {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tif protoReq.UpdateMask == nil || len(protoReq.UpdateMask.GetPaths()) == 0 {\n\t\tif fieldMask, err := runtime.FieldMaskFromRequestBody(newReader(), protoReq.IdentityProvider); err != nil {\n\t\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t\t} else {\n\t\t\tprotoReq.UpdateMask = fieldMask\n\t\t}\n\t}\n\tval, ok := pathParams[\"identity_provider.name\"]\n\tif !ok {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"missing parameter %s\", \"identity_provider.name\")\n\t}\n\terr = runtime.PopulateFieldFromPath(&protoReq, \"identity_provider.name\", val)\n\tif err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"type mismatch, parameter: %s, error: %v\", \"identity_provider.name\", err)\n\t}\n\tif err := req.ParseForm(); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tif err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_IdentityProviderService_UpdateIdentityProvider_0); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tmsg, err := server.UpdateIdentityProvider(ctx, &protoReq)\n\treturn msg, metadata, err\n}\n\nfunc request_IdentityProviderService_DeleteIdentityProvider_0(ctx context.Context, marshaler runtime.Marshaler, client IdentityProviderServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq DeleteIdentityProviderRequest\n\t\tmetadata runtime.ServerMetadata\n\t\terr      error\n\t)\n\tif req.Body != nil {\n\t\t_, _ = io.Copy(io.Discard, req.Body)\n\t}\n\tval, ok := pathParams[\"name\"]\n\tif !ok {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"missing parameter %s\", \"name\")\n\t}\n\tprotoReq.Name, err = runtime.String(val)\n\tif err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"type mismatch, parameter: %s, error: %v\", \"name\", err)\n\t}\n\tmsg, err := client.DeleteIdentityProvider(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))\n\treturn msg, metadata, err\n}\n\nfunc local_request_IdentityProviderService_DeleteIdentityProvider_0(ctx context.Context, marshaler runtime.Marshaler, server IdentityProviderServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq DeleteIdentityProviderRequest\n\t\tmetadata runtime.ServerMetadata\n\t\terr      error\n\t)\n\tval, ok := pathParams[\"name\"]\n\tif !ok {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"missing parameter %s\", \"name\")\n\t}\n\tprotoReq.Name, err = runtime.String(val)\n\tif err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"type mismatch, parameter: %s, error: %v\", \"name\", err)\n\t}\n\tmsg, err := server.DeleteIdentityProvider(ctx, &protoReq)\n\treturn msg, metadata, err\n}\n\n// RegisterIdentityProviderServiceHandlerServer registers the http handlers for service IdentityProviderService to \"mux\".\n// UnaryRPC     :call IdentityProviderServiceServer directly.\n// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906.\n// Note that using this registration option will cause many gRPC library features to stop working. Consider using RegisterIdentityProviderServiceHandlerFromEndpoint instead.\n// GRPC interceptors will not work for this type of registration. To use interceptors, you must use the \"runtime.WithMiddlewares\" option in the \"runtime.NewServeMux\" call.\nfunc RegisterIdentityProviderServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux, server IdentityProviderServiceServer) error {\n\tmux.Handle(http.MethodGet, pattern_IdentityProviderService_ListIdentityProviders_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tvar stream runtime.ServerTransportStream\n\t\tctx = grpc.NewContextWithServerTransportStream(ctx, &stream)\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, \"/memos.api.v1.IdentityProviderService/ListIdentityProviders\", runtime.WithHTTPPathPattern(\"/api/v1/identity-providers\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := local_request_IdentityProviderService_ListIdentityProviders_0(annotatedContext, inboundMarshaler, server, req, pathParams)\n\t\tmd.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_IdentityProviderService_ListIdentityProviders_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodGet, pattern_IdentityProviderService_GetIdentityProvider_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tvar stream runtime.ServerTransportStream\n\t\tctx = grpc.NewContextWithServerTransportStream(ctx, &stream)\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, \"/memos.api.v1.IdentityProviderService/GetIdentityProvider\", runtime.WithHTTPPathPattern(\"/api/v1/{name=identity-providers/*}\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := local_request_IdentityProviderService_GetIdentityProvider_0(annotatedContext, inboundMarshaler, server, req, pathParams)\n\t\tmd.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_IdentityProviderService_GetIdentityProvider_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodPost, pattern_IdentityProviderService_CreateIdentityProvider_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tvar stream runtime.ServerTransportStream\n\t\tctx = grpc.NewContextWithServerTransportStream(ctx, &stream)\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, \"/memos.api.v1.IdentityProviderService/CreateIdentityProvider\", runtime.WithHTTPPathPattern(\"/api/v1/identity-providers\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := local_request_IdentityProviderService_CreateIdentityProvider_0(annotatedContext, inboundMarshaler, server, req, pathParams)\n\t\tmd.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_IdentityProviderService_CreateIdentityProvider_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodPatch, pattern_IdentityProviderService_UpdateIdentityProvider_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tvar stream runtime.ServerTransportStream\n\t\tctx = grpc.NewContextWithServerTransportStream(ctx, &stream)\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, \"/memos.api.v1.IdentityProviderService/UpdateIdentityProvider\", runtime.WithHTTPPathPattern(\"/api/v1/{identity_provider.name=identity-providers/*}\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := local_request_IdentityProviderService_UpdateIdentityProvider_0(annotatedContext, inboundMarshaler, server, req, pathParams)\n\t\tmd.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_IdentityProviderService_UpdateIdentityProvider_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodDelete, pattern_IdentityProviderService_DeleteIdentityProvider_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tvar stream runtime.ServerTransportStream\n\t\tctx = grpc.NewContextWithServerTransportStream(ctx, &stream)\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, \"/memos.api.v1.IdentityProviderService/DeleteIdentityProvider\", runtime.WithHTTPPathPattern(\"/api/v1/{name=identity-providers/*}\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := local_request_IdentityProviderService_DeleteIdentityProvider_0(annotatedContext, inboundMarshaler, server, req, pathParams)\n\t\tmd.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_IdentityProviderService_DeleteIdentityProvider_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\n\treturn nil\n}\n\n// RegisterIdentityProviderServiceHandlerFromEndpoint is same as RegisterIdentityProviderServiceHandler but\n// automatically dials to \"endpoint\" and closes the connection when \"ctx\" gets done.\nfunc RegisterIdentityProviderServiceHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) {\n\tconn, err := grpc.NewClient(endpoint, opts...)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer func() {\n\t\tif err != nil {\n\t\t\tif cerr := conn.Close(); cerr != nil {\n\t\t\t\tgrpclog.Errorf(\"Failed to close conn to %s: %v\", endpoint, cerr)\n\t\t\t}\n\t\t\treturn\n\t\t}\n\t\tgo func() {\n\t\t\t<-ctx.Done()\n\t\t\tif cerr := conn.Close(); cerr != nil {\n\t\t\t\tgrpclog.Errorf(\"Failed to close conn to %s: %v\", endpoint, cerr)\n\t\t\t}\n\t\t}()\n\t}()\n\treturn RegisterIdentityProviderServiceHandler(ctx, mux, conn)\n}\n\n// RegisterIdentityProviderServiceHandler registers the http handlers for service IdentityProviderService to \"mux\".\n// The handlers forward requests to the grpc endpoint over \"conn\".\nfunc RegisterIdentityProviderServiceHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error {\n\treturn RegisterIdentityProviderServiceHandlerClient(ctx, mux, NewIdentityProviderServiceClient(conn))\n}\n\n// RegisterIdentityProviderServiceHandlerClient registers the http handlers for service IdentityProviderService\n// to \"mux\". The handlers forward requests to the grpc endpoint over the given implementation of \"IdentityProviderServiceClient\".\n// Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in \"IdentityProviderServiceClient\"\n// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in\n// \"IdentityProviderServiceClient\" to call the correct interceptors. This client ignores the HTTP middlewares.\nfunc RegisterIdentityProviderServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux, client IdentityProviderServiceClient) error {\n\tmux.Handle(http.MethodGet, pattern_IdentityProviderService_ListIdentityProviders_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateContext(ctx, mux, req, \"/memos.api.v1.IdentityProviderService/ListIdentityProviders\", runtime.WithHTTPPathPattern(\"/api/v1/identity-providers\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := request_IdentityProviderService_ListIdentityProviders_0(annotatedContext, inboundMarshaler, client, req, pathParams)\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_IdentityProviderService_ListIdentityProviders_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodGet, pattern_IdentityProviderService_GetIdentityProvider_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateContext(ctx, mux, req, \"/memos.api.v1.IdentityProviderService/GetIdentityProvider\", runtime.WithHTTPPathPattern(\"/api/v1/{name=identity-providers/*}\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := request_IdentityProviderService_GetIdentityProvider_0(annotatedContext, inboundMarshaler, client, req, pathParams)\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_IdentityProviderService_GetIdentityProvider_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodPost, pattern_IdentityProviderService_CreateIdentityProvider_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateContext(ctx, mux, req, \"/memos.api.v1.IdentityProviderService/CreateIdentityProvider\", runtime.WithHTTPPathPattern(\"/api/v1/identity-providers\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := request_IdentityProviderService_CreateIdentityProvider_0(annotatedContext, inboundMarshaler, client, req, pathParams)\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_IdentityProviderService_CreateIdentityProvider_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodPatch, pattern_IdentityProviderService_UpdateIdentityProvider_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateContext(ctx, mux, req, \"/memos.api.v1.IdentityProviderService/UpdateIdentityProvider\", runtime.WithHTTPPathPattern(\"/api/v1/{identity_provider.name=identity-providers/*}\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := request_IdentityProviderService_UpdateIdentityProvider_0(annotatedContext, inboundMarshaler, client, req, pathParams)\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_IdentityProviderService_UpdateIdentityProvider_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodDelete, pattern_IdentityProviderService_DeleteIdentityProvider_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateContext(ctx, mux, req, \"/memos.api.v1.IdentityProviderService/DeleteIdentityProvider\", runtime.WithHTTPPathPattern(\"/api/v1/{name=identity-providers/*}\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := request_IdentityProviderService_DeleteIdentityProvider_0(annotatedContext, inboundMarshaler, client, req, pathParams)\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_IdentityProviderService_DeleteIdentityProvider_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\treturn nil\n}\n\nvar (\n\tpattern_IdentityProviderService_ListIdentityProviders_0  = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{\"api\", \"v1\", \"identity-providers\"}, \"\"))\n\tpattern_IdentityProviderService_GetIdentityProvider_0    = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3}, []string{\"api\", \"v1\", \"identity-providers\", \"name\"}, \"\"))\n\tpattern_IdentityProviderService_CreateIdentityProvider_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{\"api\", \"v1\", \"identity-providers\"}, \"\"))\n\tpattern_IdentityProviderService_UpdateIdentityProvider_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3}, []string{\"api\", \"v1\", \"identity-providers\", \"identity_provider.name\"}, \"\"))\n\tpattern_IdentityProviderService_DeleteIdentityProvider_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3}, []string{\"api\", \"v1\", \"identity-providers\", \"name\"}, \"\"))\n)\n\nvar (\n\tforward_IdentityProviderService_ListIdentityProviders_0  = runtime.ForwardResponseMessage\n\tforward_IdentityProviderService_GetIdentityProvider_0    = runtime.ForwardResponseMessage\n\tforward_IdentityProviderService_CreateIdentityProvider_0 = runtime.ForwardResponseMessage\n\tforward_IdentityProviderService_UpdateIdentityProvider_0 = runtime.ForwardResponseMessage\n\tforward_IdentityProviderService_DeleteIdentityProvider_0 = runtime.ForwardResponseMessage\n)\n"
  },
  {
    "path": "proto/gen/api/v1/idp_service_grpc.pb.go",
    "content": "// Code generated by protoc-gen-go-grpc. DO NOT EDIT.\n// versions:\n// - protoc-gen-go-grpc v1.6.1\n// - protoc             (unknown)\n// source: api/v1/idp_service.proto\n\npackage apiv1\n\nimport (\n\tcontext \"context\"\n\tgrpc \"google.golang.org/grpc\"\n\tcodes \"google.golang.org/grpc/codes\"\n\tstatus \"google.golang.org/grpc/status\"\n\temptypb \"google.golang.org/protobuf/types/known/emptypb\"\n)\n\n// This is a compile-time assertion to ensure that this generated file\n// is compatible with the grpc package it is being compiled against.\n// Requires gRPC-Go v1.64.0 or later.\nconst _ = grpc.SupportPackageIsVersion9\n\nconst (\n\tIdentityProviderService_ListIdentityProviders_FullMethodName  = \"/memos.api.v1.IdentityProviderService/ListIdentityProviders\"\n\tIdentityProviderService_GetIdentityProvider_FullMethodName    = \"/memos.api.v1.IdentityProviderService/GetIdentityProvider\"\n\tIdentityProviderService_CreateIdentityProvider_FullMethodName = \"/memos.api.v1.IdentityProviderService/CreateIdentityProvider\"\n\tIdentityProviderService_UpdateIdentityProvider_FullMethodName = \"/memos.api.v1.IdentityProviderService/UpdateIdentityProvider\"\n\tIdentityProviderService_DeleteIdentityProvider_FullMethodName = \"/memos.api.v1.IdentityProviderService/DeleteIdentityProvider\"\n)\n\n// IdentityProviderServiceClient is the client API for IdentityProviderService service.\n//\n// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.\ntype IdentityProviderServiceClient interface {\n\t// ListIdentityProviders lists identity providers.\n\tListIdentityProviders(ctx context.Context, in *ListIdentityProvidersRequest, opts ...grpc.CallOption) (*ListIdentityProvidersResponse, error)\n\t// GetIdentityProvider gets an identity provider.\n\tGetIdentityProvider(ctx context.Context, in *GetIdentityProviderRequest, opts ...grpc.CallOption) (*IdentityProvider, error)\n\t// CreateIdentityProvider creates an identity provider.\n\tCreateIdentityProvider(ctx context.Context, in *CreateIdentityProviderRequest, opts ...grpc.CallOption) (*IdentityProvider, error)\n\t// UpdateIdentityProvider updates an identity provider.\n\tUpdateIdentityProvider(ctx context.Context, in *UpdateIdentityProviderRequest, opts ...grpc.CallOption) (*IdentityProvider, error)\n\t// DeleteIdentityProvider deletes an identity provider.\n\tDeleteIdentityProvider(ctx context.Context, in *DeleteIdentityProviderRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)\n}\n\ntype identityProviderServiceClient struct {\n\tcc grpc.ClientConnInterface\n}\n\nfunc NewIdentityProviderServiceClient(cc grpc.ClientConnInterface) IdentityProviderServiceClient {\n\treturn &identityProviderServiceClient{cc}\n}\n\nfunc (c *identityProviderServiceClient) ListIdentityProviders(ctx context.Context, in *ListIdentityProvidersRequest, opts ...grpc.CallOption) (*ListIdentityProvidersResponse, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(ListIdentityProvidersResponse)\n\terr := c.cc.Invoke(ctx, IdentityProviderService_ListIdentityProviders_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *identityProviderServiceClient) GetIdentityProvider(ctx context.Context, in *GetIdentityProviderRequest, opts ...grpc.CallOption) (*IdentityProvider, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(IdentityProvider)\n\terr := c.cc.Invoke(ctx, IdentityProviderService_GetIdentityProvider_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *identityProviderServiceClient) CreateIdentityProvider(ctx context.Context, in *CreateIdentityProviderRequest, opts ...grpc.CallOption) (*IdentityProvider, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(IdentityProvider)\n\terr := c.cc.Invoke(ctx, IdentityProviderService_CreateIdentityProvider_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *identityProviderServiceClient) UpdateIdentityProvider(ctx context.Context, in *UpdateIdentityProviderRequest, opts ...grpc.CallOption) (*IdentityProvider, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(IdentityProvider)\n\terr := c.cc.Invoke(ctx, IdentityProviderService_UpdateIdentityProvider_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *identityProviderServiceClient) DeleteIdentityProvider(ctx context.Context, in *DeleteIdentityProviderRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(emptypb.Empty)\n\terr := c.cc.Invoke(ctx, IdentityProviderService_DeleteIdentityProvider_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\n// IdentityProviderServiceServer is the server API for IdentityProviderService service.\n// All implementations must embed UnimplementedIdentityProviderServiceServer\n// for forward compatibility.\ntype IdentityProviderServiceServer interface {\n\t// ListIdentityProviders lists identity providers.\n\tListIdentityProviders(context.Context, *ListIdentityProvidersRequest) (*ListIdentityProvidersResponse, error)\n\t// GetIdentityProvider gets an identity provider.\n\tGetIdentityProvider(context.Context, *GetIdentityProviderRequest) (*IdentityProvider, error)\n\t// CreateIdentityProvider creates an identity provider.\n\tCreateIdentityProvider(context.Context, *CreateIdentityProviderRequest) (*IdentityProvider, error)\n\t// UpdateIdentityProvider updates an identity provider.\n\tUpdateIdentityProvider(context.Context, *UpdateIdentityProviderRequest) (*IdentityProvider, error)\n\t// DeleteIdentityProvider deletes an identity provider.\n\tDeleteIdentityProvider(context.Context, *DeleteIdentityProviderRequest) (*emptypb.Empty, error)\n\tmustEmbedUnimplementedIdentityProviderServiceServer()\n}\n\n// UnimplementedIdentityProviderServiceServer must be embedded to have\n// forward compatible implementations.\n//\n// NOTE: this should be embedded by value instead of pointer to avoid a nil\n// pointer dereference when methods are called.\ntype UnimplementedIdentityProviderServiceServer struct{}\n\nfunc (UnimplementedIdentityProviderServiceServer) ListIdentityProviders(context.Context, *ListIdentityProvidersRequest) (*ListIdentityProvidersResponse, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method ListIdentityProviders not implemented\")\n}\nfunc (UnimplementedIdentityProviderServiceServer) GetIdentityProvider(context.Context, *GetIdentityProviderRequest) (*IdentityProvider, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method GetIdentityProvider not implemented\")\n}\nfunc (UnimplementedIdentityProviderServiceServer) CreateIdentityProvider(context.Context, *CreateIdentityProviderRequest) (*IdentityProvider, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method CreateIdentityProvider not implemented\")\n}\nfunc (UnimplementedIdentityProviderServiceServer) UpdateIdentityProvider(context.Context, *UpdateIdentityProviderRequest) (*IdentityProvider, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method UpdateIdentityProvider not implemented\")\n}\nfunc (UnimplementedIdentityProviderServiceServer) DeleteIdentityProvider(context.Context, *DeleteIdentityProviderRequest) (*emptypb.Empty, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method DeleteIdentityProvider not implemented\")\n}\nfunc (UnimplementedIdentityProviderServiceServer) mustEmbedUnimplementedIdentityProviderServiceServer() {\n}\nfunc (UnimplementedIdentityProviderServiceServer) testEmbeddedByValue() {}\n\n// UnsafeIdentityProviderServiceServer may be embedded to opt out of forward compatibility for this service.\n// Use of this interface is not recommended, as added methods to IdentityProviderServiceServer will\n// result in compilation errors.\ntype UnsafeIdentityProviderServiceServer interface {\n\tmustEmbedUnimplementedIdentityProviderServiceServer()\n}\n\nfunc RegisterIdentityProviderServiceServer(s grpc.ServiceRegistrar, srv IdentityProviderServiceServer) {\n\t// If the following call panics, it indicates UnimplementedIdentityProviderServiceServer was\n\t// embedded by pointer and is nil.  This will cause panics if an\n\t// unimplemented method is ever invoked, so we test this at initialization\n\t// time to prevent it from happening at runtime later due to I/O.\n\tif t, ok := srv.(interface{ testEmbeddedByValue() }); ok {\n\t\tt.testEmbeddedByValue()\n\t}\n\ts.RegisterService(&IdentityProviderService_ServiceDesc, srv)\n}\n\nfunc _IdentityProviderService_ListIdentityProviders_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(ListIdentityProvidersRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(IdentityProviderServiceServer).ListIdentityProviders(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: IdentityProviderService_ListIdentityProviders_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(IdentityProviderServiceServer).ListIdentityProviders(ctx, req.(*ListIdentityProvidersRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _IdentityProviderService_GetIdentityProvider_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(GetIdentityProviderRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(IdentityProviderServiceServer).GetIdentityProvider(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: IdentityProviderService_GetIdentityProvider_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(IdentityProviderServiceServer).GetIdentityProvider(ctx, req.(*GetIdentityProviderRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _IdentityProviderService_CreateIdentityProvider_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(CreateIdentityProviderRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(IdentityProviderServiceServer).CreateIdentityProvider(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: IdentityProviderService_CreateIdentityProvider_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(IdentityProviderServiceServer).CreateIdentityProvider(ctx, req.(*CreateIdentityProviderRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _IdentityProviderService_UpdateIdentityProvider_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(UpdateIdentityProviderRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(IdentityProviderServiceServer).UpdateIdentityProvider(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: IdentityProviderService_UpdateIdentityProvider_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(IdentityProviderServiceServer).UpdateIdentityProvider(ctx, req.(*UpdateIdentityProviderRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _IdentityProviderService_DeleteIdentityProvider_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(DeleteIdentityProviderRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(IdentityProviderServiceServer).DeleteIdentityProvider(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: IdentityProviderService_DeleteIdentityProvider_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(IdentityProviderServiceServer).DeleteIdentityProvider(ctx, req.(*DeleteIdentityProviderRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\n// IdentityProviderService_ServiceDesc is the grpc.ServiceDesc for IdentityProviderService service.\n// It's only intended for direct use with grpc.RegisterService,\n// and not to be introspected or modified (even as a copy)\nvar IdentityProviderService_ServiceDesc = grpc.ServiceDesc{\n\tServiceName: \"memos.api.v1.IdentityProviderService\",\n\tHandlerType: (*IdentityProviderServiceServer)(nil),\n\tMethods: []grpc.MethodDesc{\n\t\t{\n\t\t\tMethodName: \"ListIdentityProviders\",\n\t\t\tHandler:    _IdentityProviderService_ListIdentityProviders_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"GetIdentityProvider\",\n\t\t\tHandler:    _IdentityProviderService_GetIdentityProvider_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"CreateIdentityProvider\",\n\t\t\tHandler:    _IdentityProviderService_CreateIdentityProvider_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"UpdateIdentityProvider\",\n\t\t\tHandler:    _IdentityProviderService_UpdateIdentityProvider_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"DeleteIdentityProvider\",\n\t\t\tHandler:    _IdentityProviderService_DeleteIdentityProvider_Handler,\n\t\t},\n\t},\n\tStreams:  []grpc.StreamDesc{},\n\tMetadata: \"api/v1/idp_service.proto\",\n}\n"
  },
  {
    "path": "proto/gen/api/v1/instance_service.pb.go",
    "content": "// Code generated by protoc-gen-go. DO NOT EDIT.\n// versions:\n// \tprotoc-gen-go v1.36.11\n// \tprotoc        (unknown)\n// source: api/v1/instance_service.proto\n\npackage apiv1\n\nimport (\n\t_ \"google.golang.org/genproto/googleapis/api/annotations\"\n\tcolor \"google.golang.org/genproto/googleapis/type/color\"\n\tprotoreflect \"google.golang.org/protobuf/reflect/protoreflect\"\n\tprotoimpl \"google.golang.org/protobuf/runtime/protoimpl\"\n\tfieldmaskpb \"google.golang.org/protobuf/types/known/fieldmaskpb\"\n\treflect \"reflect\"\n\tsync \"sync\"\n\tunsafe \"unsafe\"\n)\n\nconst (\n\t// Verify that this generated code is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)\n\t// Verify that runtime/protoimpl is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)\n)\n\n// Enumeration of instance setting keys.\ntype InstanceSetting_Key int32\n\nconst (\n\tInstanceSetting_KEY_UNSPECIFIED InstanceSetting_Key = 0\n\t// GENERAL is the key for general settings.\n\tInstanceSetting_GENERAL InstanceSetting_Key = 1\n\t// STORAGE is the key for storage settings.\n\tInstanceSetting_STORAGE InstanceSetting_Key = 2\n\t// MEMO_RELATED is the key for memo related settings.\n\tInstanceSetting_MEMO_RELATED InstanceSetting_Key = 3\n\t// TAGS is the key for tag metadata.\n\tInstanceSetting_TAGS InstanceSetting_Key = 4\n\t// NOTIFICATION is the key for notification transport settings.\n\tInstanceSetting_NOTIFICATION InstanceSetting_Key = 5\n)\n\n// Enum value maps for InstanceSetting_Key.\nvar (\n\tInstanceSetting_Key_name = map[int32]string{\n\t\t0: \"KEY_UNSPECIFIED\",\n\t\t1: \"GENERAL\",\n\t\t2: \"STORAGE\",\n\t\t3: \"MEMO_RELATED\",\n\t\t4: \"TAGS\",\n\t\t5: \"NOTIFICATION\",\n\t}\n\tInstanceSetting_Key_value = map[string]int32{\n\t\t\"KEY_UNSPECIFIED\": 0,\n\t\t\"GENERAL\":         1,\n\t\t\"STORAGE\":         2,\n\t\t\"MEMO_RELATED\":    3,\n\t\t\"TAGS\":            4,\n\t\t\"NOTIFICATION\":    5,\n\t}\n)\n\nfunc (x InstanceSetting_Key) Enum() *InstanceSetting_Key {\n\tp := new(InstanceSetting_Key)\n\t*p = x\n\treturn p\n}\n\nfunc (x InstanceSetting_Key) String() string {\n\treturn protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))\n}\n\nfunc (InstanceSetting_Key) Descriptor() protoreflect.EnumDescriptor {\n\treturn file_api_v1_instance_service_proto_enumTypes[0].Descriptor()\n}\n\nfunc (InstanceSetting_Key) Type() protoreflect.EnumType {\n\treturn &file_api_v1_instance_service_proto_enumTypes[0]\n}\n\nfunc (x InstanceSetting_Key) Number() protoreflect.EnumNumber {\n\treturn protoreflect.EnumNumber(x)\n}\n\n// Deprecated: Use InstanceSetting_Key.Descriptor instead.\nfunc (InstanceSetting_Key) EnumDescriptor() ([]byte, []int) {\n\treturn file_api_v1_instance_service_proto_rawDescGZIP(), []int{2, 0}\n}\n\n// Storage type enumeration for different storage backends.\ntype InstanceSetting_StorageSetting_StorageType int32\n\nconst (\n\tInstanceSetting_StorageSetting_STORAGE_TYPE_UNSPECIFIED InstanceSetting_StorageSetting_StorageType = 0\n\t// DATABASE is the database storage type.\n\tInstanceSetting_StorageSetting_DATABASE InstanceSetting_StorageSetting_StorageType = 1\n\t// LOCAL is the local storage type.\n\tInstanceSetting_StorageSetting_LOCAL InstanceSetting_StorageSetting_StorageType = 2\n\t// S3 is the S3 storage type.\n\tInstanceSetting_StorageSetting_S3 InstanceSetting_StorageSetting_StorageType = 3\n)\n\n// Enum value maps for InstanceSetting_StorageSetting_StorageType.\nvar (\n\tInstanceSetting_StorageSetting_StorageType_name = map[int32]string{\n\t\t0: \"STORAGE_TYPE_UNSPECIFIED\",\n\t\t1: \"DATABASE\",\n\t\t2: \"LOCAL\",\n\t\t3: \"S3\",\n\t}\n\tInstanceSetting_StorageSetting_StorageType_value = map[string]int32{\n\t\t\"STORAGE_TYPE_UNSPECIFIED\": 0,\n\t\t\"DATABASE\":                 1,\n\t\t\"LOCAL\":                    2,\n\t\t\"S3\":                       3,\n\t}\n)\n\nfunc (x InstanceSetting_StorageSetting_StorageType) Enum() *InstanceSetting_StorageSetting_StorageType {\n\tp := new(InstanceSetting_StorageSetting_StorageType)\n\t*p = x\n\treturn p\n}\n\nfunc (x InstanceSetting_StorageSetting_StorageType) String() string {\n\treturn protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))\n}\n\nfunc (InstanceSetting_StorageSetting_StorageType) Descriptor() protoreflect.EnumDescriptor {\n\treturn file_api_v1_instance_service_proto_enumTypes[1].Descriptor()\n}\n\nfunc (InstanceSetting_StorageSetting_StorageType) Type() protoreflect.EnumType {\n\treturn &file_api_v1_instance_service_proto_enumTypes[1]\n}\n\nfunc (x InstanceSetting_StorageSetting_StorageType) Number() protoreflect.EnumNumber {\n\treturn protoreflect.EnumNumber(x)\n}\n\n// Deprecated: Use InstanceSetting_StorageSetting_StorageType.Descriptor instead.\nfunc (InstanceSetting_StorageSetting_StorageType) EnumDescriptor() ([]byte, []int) {\n\treturn file_api_v1_instance_service_proto_rawDescGZIP(), []int{2, 1, 0}\n}\n\n// Instance profile message containing basic instance information.\ntype InstanceProfile struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// Version is the current version of instance.\n\tVersion string `protobuf:\"bytes,2,opt,name=version,proto3\" json:\"version,omitempty\"`\n\t// Demo indicates if the instance is in demo mode.\n\tDemo bool `protobuf:\"varint,3,opt,name=demo,proto3\" json:\"demo,omitempty\"`\n\t// Instance URL is the URL of the instance.\n\tInstanceUrl string `protobuf:\"bytes,6,opt,name=instance_url,json=instanceUrl,proto3\" json:\"instance_url,omitempty\"`\n\t// The first administrator who set up this instance.\n\t// When null, instance requires initial setup (creating the first admin account).\n\tAdmin         *User `protobuf:\"bytes,7,opt,name=admin,proto3\" json:\"admin,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *InstanceProfile) Reset() {\n\t*x = InstanceProfile{}\n\tmi := &file_api_v1_instance_service_proto_msgTypes[0]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *InstanceProfile) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*InstanceProfile) ProtoMessage() {}\n\nfunc (x *InstanceProfile) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_instance_service_proto_msgTypes[0]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use InstanceProfile.ProtoReflect.Descriptor instead.\nfunc (*InstanceProfile) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_instance_service_proto_rawDescGZIP(), []int{0}\n}\n\nfunc (x *InstanceProfile) GetVersion() string {\n\tif x != nil {\n\t\treturn x.Version\n\t}\n\treturn \"\"\n}\n\nfunc (x *InstanceProfile) GetDemo() bool {\n\tif x != nil {\n\t\treturn x.Demo\n\t}\n\treturn false\n}\n\nfunc (x *InstanceProfile) GetInstanceUrl() string {\n\tif x != nil {\n\t\treturn x.InstanceUrl\n\t}\n\treturn \"\"\n}\n\nfunc (x *InstanceProfile) GetAdmin() *User {\n\tif x != nil {\n\t\treturn x.Admin\n\t}\n\treturn nil\n}\n\n// Request for instance profile.\ntype GetInstanceProfileRequest struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *GetInstanceProfileRequest) Reset() {\n\t*x = GetInstanceProfileRequest{}\n\tmi := &file_api_v1_instance_service_proto_msgTypes[1]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *GetInstanceProfileRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*GetInstanceProfileRequest) ProtoMessage() {}\n\nfunc (x *GetInstanceProfileRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_instance_service_proto_msgTypes[1]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use GetInstanceProfileRequest.ProtoReflect.Descriptor instead.\nfunc (*GetInstanceProfileRequest) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_instance_service_proto_rawDescGZIP(), []int{1}\n}\n\n// An instance setting resource.\ntype InstanceSetting struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// The name of the instance setting.\n\t// Format: instance/settings/{setting}\n\tName string `protobuf:\"bytes,1,opt,name=name,proto3\" json:\"name,omitempty\"`\n\t// Types that are valid to be assigned to Value:\n\t//\n\t//\t*InstanceSetting_GeneralSetting_\n\t//\t*InstanceSetting_StorageSetting_\n\t//\t*InstanceSetting_MemoRelatedSetting_\n\t//\t*InstanceSetting_TagsSetting_\n\t//\t*InstanceSetting_NotificationSetting_\n\tValue         isInstanceSetting_Value `protobuf_oneof:\"value\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *InstanceSetting) Reset() {\n\t*x = InstanceSetting{}\n\tmi := &file_api_v1_instance_service_proto_msgTypes[2]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *InstanceSetting) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*InstanceSetting) ProtoMessage() {}\n\nfunc (x *InstanceSetting) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_instance_service_proto_msgTypes[2]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use InstanceSetting.ProtoReflect.Descriptor instead.\nfunc (*InstanceSetting) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_instance_service_proto_rawDescGZIP(), []int{2}\n}\n\nfunc (x *InstanceSetting) GetName() string {\n\tif x != nil {\n\t\treturn x.Name\n\t}\n\treturn \"\"\n}\n\nfunc (x *InstanceSetting) GetValue() isInstanceSetting_Value {\n\tif x != nil {\n\t\treturn x.Value\n\t}\n\treturn nil\n}\n\nfunc (x *InstanceSetting) GetGeneralSetting() *InstanceSetting_GeneralSetting {\n\tif x != nil {\n\t\tif x, ok := x.Value.(*InstanceSetting_GeneralSetting_); ok {\n\t\t\treturn x.GeneralSetting\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *InstanceSetting) GetStorageSetting() *InstanceSetting_StorageSetting {\n\tif x != nil {\n\t\tif x, ok := x.Value.(*InstanceSetting_StorageSetting_); ok {\n\t\t\treturn x.StorageSetting\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *InstanceSetting) GetMemoRelatedSetting() *InstanceSetting_MemoRelatedSetting {\n\tif x != nil {\n\t\tif x, ok := x.Value.(*InstanceSetting_MemoRelatedSetting_); ok {\n\t\t\treturn x.MemoRelatedSetting\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *InstanceSetting) GetTagsSetting() *InstanceSetting_TagsSetting {\n\tif x != nil {\n\t\tif x, ok := x.Value.(*InstanceSetting_TagsSetting_); ok {\n\t\t\treturn x.TagsSetting\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *InstanceSetting) GetNotificationSetting() *InstanceSetting_NotificationSetting {\n\tif x != nil {\n\t\tif x, ok := x.Value.(*InstanceSetting_NotificationSetting_); ok {\n\t\t\treturn x.NotificationSetting\n\t\t}\n\t}\n\treturn nil\n}\n\ntype isInstanceSetting_Value interface {\n\tisInstanceSetting_Value()\n}\n\ntype InstanceSetting_GeneralSetting_ struct {\n\tGeneralSetting *InstanceSetting_GeneralSetting `protobuf:\"bytes,2,opt,name=general_setting,json=generalSetting,proto3,oneof\"`\n}\n\ntype InstanceSetting_StorageSetting_ struct {\n\tStorageSetting *InstanceSetting_StorageSetting `protobuf:\"bytes,3,opt,name=storage_setting,json=storageSetting,proto3,oneof\"`\n}\n\ntype InstanceSetting_MemoRelatedSetting_ struct {\n\tMemoRelatedSetting *InstanceSetting_MemoRelatedSetting `protobuf:\"bytes,4,opt,name=memo_related_setting,json=memoRelatedSetting,proto3,oneof\"`\n}\n\ntype InstanceSetting_TagsSetting_ struct {\n\tTagsSetting *InstanceSetting_TagsSetting `protobuf:\"bytes,5,opt,name=tags_setting,json=tagsSetting,proto3,oneof\"`\n}\n\ntype InstanceSetting_NotificationSetting_ struct {\n\tNotificationSetting *InstanceSetting_NotificationSetting `protobuf:\"bytes,6,opt,name=notification_setting,json=notificationSetting,proto3,oneof\"`\n}\n\nfunc (*InstanceSetting_GeneralSetting_) isInstanceSetting_Value() {}\n\nfunc (*InstanceSetting_StorageSetting_) isInstanceSetting_Value() {}\n\nfunc (*InstanceSetting_MemoRelatedSetting_) isInstanceSetting_Value() {}\n\nfunc (*InstanceSetting_TagsSetting_) isInstanceSetting_Value() {}\n\nfunc (*InstanceSetting_NotificationSetting_) isInstanceSetting_Value() {}\n\n// Request message for GetInstanceSetting method.\ntype GetInstanceSettingRequest struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// The resource name of the instance setting.\n\t// Format: instance/settings/{setting}\n\tName          string `protobuf:\"bytes,1,opt,name=name,proto3\" json:\"name,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *GetInstanceSettingRequest) Reset() {\n\t*x = GetInstanceSettingRequest{}\n\tmi := &file_api_v1_instance_service_proto_msgTypes[3]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *GetInstanceSettingRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*GetInstanceSettingRequest) ProtoMessage() {}\n\nfunc (x *GetInstanceSettingRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_instance_service_proto_msgTypes[3]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use GetInstanceSettingRequest.ProtoReflect.Descriptor instead.\nfunc (*GetInstanceSettingRequest) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_instance_service_proto_rawDescGZIP(), []int{3}\n}\n\nfunc (x *GetInstanceSettingRequest) GetName() string {\n\tif x != nil {\n\t\treturn x.Name\n\t}\n\treturn \"\"\n}\n\n// Request message for UpdateInstanceSetting method.\ntype UpdateInstanceSettingRequest struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// The instance setting resource which replaces the resource on the server.\n\tSetting *InstanceSetting `protobuf:\"bytes,1,opt,name=setting,proto3\" json:\"setting,omitempty\"`\n\t// The list of fields to update.\n\tUpdateMask    *fieldmaskpb.FieldMask `protobuf:\"bytes,2,opt,name=update_mask,json=updateMask,proto3\" json:\"update_mask,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *UpdateInstanceSettingRequest) Reset() {\n\t*x = UpdateInstanceSettingRequest{}\n\tmi := &file_api_v1_instance_service_proto_msgTypes[4]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *UpdateInstanceSettingRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*UpdateInstanceSettingRequest) ProtoMessage() {}\n\nfunc (x *UpdateInstanceSettingRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_instance_service_proto_msgTypes[4]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use UpdateInstanceSettingRequest.ProtoReflect.Descriptor instead.\nfunc (*UpdateInstanceSettingRequest) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_instance_service_proto_rawDescGZIP(), []int{4}\n}\n\nfunc (x *UpdateInstanceSettingRequest) GetSetting() *InstanceSetting {\n\tif x != nil {\n\t\treturn x.Setting\n\t}\n\treturn nil\n}\n\nfunc (x *UpdateInstanceSettingRequest) GetUpdateMask() *fieldmaskpb.FieldMask {\n\tif x != nil {\n\t\treturn x.UpdateMask\n\t}\n\treturn nil\n}\n\n// General instance settings configuration.\ntype InstanceSetting_GeneralSetting struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// disallow_user_registration disallows user registration.\n\tDisallowUserRegistration bool `protobuf:\"varint,2,opt,name=disallow_user_registration,json=disallowUserRegistration,proto3\" json:\"disallow_user_registration,omitempty\"`\n\t// disallow_password_auth disallows password authentication.\n\tDisallowPasswordAuth bool `protobuf:\"varint,3,opt,name=disallow_password_auth,json=disallowPasswordAuth,proto3\" json:\"disallow_password_auth,omitempty\"`\n\t// additional_script is the additional script.\n\tAdditionalScript string `protobuf:\"bytes,4,opt,name=additional_script,json=additionalScript,proto3\" json:\"additional_script,omitempty\"`\n\t// additional_style is the additional style.\n\tAdditionalStyle string `protobuf:\"bytes,5,opt,name=additional_style,json=additionalStyle,proto3\" json:\"additional_style,omitempty\"`\n\t// custom_profile is the custom profile.\n\tCustomProfile *InstanceSetting_GeneralSetting_CustomProfile `protobuf:\"bytes,6,opt,name=custom_profile,json=customProfile,proto3\" json:\"custom_profile,omitempty\"`\n\t// week_start_day_offset is the week start day offset from Sunday.\n\t// 0: Sunday, 1: Monday, 2: Tuesday, 3: Wednesday, 4: Thursday, 5: Friday, 6: Saturday\n\t// Default is Sunday.\n\tWeekStartDayOffset int32 `protobuf:\"varint,7,opt,name=week_start_day_offset,json=weekStartDayOffset,proto3\" json:\"week_start_day_offset,omitempty\"`\n\t// disallow_change_username disallows changing username.\n\tDisallowChangeUsername bool `protobuf:\"varint,8,opt,name=disallow_change_username,json=disallowChangeUsername,proto3\" json:\"disallow_change_username,omitempty\"`\n\t// disallow_change_nickname disallows changing nickname.\n\tDisallowChangeNickname bool `protobuf:\"varint,9,opt,name=disallow_change_nickname,json=disallowChangeNickname,proto3\" json:\"disallow_change_nickname,omitempty\"`\n\tunknownFields          protoimpl.UnknownFields\n\tsizeCache              protoimpl.SizeCache\n}\n\nfunc (x *InstanceSetting_GeneralSetting) Reset() {\n\t*x = InstanceSetting_GeneralSetting{}\n\tmi := &file_api_v1_instance_service_proto_msgTypes[5]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *InstanceSetting_GeneralSetting) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*InstanceSetting_GeneralSetting) ProtoMessage() {}\n\nfunc (x *InstanceSetting_GeneralSetting) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_instance_service_proto_msgTypes[5]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use InstanceSetting_GeneralSetting.ProtoReflect.Descriptor instead.\nfunc (*InstanceSetting_GeneralSetting) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_instance_service_proto_rawDescGZIP(), []int{2, 0}\n}\n\nfunc (x *InstanceSetting_GeneralSetting) GetDisallowUserRegistration() bool {\n\tif x != nil {\n\t\treturn x.DisallowUserRegistration\n\t}\n\treturn false\n}\n\nfunc (x *InstanceSetting_GeneralSetting) GetDisallowPasswordAuth() bool {\n\tif x != nil {\n\t\treturn x.DisallowPasswordAuth\n\t}\n\treturn false\n}\n\nfunc (x *InstanceSetting_GeneralSetting) GetAdditionalScript() string {\n\tif x != nil {\n\t\treturn x.AdditionalScript\n\t}\n\treturn \"\"\n}\n\nfunc (x *InstanceSetting_GeneralSetting) GetAdditionalStyle() string {\n\tif x != nil {\n\t\treturn x.AdditionalStyle\n\t}\n\treturn \"\"\n}\n\nfunc (x *InstanceSetting_GeneralSetting) GetCustomProfile() *InstanceSetting_GeneralSetting_CustomProfile {\n\tif x != nil {\n\t\treturn x.CustomProfile\n\t}\n\treturn nil\n}\n\nfunc (x *InstanceSetting_GeneralSetting) GetWeekStartDayOffset() int32 {\n\tif x != nil {\n\t\treturn x.WeekStartDayOffset\n\t}\n\treturn 0\n}\n\nfunc (x *InstanceSetting_GeneralSetting) GetDisallowChangeUsername() bool {\n\tif x != nil {\n\t\treturn x.DisallowChangeUsername\n\t}\n\treturn false\n}\n\nfunc (x *InstanceSetting_GeneralSetting) GetDisallowChangeNickname() bool {\n\tif x != nil {\n\t\treturn x.DisallowChangeNickname\n\t}\n\treturn false\n}\n\n// Storage configuration settings for instance attachments.\ntype InstanceSetting_StorageSetting struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// storage_type is the storage type.\n\tStorageType InstanceSetting_StorageSetting_StorageType `protobuf:\"varint,1,opt,name=storage_type,json=storageType,proto3,enum=memos.api.v1.InstanceSetting_StorageSetting_StorageType\" json:\"storage_type,omitempty\"`\n\t// The template of file path.\n\t// e.g. assets/{timestamp}_{filename}\n\tFilepathTemplate string `protobuf:\"bytes,2,opt,name=filepath_template,json=filepathTemplate,proto3\" json:\"filepath_template,omitempty\"`\n\t// The max upload size in megabytes.\n\tUploadSizeLimitMb int64 `protobuf:\"varint,3,opt,name=upload_size_limit_mb,json=uploadSizeLimitMb,proto3\" json:\"upload_size_limit_mb,omitempty\"`\n\t// The S3 config.\n\tS3Config      *InstanceSetting_StorageSetting_S3Config `protobuf:\"bytes,4,opt,name=s3_config,json=s3Config,proto3\" json:\"s3_config,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *InstanceSetting_StorageSetting) Reset() {\n\t*x = InstanceSetting_StorageSetting{}\n\tmi := &file_api_v1_instance_service_proto_msgTypes[6]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *InstanceSetting_StorageSetting) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*InstanceSetting_StorageSetting) ProtoMessage() {}\n\nfunc (x *InstanceSetting_StorageSetting) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_instance_service_proto_msgTypes[6]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use InstanceSetting_StorageSetting.ProtoReflect.Descriptor instead.\nfunc (*InstanceSetting_StorageSetting) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_instance_service_proto_rawDescGZIP(), []int{2, 1}\n}\n\nfunc (x *InstanceSetting_StorageSetting) GetStorageType() InstanceSetting_StorageSetting_StorageType {\n\tif x != nil {\n\t\treturn x.StorageType\n\t}\n\treturn InstanceSetting_StorageSetting_STORAGE_TYPE_UNSPECIFIED\n}\n\nfunc (x *InstanceSetting_StorageSetting) GetFilepathTemplate() string {\n\tif x != nil {\n\t\treturn x.FilepathTemplate\n\t}\n\treturn \"\"\n}\n\nfunc (x *InstanceSetting_StorageSetting) GetUploadSizeLimitMb() int64 {\n\tif x != nil {\n\t\treturn x.UploadSizeLimitMb\n\t}\n\treturn 0\n}\n\nfunc (x *InstanceSetting_StorageSetting) GetS3Config() *InstanceSetting_StorageSetting_S3Config {\n\tif x != nil {\n\t\treturn x.S3Config\n\t}\n\treturn nil\n}\n\n// Memo-related instance settings and policies.\ntype InstanceSetting_MemoRelatedSetting struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// display_with_update_time orders and displays memo with update time.\n\tDisplayWithUpdateTime bool `protobuf:\"varint,2,opt,name=display_with_update_time,json=displayWithUpdateTime,proto3\" json:\"display_with_update_time,omitempty\"`\n\t// content_length_limit is the limit of content length. Unit is byte.\n\tContentLengthLimit int32 `protobuf:\"varint,3,opt,name=content_length_limit,json=contentLengthLimit,proto3\" json:\"content_length_limit,omitempty\"`\n\t// enable_double_click_edit enables editing on double click.\n\tEnableDoubleClickEdit bool `protobuf:\"varint,4,opt,name=enable_double_click_edit,json=enableDoubleClickEdit,proto3\" json:\"enable_double_click_edit,omitempty\"`\n\t// reactions is the list of reactions.\n\tReactions     []string `protobuf:\"bytes,7,rep,name=reactions,proto3\" json:\"reactions,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *InstanceSetting_MemoRelatedSetting) Reset() {\n\t*x = InstanceSetting_MemoRelatedSetting{}\n\tmi := &file_api_v1_instance_service_proto_msgTypes[7]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *InstanceSetting_MemoRelatedSetting) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*InstanceSetting_MemoRelatedSetting) ProtoMessage() {}\n\nfunc (x *InstanceSetting_MemoRelatedSetting) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_instance_service_proto_msgTypes[7]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use InstanceSetting_MemoRelatedSetting.ProtoReflect.Descriptor instead.\nfunc (*InstanceSetting_MemoRelatedSetting) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_instance_service_proto_rawDescGZIP(), []int{2, 2}\n}\n\nfunc (x *InstanceSetting_MemoRelatedSetting) GetDisplayWithUpdateTime() bool {\n\tif x != nil {\n\t\treturn x.DisplayWithUpdateTime\n\t}\n\treturn false\n}\n\nfunc (x *InstanceSetting_MemoRelatedSetting) GetContentLengthLimit() int32 {\n\tif x != nil {\n\t\treturn x.ContentLengthLimit\n\t}\n\treturn 0\n}\n\nfunc (x *InstanceSetting_MemoRelatedSetting) GetEnableDoubleClickEdit() bool {\n\tif x != nil {\n\t\treturn x.EnableDoubleClickEdit\n\t}\n\treturn false\n}\n\nfunc (x *InstanceSetting_MemoRelatedSetting) GetReactions() []string {\n\tif x != nil {\n\t\treturn x.Reactions\n\t}\n\treturn nil\n}\n\n// Metadata for a tag.\ntype InstanceSetting_TagMetadata struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// Background color for the tag label.\n\tBackgroundColor *color.Color `protobuf:\"bytes,1,opt,name=background_color,json=backgroundColor,proto3\" json:\"background_color,omitempty\"`\n\tunknownFields   protoimpl.UnknownFields\n\tsizeCache       protoimpl.SizeCache\n}\n\nfunc (x *InstanceSetting_TagMetadata) Reset() {\n\t*x = InstanceSetting_TagMetadata{}\n\tmi := &file_api_v1_instance_service_proto_msgTypes[8]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *InstanceSetting_TagMetadata) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*InstanceSetting_TagMetadata) ProtoMessage() {}\n\nfunc (x *InstanceSetting_TagMetadata) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_instance_service_proto_msgTypes[8]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use InstanceSetting_TagMetadata.ProtoReflect.Descriptor instead.\nfunc (*InstanceSetting_TagMetadata) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_instance_service_proto_rawDescGZIP(), []int{2, 3}\n}\n\nfunc (x *InstanceSetting_TagMetadata) GetBackgroundColor() *color.Color {\n\tif x != nil {\n\t\treturn x.BackgroundColor\n\t}\n\treturn nil\n}\n\n// Tag metadata configuration.\ntype InstanceSetting_TagsSetting struct {\n\tstate         protoimpl.MessageState                  `protogen:\"open.v1\"`\n\tTags          map[string]*InstanceSetting_TagMetadata `protobuf:\"bytes,1,rep,name=tags,proto3\" json:\"tags,omitempty\" protobuf_key:\"bytes,1,opt,name=key\" protobuf_val:\"bytes,2,opt,name=value\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *InstanceSetting_TagsSetting) Reset() {\n\t*x = InstanceSetting_TagsSetting{}\n\tmi := &file_api_v1_instance_service_proto_msgTypes[9]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *InstanceSetting_TagsSetting) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*InstanceSetting_TagsSetting) ProtoMessage() {}\n\nfunc (x *InstanceSetting_TagsSetting) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_instance_service_proto_msgTypes[9]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use InstanceSetting_TagsSetting.ProtoReflect.Descriptor instead.\nfunc (*InstanceSetting_TagsSetting) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_instance_service_proto_rawDescGZIP(), []int{2, 4}\n}\n\nfunc (x *InstanceSetting_TagsSetting) GetTags() map[string]*InstanceSetting_TagMetadata {\n\tif x != nil {\n\t\treturn x.Tags\n\t}\n\treturn nil\n}\n\n// Notification transport configuration.\ntype InstanceSetting_NotificationSetting struct {\n\tstate         protoimpl.MessageState                            `protogen:\"open.v1\"`\n\tEmail         *InstanceSetting_NotificationSetting_EmailSetting `protobuf:\"bytes,1,opt,name=email,proto3\" json:\"email,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *InstanceSetting_NotificationSetting) Reset() {\n\t*x = InstanceSetting_NotificationSetting{}\n\tmi := &file_api_v1_instance_service_proto_msgTypes[10]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *InstanceSetting_NotificationSetting) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*InstanceSetting_NotificationSetting) ProtoMessage() {}\n\nfunc (x *InstanceSetting_NotificationSetting) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_instance_service_proto_msgTypes[10]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use InstanceSetting_NotificationSetting.ProtoReflect.Descriptor instead.\nfunc (*InstanceSetting_NotificationSetting) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_instance_service_proto_rawDescGZIP(), []int{2, 5}\n}\n\nfunc (x *InstanceSetting_NotificationSetting) GetEmail() *InstanceSetting_NotificationSetting_EmailSetting {\n\tif x != nil {\n\t\treturn x.Email\n\t}\n\treturn nil\n}\n\n// Custom profile configuration for instance branding.\ntype InstanceSetting_GeneralSetting_CustomProfile struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tTitle         string                 `protobuf:\"bytes,1,opt,name=title,proto3\" json:\"title,omitempty\"`\n\tDescription   string                 `protobuf:\"bytes,2,opt,name=description,proto3\" json:\"description,omitempty\"`\n\tLogoUrl       string                 `protobuf:\"bytes,3,opt,name=logo_url,json=logoUrl,proto3\" json:\"logo_url,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *InstanceSetting_GeneralSetting_CustomProfile) Reset() {\n\t*x = InstanceSetting_GeneralSetting_CustomProfile{}\n\tmi := &file_api_v1_instance_service_proto_msgTypes[11]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *InstanceSetting_GeneralSetting_CustomProfile) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*InstanceSetting_GeneralSetting_CustomProfile) ProtoMessage() {}\n\nfunc (x *InstanceSetting_GeneralSetting_CustomProfile) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_instance_service_proto_msgTypes[11]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use InstanceSetting_GeneralSetting_CustomProfile.ProtoReflect.Descriptor instead.\nfunc (*InstanceSetting_GeneralSetting_CustomProfile) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_instance_service_proto_rawDescGZIP(), []int{2, 0, 0}\n}\n\nfunc (x *InstanceSetting_GeneralSetting_CustomProfile) GetTitle() string {\n\tif x != nil {\n\t\treturn x.Title\n\t}\n\treturn \"\"\n}\n\nfunc (x *InstanceSetting_GeneralSetting_CustomProfile) GetDescription() string {\n\tif x != nil {\n\t\treturn x.Description\n\t}\n\treturn \"\"\n}\n\nfunc (x *InstanceSetting_GeneralSetting_CustomProfile) GetLogoUrl() string {\n\tif x != nil {\n\t\treturn x.LogoUrl\n\t}\n\treturn \"\"\n}\n\n// S3 configuration for cloud storage backend.\n// Reference: https://developers.cloudflare.com/r2/examples/aws/aws-sdk-go/\ntype InstanceSetting_StorageSetting_S3Config struct {\n\tstate           protoimpl.MessageState `protogen:\"open.v1\"`\n\tAccessKeyId     string                 `protobuf:\"bytes,1,opt,name=access_key_id,json=accessKeyId,proto3\" json:\"access_key_id,omitempty\"`\n\tAccessKeySecret string                 `protobuf:\"bytes,2,opt,name=access_key_secret,json=accessKeySecret,proto3\" json:\"access_key_secret,omitempty\"`\n\tEndpoint        string                 `protobuf:\"bytes,3,opt,name=endpoint,proto3\" json:\"endpoint,omitempty\"`\n\tRegion          string                 `protobuf:\"bytes,4,opt,name=region,proto3\" json:\"region,omitempty\"`\n\tBucket          string                 `protobuf:\"bytes,5,opt,name=bucket,proto3\" json:\"bucket,omitempty\"`\n\tUsePathStyle    bool                   `protobuf:\"varint,6,opt,name=use_path_style,json=usePathStyle,proto3\" json:\"use_path_style,omitempty\"`\n\tunknownFields   protoimpl.UnknownFields\n\tsizeCache       protoimpl.SizeCache\n}\n\nfunc (x *InstanceSetting_StorageSetting_S3Config) Reset() {\n\t*x = InstanceSetting_StorageSetting_S3Config{}\n\tmi := &file_api_v1_instance_service_proto_msgTypes[12]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *InstanceSetting_StorageSetting_S3Config) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*InstanceSetting_StorageSetting_S3Config) ProtoMessage() {}\n\nfunc (x *InstanceSetting_StorageSetting_S3Config) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_instance_service_proto_msgTypes[12]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use InstanceSetting_StorageSetting_S3Config.ProtoReflect.Descriptor instead.\nfunc (*InstanceSetting_StorageSetting_S3Config) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_instance_service_proto_rawDescGZIP(), []int{2, 1, 0}\n}\n\nfunc (x *InstanceSetting_StorageSetting_S3Config) GetAccessKeyId() string {\n\tif x != nil {\n\t\treturn x.AccessKeyId\n\t}\n\treturn \"\"\n}\n\nfunc (x *InstanceSetting_StorageSetting_S3Config) GetAccessKeySecret() string {\n\tif x != nil {\n\t\treturn x.AccessKeySecret\n\t}\n\treturn \"\"\n}\n\nfunc (x *InstanceSetting_StorageSetting_S3Config) GetEndpoint() string {\n\tif x != nil {\n\t\treturn x.Endpoint\n\t}\n\treturn \"\"\n}\n\nfunc (x *InstanceSetting_StorageSetting_S3Config) GetRegion() string {\n\tif x != nil {\n\t\treturn x.Region\n\t}\n\treturn \"\"\n}\n\nfunc (x *InstanceSetting_StorageSetting_S3Config) GetBucket() string {\n\tif x != nil {\n\t\treturn x.Bucket\n\t}\n\treturn \"\"\n}\n\nfunc (x *InstanceSetting_StorageSetting_S3Config) GetUsePathStyle() bool {\n\tif x != nil {\n\t\treturn x.UsePathStyle\n\t}\n\treturn false\n}\n\n// Email delivery configuration for notifications.\ntype InstanceSetting_NotificationSetting_EmailSetting struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tEnabled       bool                   `protobuf:\"varint,1,opt,name=enabled,proto3\" json:\"enabled,omitempty\"`\n\tSmtpHost      string                 `protobuf:\"bytes,2,opt,name=smtp_host,json=smtpHost,proto3\" json:\"smtp_host,omitempty\"`\n\tSmtpPort      int32                  `protobuf:\"varint,3,opt,name=smtp_port,json=smtpPort,proto3\" json:\"smtp_port,omitempty\"`\n\tSmtpUsername  string                 `protobuf:\"bytes,4,opt,name=smtp_username,json=smtpUsername,proto3\" json:\"smtp_username,omitempty\"`\n\tSmtpPassword  string                 `protobuf:\"bytes,5,opt,name=smtp_password,json=smtpPassword,proto3\" json:\"smtp_password,omitempty\"`\n\tFromEmail     string                 `protobuf:\"bytes,6,opt,name=from_email,json=fromEmail,proto3\" json:\"from_email,omitempty\"`\n\tFromName      string                 `protobuf:\"bytes,7,opt,name=from_name,json=fromName,proto3\" json:\"from_name,omitempty\"`\n\tReplyTo       string                 `protobuf:\"bytes,8,opt,name=reply_to,json=replyTo,proto3\" json:\"reply_to,omitempty\"`\n\tUseTls        bool                   `protobuf:\"varint,9,opt,name=use_tls,json=useTls,proto3\" json:\"use_tls,omitempty\"`\n\tUseSsl        bool                   `protobuf:\"varint,10,opt,name=use_ssl,json=useSsl,proto3\" json:\"use_ssl,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *InstanceSetting_NotificationSetting_EmailSetting) Reset() {\n\t*x = InstanceSetting_NotificationSetting_EmailSetting{}\n\tmi := &file_api_v1_instance_service_proto_msgTypes[14]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *InstanceSetting_NotificationSetting_EmailSetting) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*InstanceSetting_NotificationSetting_EmailSetting) ProtoMessage() {}\n\nfunc (x *InstanceSetting_NotificationSetting_EmailSetting) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_instance_service_proto_msgTypes[14]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use InstanceSetting_NotificationSetting_EmailSetting.ProtoReflect.Descriptor instead.\nfunc (*InstanceSetting_NotificationSetting_EmailSetting) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_instance_service_proto_rawDescGZIP(), []int{2, 5, 0}\n}\n\nfunc (x *InstanceSetting_NotificationSetting_EmailSetting) GetEnabled() bool {\n\tif x != nil {\n\t\treturn x.Enabled\n\t}\n\treturn false\n}\n\nfunc (x *InstanceSetting_NotificationSetting_EmailSetting) GetSmtpHost() string {\n\tif x != nil {\n\t\treturn x.SmtpHost\n\t}\n\treturn \"\"\n}\n\nfunc (x *InstanceSetting_NotificationSetting_EmailSetting) GetSmtpPort() int32 {\n\tif x != nil {\n\t\treturn x.SmtpPort\n\t}\n\treturn 0\n}\n\nfunc (x *InstanceSetting_NotificationSetting_EmailSetting) GetSmtpUsername() string {\n\tif x != nil {\n\t\treturn x.SmtpUsername\n\t}\n\treturn \"\"\n}\n\nfunc (x *InstanceSetting_NotificationSetting_EmailSetting) GetSmtpPassword() string {\n\tif x != nil {\n\t\treturn x.SmtpPassword\n\t}\n\treturn \"\"\n}\n\nfunc (x *InstanceSetting_NotificationSetting_EmailSetting) GetFromEmail() string {\n\tif x != nil {\n\t\treturn x.FromEmail\n\t}\n\treturn \"\"\n}\n\nfunc (x *InstanceSetting_NotificationSetting_EmailSetting) GetFromName() string {\n\tif x != nil {\n\t\treturn x.FromName\n\t}\n\treturn \"\"\n}\n\nfunc (x *InstanceSetting_NotificationSetting_EmailSetting) GetReplyTo() string {\n\tif x != nil {\n\t\treturn x.ReplyTo\n\t}\n\treturn \"\"\n}\n\nfunc (x *InstanceSetting_NotificationSetting_EmailSetting) GetUseTls() bool {\n\tif x != nil {\n\t\treturn x.UseTls\n\t}\n\treturn false\n}\n\nfunc (x *InstanceSetting_NotificationSetting_EmailSetting) GetUseSsl() bool {\n\tif x != nil {\n\t\treturn x.UseSsl\n\t}\n\treturn false\n}\n\nvar File_api_v1_instance_service_proto protoreflect.FileDescriptor\n\nconst file_api_v1_instance_service_proto_rawDesc = \"\" +\n\t\"\\n\" +\n\t\"\\x1dapi/v1/instance_service.proto\\x12\\fmemos.api.v1\\x1a\\x19api/v1/user_service.proto\\x1a\\x1cgoogle/api/annotations.proto\\x1a\\x17google/api/client.proto\\x1a\\x1fgoogle/api/field_behavior.proto\\x1a\\x19google/api/resource.proto\\x1a google/protobuf/field_mask.proto\\x1a\\x17google/type/color.proto\\\"\\x8c\\x01\\n\" +\n\t\"\\x0fInstanceProfile\\x12\\x18\\n\" +\n\t\"\\aversion\\x18\\x02 \\x01(\\tR\\aversion\\x12\\x12\\n\" +\n\t\"\\x04demo\\x18\\x03 \\x01(\\bR\\x04demo\\x12!\\n\" +\n\t\"\\finstance_url\\x18\\x06 \\x01(\\tR\\vinstanceUrl\\x12(\\n\" +\n\t\"\\x05admin\\x18\\a \\x01(\\v2\\x12.memos.api.v1.UserR\\x05admin\\\"\\x1b\\n\" +\n\t\"\\x19GetInstanceProfileRequest\\\"\\xe0\\x15\\n\" +\n\t\"\\x0fInstanceSetting\\x12\\x17\\n\" +\n\t\"\\x04name\\x18\\x01 \\x01(\\tB\\x03\\xe0A\\bR\\x04name\\x12W\\n\" +\n\t\"\\x0fgeneral_setting\\x18\\x02 \\x01(\\v2,.memos.api.v1.InstanceSetting.GeneralSettingH\\x00R\\x0egeneralSetting\\x12W\\n\" +\n\t\"\\x0fstorage_setting\\x18\\x03 \\x01(\\v2,.memos.api.v1.InstanceSetting.StorageSettingH\\x00R\\x0estorageSetting\\x12d\\n\" +\n\t\"\\x14memo_related_setting\\x18\\x04 \\x01(\\v20.memos.api.v1.InstanceSetting.MemoRelatedSettingH\\x00R\\x12memoRelatedSetting\\x12N\\n\" +\n\t\"\\ftags_setting\\x18\\x05 \\x01(\\v2).memos.api.v1.InstanceSetting.TagsSettingH\\x00R\\vtagsSetting\\x12f\\n\" +\n\t\"\\x14notification_setting\\x18\\x06 \\x01(\\v21.memos.api.v1.InstanceSetting.NotificationSettingH\\x00R\\x13notificationSetting\\x1a\\xca\\x04\\n\" +\n\t\"\\x0eGeneralSetting\\x12<\\n\" +\n\t\"\\x1adisallow_user_registration\\x18\\x02 \\x01(\\bR\\x18disallowUserRegistration\\x124\\n\" +\n\t\"\\x16disallow_password_auth\\x18\\x03 \\x01(\\bR\\x14disallowPasswordAuth\\x12+\\n\" +\n\t\"\\x11additional_script\\x18\\x04 \\x01(\\tR\\x10additionalScript\\x12)\\n\" +\n\t\"\\x10additional_style\\x18\\x05 \\x01(\\tR\\x0fadditionalStyle\\x12a\\n\" +\n\t\"\\x0ecustom_profile\\x18\\x06 \\x01(\\v2:.memos.api.v1.InstanceSetting.GeneralSetting.CustomProfileR\\rcustomProfile\\x121\\n\" +\n\t\"\\x15week_start_day_offset\\x18\\a \\x01(\\x05R\\x12weekStartDayOffset\\x128\\n\" +\n\t\"\\x18disallow_change_username\\x18\\b \\x01(\\bR\\x16disallowChangeUsername\\x128\\n\" +\n\t\"\\x18disallow_change_nickname\\x18\\t \\x01(\\bR\\x16disallowChangeNickname\\x1ab\\n\" +\n\t\"\\rCustomProfile\\x12\\x14\\n\" +\n\t\"\\x05title\\x18\\x01 \\x01(\\tR\\x05title\\x12 \\n\" +\n\t\"\\vdescription\\x18\\x02 \\x01(\\tR\\vdescription\\x12\\x19\\n\" +\n\t\"\\blogo_url\\x18\\x03 \\x01(\\tR\\alogoUrl\\x1a\\xbc\\x04\\n\" +\n\t\"\\x0eStorageSetting\\x12[\\n\" +\n\t\"\\fstorage_type\\x18\\x01 \\x01(\\x0e28.memos.api.v1.InstanceSetting.StorageSetting.StorageTypeR\\vstorageType\\x12+\\n\" +\n\t\"\\x11filepath_template\\x18\\x02 \\x01(\\tR\\x10filepathTemplate\\x12/\\n\" +\n\t\"\\x14upload_size_limit_mb\\x18\\x03 \\x01(\\x03R\\x11uploadSizeLimitMb\\x12R\\n\" +\n\t\"\\ts3_config\\x18\\x04 \\x01(\\v25.memos.api.v1.InstanceSetting.StorageSetting.S3ConfigR\\bs3Config\\x1a\\xcc\\x01\\n\" +\n\t\"\\bS3Config\\x12\\\"\\n\" +\n\t\"\\raccess_key_id\\x18\\x01 \\x01(\\tR\\vaccessKeyId\\x12*\\n\" +\n\t\"\\x11access_key_secret\\x18\\x02 \\x01(\\tR\\x0faccessKeySecret\\x12\\x1a\\n\" +\n\t\"\\bendpoint\\x18\\x03 \\x01(\\tR\\bendpoint\\x12\\x16\\n\" +\n\t\"\\x06region\\x18\\x04 \\x01(\\tR\\x06region\\x12\\x16\\n\" +\n\t\"\\x06bucket\\x18\\x05 \\x01(\\tR\\x06bucket\\x12$\\n\" +\n\t\"\\x0euse_path_style\\x18\\x06 \\x01(\\bR\\fusePathStyle\\\"L\\n\" +\n\t\"\\vStorageType\\x12\\x1c\\n\" +\n\t\"\\x18STORAGE_TYPE_UNSPECIFIED\\x10\\x00\\x12\\f\\n\" +\n\t\"\\bDATABASE\\x10\\x01\\x12\\t\\n\" +\n\t\"\\x05LOCAL\\x10\\x02\\x12\\x06\\n\" +\n\t\"\\x02S3\\x10\\x03\\x1a\\xd6\\x01\\n\" +\n\t\"\\x12MemoRelatedSetting\\x127\\n\" +\n\t\"\\x18display_with_update_time\\x18\\x02 \\x01(\\bR\\x15displayWithUpdateTime\\x120\\n\" +\n\t\"\\x14content_length_limit\\x18\\x03 \\x01(\\x05R\\x12contentLengthLimit\\x127\\n\" +\n\t\"\\x18enable_double_click_edit\\x18\\x04 \\x01(\\bR\\x15enableDoubleClickEdit\\x12\\x1c\\n\" +\n\t\"\\treactions\\x18\\a \\x03(\\tR\\treactions\\x1aL\\n\" +\n\t\"\\vTagMetadata\\x12=\\n\" +\n\t\"\\x10background_color\\x18\\x01 \\x01(\\v2\\x12.google.type.ColorR\\x0fbackgroundColor\\x1a\\xba\\x01\\n\" +\n\t\"\\vTagsSetting\\x12G\\n\" +\n\t\"\\x04tags\\x18\\x01 \\x03(\\v23.memos.api.v1.InstanceSetting.TagsSetting.TagsEntryR\\x04tags\\x1ab\\n\" +\n\t\"\\tTagsEntry\\x12\\x10\\n\" +\n\t\"\\x03key\\x18\\x01 \\x01(\\tR\\x03key\\x12?\\n\" +\n\t\"\\x05value\\x18\\x02 \\x01(\\v2).memos.api.v1.InstanceSetting.TagMetadataR\\x05value:\\x028\\x01\\x1a\\xa3\\x03\\n\" +\n\t\"\\x13NotificationSetting\\x12T\\n\" +\n\t\"\\x05email\\x18\\x01 \\x01(\\v2>.memos.api.v1.InstanceSetting.NotificationSetting.EmailSettingR\\x05email\\x1a\\xb5\\x02\\n\" +\n\t\"\\fEmailSetting\\x12\\x18\\n\" +\n\t\"\\aenabled\\x18\\x01 \\x01(\\bR\\aenabled\\x12\\x1b\\n\" +\n\t\"\\tsmtp_host\\x18\\x02 \\x01(\\tR\\bsmtpHost\\x12\\x1b\\n\" +\n\t\"\\tsmtp_port\\x18\\x03 \\x01(\\x05R\\bsmtpPort\\x12#\\n\" +\n\t\"\\rsmtp_username\\x18\\x04 \\x01(\\tR\\fsmtpUsername\\x12#\\n\" +\n\t\"\\rsmtp_password\\x18\\x05 \\x01(\\tR\\fsmtpPassword\\x12\\x1d\\n\" +\n\t\"\\n\" +\n\t\"from_email\\x18\\x06 \\x01(\\tR\\tfromEmail\\x12\\x1b\\n\" +\n\t\"\\tfrom_name\\x18\\a \\x01(\\tR\\bfromName\\x12\\x19\\n\" +\n\t\"\\breply_to\\x18\\b \\x01(\\tR\\areplyTo\\x12\\x17\\n\" +\n\t\"\\ause_tls\\x18\\t \\x01(\\bR\\x06useTls\\x12\\x17\\n\" +\n\t\"\\ause_ssl\\x18\\n\" +\n\t\" \\x01(\\bR\\x06useSsl\\\"b\\n\" +\n\t\"\\x03Key\\x12\\x13\\n\" +\n\t\"\\x0fKEY_UNSPECIFIED\\x10\\x00\\x12\\v\\n\" +\n\t\"\\aGENERAL\\x10\\x01\\x12\\v\\n\" +\n\t\"\\aSTORAGE\\x10\\x02\\x12\\x10\\n\" +\n\t\"\\fMEMO_RELATED\\x10\\x03\\x12\\b\\n\" +\n\t\"\\x04TAGS\\x10\\x04\\x12\\x10\\n\" +\n\t\"\\fNOTIFICATION\\x10\\x05:a\\xeaA^\\n\" +\n\t\"\\x1cmemos.api.v1/InstanceSetting\\x12\\x1binstance/settings/{setting}*\\x10instanceSettings2\\x0finstanceSettingB\\a\\n\" +\n\t\"\\x05value\\\"U\\n\" +\n\t\"\\x19GetInstanceSettingRequest\\x128\\n\" +\n\t\"\\x04name\\x18\\x01 \\x01(\\tB$\\xe0A\\x02\\xfaA\\x1e\\n\" +\n\t\"\\x1cmemos.api.v1/InstanceSettingR\\x04name\\\"\\x9e\\x01\\n\" +\n\t\"\\x1cUpdateInstanceSettingRequest\\x12<\\n\" +\n\t\"\\asetting\\x18\\x01 \\x01(\\v2\\x1d.memos.api.v1.InstanceSettingB\\x03\\xe0A\\x02R\\asetting\\x12@\\n\" +\n\t\"\\vupdate_mask\\x18\\x02 \\x01(\\v2\\x1a.google.protobuf.FieldMaskB\\x03\\xe0A\\x01R\\n\" +\n\t\"updateMask2\\xdb\\x03\\n\" +\n\t\"\\x0fInstanceService\\x12~\\n\" +\n\t\"\\x12GetInstanceProfile\\x12'.memos.api.v1.GetInstanceProfileRequest\\x1a\\x1d.memos.api.v1.InstanceProfile\\\" \\x82\\xd3\\xe4\\x93\\x02\\x1a\\x12\\x18/api/v1/instance/profile\\x12\\x8f\\x01\\n\" +\n\t\"\\x12GetInstanceSetting\\x12'.memos.api.v1.GetInstanceSettingRequest\\x1a\\x1d.memos.api.v1.InstanceSetting\\\"1\\xdaA\\x04name\\x82\\xd3\\xe4\\x93\\x02$\\x12\\\"/api/v1/{name=instance/settings/*}\\x12\\xb5\\x01\\n\" +\n\t\"\\x15UpdateInstanceSetting\\x12*.memos.api.v1.UpdateInstanceSettingRequest\\x1a\\x1d.memos.api.v1.InstanceSetting\\\"Q\\xdaA\\x13setting,update_mask\\x82\\xd3\\xe4\\x93\\x025:\\asetting2*/api/v1/{setting.name=instance/settings/*}B\\xac\\x01\\n\" +\n\t\"\\x10com.memos.api.v1B\\x14InstanceServiceProtoP\\x01Z0github.com/usememos/memos/proto/gen/api/v1;apiv1\\xa2\\x02\\x03MAX\\xaa\\x02\\fMemos.Api.V1\\xca\\x02\\fMemos\\\\Api\\\\V1\\xe2\\x02\\x18Memos\\\\Api\\\\V1\\\\GPBMetadata\\xea\\x02\\x0eMemos::Api::V1b\\x06proto3\"\n\nvar (\n\tfile_api_v1_instance_service_proto_rawDescOnce sync.Once\n\tfile_api_v1_instance_service_proto_rawDescData []byte\n)\n\nfunc file_api_v1_instance_service_proto_rawDescGZIP() []byte {\n\tfile_api_v1_instance_service_proto_rawDescOnce.Do(func() {\n\t\tfile_api_v1_instance_service_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_api_v1_instance_service_proto_rawDesc), len(file_api_v1_instance_service_proto_rawDesc)))\n\t})\n\treturn file_api_v1_instance_service_proto_rawDescData\n}\n\nvar file_api_v1_instance_service_proto_enumTypes = make([]protoimpl.EnumInfo, 2)\nvar file_api_v1_instance_service_proto_msgTypes = make([]protoimpl.MessageInfo, 15)\nvar file_api_v1_instance_service_proto_goTypes = []any{\n\t(InstanceSetting_Key)(0),                             // 0: memos.api.v1.InstanceSetting.Key\n\t(InstanceSetting_StorageSetting_StorageType)(0),      // 1: memos.api.v1.InstanceSetting.StorageSetting.StorageType\n\t(*InstanceProfile)(nil),                              // 2: memos.api.v1.InstanceProfile\n\t(*GetInstanceProfileRequest)(nil),                    // 3: memos.api.v1.GetInstanceProfileRequest\n\t(*InstanceSetting)(nil),                              // 4: memos.api.v1.InstanceSetting\n\t(*GetInstanceSettingRequest)(nil),                    // 5: memos.api.v1.GetInstanceSettingRequest\n\t(*UpdateInstanceSettingRequest)(nil),                 // 6: memos.api.v1.UpdateInstanceSettingRequest\n\t(*InstanceSetting_GeneralSetting)(nil),               // 7: memos.api.v1.InstanceSetting.GeneralSetting\n\t(*InstanceSetting_StorageSetting)(nil),               // 8: memos.api.v1.InstanceSetting.StorageSetting\n\t(*InstanceSetting_MemoRelatedSetting)(nil),           // 9: memos.api.v1.InstanceSetting.MemoRelatedSetting\n\t(*InstanceSetting_TagMetadata)(nil),                  // 10: memos.api.v1.InstanceSetting.TagMetadata\n\t(*InstanceSetting_TagsSetting)(nil),                  // 11: memos.api.v1.InstanceSetting.TagsSetting\n\t(*InstanceSetting_NotificationSetting)(nil),          // 12: memos.api.v1.InstanceSetting.NotificationSetting\n\t(*InstanceSetting_GeneralSetting_CustomProfile)(nil), // 13: memos.api.v1.InstanceSetting.GeneralSetting.CustomProfile\n\t(*InstanceSetting_StorageSetting_S3Config)(nil),      // 14: memos.api.v1.InstanceSetting.StorageSetting.S3Config\n\tnil, // 15: memos.api.v1.InstanceSetting.TagsSetting.TagsEntry\n\t(*InstanceSetting_NotificationSetting_EmailSetting)(nil), // 16: memos.api.v1.InstanceSetting.NotificationSetting.EmailSetting\n\t(*User)(nil),                  // 17: memos.api.v1.User\n\t(*fieldmaskpb.FieldMask)(nil), // 18: google.protobuf.FieldMask\n\t(*color.Color)(nil),           // 19: google.type.Color\n}\nvar file_api_v1_instance_service_proto_depIdxs = []int32{\n\t17, // 0: memos.api.v1.InstanceProfile.admin:type_name -> memos.api.v1.User\n\t7,  // 1: memos.api.v1.InstanceSetting.general_setting:type_name -> memos.api.v1.InstanceSetting.GeneralSetting\n\t8,  // 2: memos.api.v1.InstanceSetting.storage_setting:type_name -> memos.api.v1.InstanceSetting.StorageSetting\n\t9,  // 3: memos.api.v1.InstanceSetting.memo_related_setting:type_name -> memos.api.v1.InstanceSetting.MemoRelatedSetting\n\t11, // 4: memos.api.v1.InstanceSetting.tags_setting:type_name -> memos.api.v1.InstanceSetting.TagsSetting\n\t12, // 5: memos.api.v1.InstanceSetting.notification_setting:type_name -> memos.api.v1.InstanceSetting.NotificationSetting\n\t4,  // 6: memos.api.v1.UpdateInstanceSettingRequest.setting:type_name -> memos.api.v1.InstanceSetting\n\t18, // 7: memos.api.v1.UpdateInstanceSettingRequest.update_mask:type_name -> google.protobuf.FieldMask\n\t13, // 8: memos.api.v1.InstanceSetting.GeneralSetting.custom_profile:type_name -> memos.api.v1.InstanceSetting.GeneralSetting.CustomProfile\n\t1,  // 9: memos.api.v1.InstanceSetting.StorageSetting.storage_type:type_name -> memos.api.v1.InstanceSetting.StorageSetting.StorageType\n\t14, // 10: memos.api.v1.InstanceSetting.StorageSetting.s3_config:type_name -> memos.api.v1.InstanceSetting.StorageSetting.S3Config\n\t19, // 11: memos.api.v1.InstanceSetting.TagMetadata.background_color:type_name -> google.type.Color\n\t15, // 12: memos.api.v1.InstanceSetting.TagsSetting.tags:type_name -> memos.api.v1.InstanceSetting.TagsSetting.TagsEntry\n\t16, // 13: memos.api.v1.InstanceSetting.NotificationSetting.email:type_name -> memos.api.v1.InstanceSetting.NotificationSetting.EmailSetting\n\t10, // 14: memos.api.v1.InstanceSetting.TagsSetting.TagsEntry.value:type_name -> memos.api.v1.InstanceSetting.TagMetadata\n\t3,  // 15: memos.api.v1.InstanceService.GetInstanceProfile:input_type -> memos.api.v1.GetInstanceProfileRequest\n\t5,  // 16: memos.api.v1.InstanceService.GetInstanceSetting:input_type -> memos.api.v1.GetInstanceSettingRequest\n\t6,  // 17: memos.api.v1.InstanceService.UpdateInstanceSetting:input_type -> memos.api.v1.UpdateInstanceSettingRequest\n\t2,  // 18: memos.api.v1.InstanceService.GetInstanceProfile:output_type -> memos.api.v1.InstanceProfile\n\t4,  // 19: memos.api.v1.InstanceService.GetInstanceSetting:output_type -> memos.api.v1.InstanceSetting\n\t4,  // 20: memos.api.v1.InstanceService.UpdateInstanceSetting:output_type -> memos.api.v1.InstanceSetting\n\t18, // [18:21] is the sub-list for method output_type\n\t15, // [15:18] is the sub-list for method input_type\n\t15, // [15:15] is the sub-list for extension type_name\n\t15, // [15:15] is the sub-list for extension extendee\n\t0,  // [0:15] is the sub-list for field type_name\n}\n\nfunc init() { file_api_v1_instance_service_proto_init() }\nfunc file_api_v1_instance_service_proto_init() {\n\tif File_api_v1_instance_service_proto != nil {\n\t\treturn\n\t}\n\tfile_api_v1_user_service_proto_init()\n\tfile_api_v1_instance_service_proto_msgTypes[2].OneofWrappers = []any{\n\t\t(*InstanceSetting_GeneralSetting_)(nil),\n\t\t(*InstanceSetting_StorageSetting_)(nil),\n\t\t(*InstanceSetting_MemoRelatedSetting_)(nil),\n\t\t(*InstanceSetting_TagsSetting_)(nil),\n\t\t(*InstanceSetting_NotificationSetting_)(nil),\n\t}\n\ttype x struct{}\n\tout := protoimpl.TypeBuilder{\n\t\tFile: protoimpl.DescBuilder{\n\t\t\tGoPackagePath: reflect.TypeOf(x{}).PkgPath(),\n\t\t\tRawDescriptor: unsafe.Slice(unsafe.StringData(file_api_v1_instance_service_proto_rawDesc), len(file_api_v1_instance_service_proto_rawDesc)),\n\t\t\tNumEnums:      2,\n\t\t\tNumMessages:   15,\n\t\t\tNumExtensions: 0,\n\t\t\tNumServices:   1,\n\t\t},\n\t\tGoTypes:           file_api_v1_instance_service_proto_goTypes,\n\t\tDependencyIndexes: file_api_v1_instance_service_proto_depIdxs,\n\t\tEnumInfos:         file_api_v1_instance_service_proto_enumTypes,\n\t\tMessageInfos:      file_api_v1_instance_service_proto_msgTypes,\n\t}.Build()\n\tFile_api_v1_instance_service_proto = out.File\n\tfile_api_v1_instance_service_proto_goTypes = nil\n\tfile_api_v1_instance_service_proto_depIdxs = nil\n}\n"
  },
  {
    "path": "proto/gen/api/v1/instance_service.pb.gw.go",
    "content": "// Code generated by protoc-gen-grpc-gateway. DO NOT EDIT.\n// source: api/v1/instance_service.proto\n\n/*\nPackage apiv1 is a reverse proxy.\n\nIt translates gRPC into RESTful JSON APIs.\n*/\npackage apiv1\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"io\"\n\t\"net/http\"\n\n\t\"github.com/grpc-ecosystem/grpc-gateway/v2/runtime\"\n\t\"github.com/grpc-ecosystem/grpc-gateway/v2/utilities\"\n\t\"google.golang.org/grpc\"\n\t\"google.golang.org/grpc/codes\"\n\t\"google.golang.org/grpc/grpclog\"\n\t\"google.golang.org/grpc/metadata\"\n\t\"google.golang.org/grpc/status\"\n\t\"google.golang.org/protobuf/proto\"\n)\n\n// Suppress \"imported and not used\" errors\nvar (\n\t_ codes.Code\n\t_ io.Reader\n\t_ status.Status\n\t_ = errors.New\n\t_ = runtime.String\n\t_ = utilities.NewDoubleArray\n\t_ = metadata.Join\n)\n\nfunc request_InstanceService_GetInstanceProfile_0(ctx context.Context, marshaler runtime.Marshaler, client InstanceServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq GetInstanceProfileRequest\n\t\tmetadata runtime.ServerMetadata\n\t)\n\tif req.Body != nil {\n\t\t_, _ = io.Copy(io.Discard, req.Body)\n\t}\n\tmsg, err := client.GetInstanceProfile(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))\n\treturn msg, metadata, err\n}\n\nfunc local_request_InstanceService_GetInstanceProfile_0(ctx context.Context, marshaler runtime.Marshaler, server InstanceServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq GetInstanceProfileRequest\n\t\tmetadata runtime.ServerMetadata\n\t)\n\tmsg, err := server.GetInstanceProfile(ctx, &protoReq)\n\treturn msg, metadata, err\n}\n\nfunc request_InstanceService_GetInstanceSetting_0(ctx context.Context, marshaler runtime.Marshaler, client InstanceServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq GetInstanceSettingRequest\n\t\tmetadata runtime.ServerMetadata\n\t\terr      error\n\t)\n\tif req.Body != nil {\n\t\t_, _ = io.Copy(io.Discard, req.Body)\n\t}\n\tval, ok := pathParams[\"name\"]\n\tif !ok {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"missing parameter %s\", \"name\")\n\t}\n\tprotoReq.Name, err = runtime.String(val)\n\tif err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"type mismatch, parameter: %s, error: %v\", \"name\", err)\n\t}\n\tmsg, err := client.GetInstanceSetting(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))\n\treturn msg, metadata, err\n}\n\nfunc local_request_InstanceService_GetInstanceSetting_0(ctx context.Context, marshaler runtime.Marshaler, server InstanceServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq GetInstanceSettingRequest\n\t\tmetadata runtime.ServerMetadata\n\t\terr      error\n\t)\n\tval, ok := pathParams[\"name\"]\n\tif !ok {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"missing parameter %s\", \"name\")\n\t}\n\tprotoReq.Name, err = runtime.String(val)\n\tif err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"type mismatch, parameter: %s, error: %v\", \"name\", err)\n\t}\n\tmsg, err := server.GetInstanceSetting(ctx, &protoReq)\n\treturn msg, metadata, err\n}\n\nvar filter_InstanceService_UpdateInstanceSetting_0 = &utilities.DoubleArray{Encoding: map[string]int{\"setting\": 0, \"name\": 1}, Base: []int{1, 2, 1, 0, 0}, Check: []int{0, 1, 2, 3, 2}}\n\nfunc request_InstanceService_UpdateInstanceSetting_0(ctx context.Context, marshaler runtime.Marshaler, client InstanceServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq UpdateInstanceSettingRequest\n\t\tmetadata runtime.ServerMetadata\n\t\terr      error\n\t)\n\tnewReader, berr := utilities.IOReaderFactory(req.Body)\n\tif berr != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", berr)\n\t}\n\tif err := marshaler.NewDecoder(newReader()).Decode(&protoReq.Setting); err != nil && !errors.Is(err, io.EOF) {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tif req.Body != nil {\n\t\t_, _ = io.Copy(io.Discard, req.Body)\n\t}\n\tif protoReq.UpdateMask == nil || len(protoReq.UpdateMask.GetPaths()) == 0 {\n\t\tif fieldMask, err := runtime.FieldMaskFromRequestBody(newReader(), protoReq.Setting); err != nil {\n\t\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t\t} else {\n\t\t\tprotoReq.UpdateMask = fieldMask\n\t\t}\n\t}\n\tval, ok := pathParams[\"setting.name\"]\n\tif !ok {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"missing parameter %s\", \"setting.name\")\n\t}\n\terr = runtime.PopulateFieldFromPath(&protoReq, \"setting.name\", val)\n\tif err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"type mismatch, parameter: %s, error: %v\", \"setting.name\", err)\n\t}\n\tif err := req.ParseForm(); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tif err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_InstanceService_UpdateInstanceSetting_0); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tmsg, err := client.UpdateInstanceSetting(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))\n\treturn msg, metadata, err\n}\n\nfunc local_request_InstanceService_UpdateInstanceSetting_0(ctx context.Context, marshaler runtime.Marshaler, server InstanceServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq UpdateInstanceSettingRequest\n\t\tmetadata runtime.ServerMetadata\n\t\terr      error\n\t)\n\tnewReader, berr := utilities.IOReaderFactory(req.Body)\n\tif berr != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", berr)\n\t}\n\tif err := marshaler.NewDecoder(newReader()).Decode(&protoReq.Setting); err != nil && !errors.Is(err, io.EOF) {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tif protoReq.UpdateMask == nil || len(protoReq.UpdateMask.GetPaths()) == 0 {\n\t\tif fieldMask, err := runtime.FieldMaskFromRequestBody(newReader(), protoReq.Setting); err != nil {\n\t\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t\t} else {\n\t\t\tprotoReq.UpdateMask = fieldMask\n\t\t}\n\t}\n\tval, ok := pathParams[\"setting.name\"]\n\tif !ok {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"missing parameter %s\", \"setting.name\")\n\t}\n\terr = runtime.PopulateFieldFromPath(&protoReq, \"setting.name\", val)\n\tif err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"type mismatch, parameter: %s, error: %v\", \"setting.name\", err)\n\t}\n\tif err := req.ParseForm(); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tif err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_InstanceService_UpdateInstanceSetting_0); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tmsg, err := server.UpdateInstanceSetting(ctx, &protoReq)\n\treturn msg, metadata, err\n}\n\n// RegisterInstanceServiceHandlerServer registers the http handlers for service InstanceService to \"mux\".\n// UnaryRPC     :call InstanceServiceServer directly.\n// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906.\n// Note that using this registration option will cause many gRPC library features to stop working. Consider using RegisterInstanceServiceHandlerFromEndpoint instead.\n// GRPC interceptors will not work for this type of registration. To use interceptors, you must use the \"runtime.WithMiddlewares\" option in the \"runtime.NewServeMux\" call.\nfunc RegisterInstanceServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux, server InstanceServiceServer) error {\n\tmux.Handle(http.MethodGet, pattern_InstanceService_GetInstanceProfile_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tvar stream runtime.ServerTransportStream\n\t\tctx = grpc.NewContextWithServerTransportStream(ctx, &stream)\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, \"/memos.api.v1.InstanceService/GetInstanceProfile\", runtime.WithHTTPPathPattern(\"/api/v1/instance/profile\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := local_request_InstanceService_GetInstanceProfile_0(annotatedContext, inboundMarshaler, server, req, pathParams)\n\t\tmd.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_InstanceService_GetInstanceProfile_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodGet, pattern_InstanceService_GetInstanceSetting_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tvar stream runtime.ServerTransportStream\n\t\tctx = grpc.NewContextWithServerTransportStream(ctx, &stream)\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, \"/memos.api.v1.InstanceService/GetInstanceSetting\", runtime.WithHTTPPathPattern(\"/api/v1/{name=instance/settings/*}\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := local_request_InstanceService_GetInstanceSetting_0(annotatedContext, inboundMarshaler, server, req, pathParams)\n\t\tmd.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_InstanceService_GetInstanceSetting_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodPatch, pattern_InstanceService_UpdateInstanceSetting_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tvar stream runtime.ServerTransportStream\n\t\tctx = grpc.NewContextWithServerTransportStream(ctx, &stream)\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, \"/memos.api.v1.InstanceService/UpdateInstanceSetting\", runtime.WithHTTPPathPattern(\"/api/v1/{setting.name=instance/settings/*}\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := local_request_InstanceService_UpdateInstanceSetting_0(annotatedContext, inboundMarshaler, server, req, pathParams)\n\t\tmd.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_InstanceService_UpdateInstanceSetting_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\n\treturn nil\n}\n\n// RegisterInstanceServiceHandlerFromEndpoint is same as RegisterInstanceServiceHandler but\n// automatically dials to \"endpoint\" and closes the connection when \"ctx\" gets done.\nfunc RegisterInstanceServiceHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) {\n\tconn, err := grpc.NewClient(endpoint, opts...)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer func() {\n\t\tif err != nil {\n\t\t\tif cerr := conn.Close(); cerr != nil {\n\t\t\t\tgrpclog.Errorf(\"Failed to close conn to %s: %v\", endpoint, cerr)\n\t\t\t}\n\t\t\treturn\n\t\t}\n\t\tgo func() {\n\t\t\t<-ctx.Done()\n\t\t\tif cerr := conn.Close(); cerr != nil {\n\t\t\t\tgrpclog.Errorf(\"Failed to close conn to %s: %v\", endpoint, cerr)\n\t\t\t}\n\t\t}()\n\t}()\n\treturn RegisterInstanceServiceHandler(ctx, mux, conn)\n}\n\n// RegisterInstanceServiceHandler registers the http handlers for service InstanceService to \"mux\".\n// The handlers forward requests to the grpc endpoint over \"conn\".\nfunc RegisterInstanceServiceHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error {\n\treturn RegisterInstanceServiceHandlerClient(ctx, mux, NewInstanceServiceClient(conn))\n}\n\n// RegisterInstanceServiceHandlerClient registers the http handlers for service InstanceService\n// to \"mux\". The handlers forward requests to the grpc endpoint over the given implementation of \"InstanceServiceClient\".\n// Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in \"InstanceServiceClient\"\n// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in\n// \"InstanceServiceClient\" to call the correct interceptors. This client ignores the HTTP middlewares.\nfunc RegisterInstanceServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux, client InstanceServiceClient) error {\n\tmux.Handle(http.MethodGet, pattern_InstanceService_GetInstanceProfile_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateContext(ctx, mux, req, \"/memos.api.v1.InstanceService/GetInstanceProfile\", runtime.WithHTTPPathPattern(\"/api/v1/instance/profile\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := request_InstanceService_GetInstanceProfile_0(annotatedContext, inboundMarshaler, client, req, pathParams)\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_InstanceService_GetInstanceProfile_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodGet, pattern_InstanceService_GetInstanceSetting_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateContext(ctx, mux, req, \"/memos.api.v1.InstanceService/GetInstanceSetting\", runtime.WithHTTPPathPattern(\"/api/v1/{name=instance/settings/*}\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := request_InstanceService_GetInstanceSetting_0(annotatedContext, inboundMarshaler, client, req, pathParams)\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_InstanceService_GetInstanceSetting_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodPatch, pattern_InstanceService_UpdateInstanceSetting_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateContext(ctx, mux, req, \"/memos.api.v1.InstanceService/UpdateInstanceSetting\", runtime.WithHTTPPathPattern(\"/api/v1/{setting.name=instance/settings/*}\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := request_InstanceService_UpdateInstanceSetting_0(annotatedContext, inboundMarshaler, client, req, pathParams)\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_InstanceService_UpdateInstanceSetting_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\treturn nil\n}\n\nvar (\n\tpattern_InstanceService_GetInstanceProfile_0    = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{\"api\", \"v1\", \"instance\", \"profile\"}, \"\"))\n\tpattern_InstanceService_GetInstanceSetting_0    = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 1, 0, 4, 3, 5, 4}, []string{\"api\", \"v1\", \"instance\", \"settings\", \"name\"}, \"\"))\n\tpattern_InstanceService_UpdateInstanceSetting_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 1, 0, 4, 3, 5, 4}, []string{\"api\", \"v1\", \"instance\", \"settings\", \"setting.name\"}, \"\"))\n)\n\nvar (\n\tforward_InstanceService_GetInstanceProfile_0    = runtime.ForwardResponseMessage\n\tforward_InstanceService_GetInstanceSetting_0    = runtime.ForwardResponseMessage\n\tforward_InstanceService_UpdateInstanceSetting_0 = runtime.ForwardResponseMessage\n)\n"
  },
  {
    "path": "proto/gen/api/v1/instance_service_grpc.pb.go",
    "content": "// Code generated by protoc-gen-go-grpc. DO NOT EDIT.\n// versions:\n// - protoc-gen-go-grpc v1.6.1\n// - protoc             (unknown)\n// source: api/v1/instance_service.proto\n\npackage apiv1\n\nimport (\n\tcontext \"context\"\n\tgrpc \"google.golang.org/grpc\"\n\tcodes \"google.golang.org/grpc/codes\"\n\tstatus \"google.golang.org/grpc/status\"\n)\n\n// This is a compile-time assertion to ensure that this generated file\n// is compatible with the grpc package it is being compiled against.\n// Requires gRPC-Go v1.64.0 or later.\nconst _ = grpc.SupportPackageIsVersion9\n\nconst (\n\tInstanceService_GetInstanceProfile_FullMethodName    = \"/memos.api.v1.InstanceService/GetInstanceProfile\"\n\tInstanceService_GetInstanceSetting_FullMethodName    = \"/memos.api.v1.InstanceService/GetInstanceSetting\"\n\tInstanceService_UpdateInstanceSetting_FullMethodName = \"/memos.api.v1.InstanceService/UpdateInstanceSetting\"\n)\n\n// InstanceServiceClient is the client API for InstanceService service.\n//\n// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.\ntype InstanceServiceClient interface {\n\t// Gets the instance profile.\n\tGetInstanceProfile(ctx context.Context, in *GetInstanceProfileRequest, opts ...grpc.CallOption) (*InstanceProfile, error)\n\t// Gets an instance setting.\n\tGetInstanceSetting(ctx context.Context, in *GetInstanceSettingRequest, opts ...grpc.CallOption) (*InstanceSetting, error)\n\t// Updates an instance setting.\n\tUpdateInstanceSetting(ctx context.Context, in *UpdateInstanceSettingRequest, opts ...grpc.CallOption) (*InstanceSetting, error)\n}\n\ntype instanceServiceClient struct {\n\tcc grpc.ClientConnInterface\n}\n\nfunc NewInstanceServiceClient(cc grpc.ClientConnInterface) InstanceServiceClient {\n\treturn &instanceServiceClient{cc}\n}\n\nfunc (c *instanceServiceClient) GetInstanceProfile(ctx context.Context, in *GetInstanceProfileRequest, opts ...grpc.CallOption) (*InstanceProfile, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(InstanceProfile)\n\terr := c.cc.Invoke(ctx, InstanceService_GetInstanceProfile_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *instanceServiceClient) GetInstanceSetting(ctx context.Context, in *GetInstanceSettingRequest, opts ...grpc.CallOption) (*InstanceSetting, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(InstanceSetting)\n\terr := c.cc.Invoke(ctx, InstanceService_GetInstanceSetting_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *instanceServiceClient) UpdateInstanceSetting(ctx context.Context, in *UpdateInstanceSettingRequest, opts ...grpc.CallOption) (*InstanceSetting, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(InstanceSetting)\n\terr := c.cc.Invoke(ctx, InstanceService_UpdateInstanceSetting_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\n// InstanceServiceServer is the server API for InstanceService service.\n// All implementations must embed UnimplementedInstanceServiceServer\n// for forward compatibility.\ntype InstanceServiceServer interface {\n\t// Gets the instance profile.\n\tGetInstanceProfile(context.Context, *GetInstanceProfileRequest) (*InstanceProfile, error)\n\t// Gets an instance setting.\n\tGetInstanceSetting(context.Context, *GetInstanceSettingRequest) (*InstanceSetting, error)\n\t// Updates an instance setting.\n\tUpdateInstanceSetting(context.Context, *UpdateInstanceSettingRequest) (*InstanceSetting, error)\n\tmustEmbedUnimplementedInstanceServiceServer()\n}\n\n// UnimplementedInstanceServiceServer must be embedded to have\n// forward compatible implementations.\n//\n// NOTE: this should be embedded by value instead of pointer to avoid a nil\n// pointer dereference when methods are called.\ntype UnimplementedInstanceServiceServer struct{}\n\nfunc (UnimplementedInstanceServiceServer) GetInstanceProfile(context.Context, *GetInstanceProfileRequest) (*InstanceProfile, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method GetInstanceProfile not implemented\")\n}\nfunc (UnimplementedInstanceServiceServer) GetInstanceSetting(context.Context, *GetInstanceSettingRequest) (*InstanceSetting, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method GetInstanceSetting not implemented\")\n}\nfunc (UnimplementedInstanceServiceServer) UpdateInstanceSetting(context.Context, *UpdateInstanceSettingRequest) (*InstanceSetting, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method UpdateInstanceSetting not implemented\")\n}\nfunc (UnimplementedInstanceServiceServer) mustEmbedUnimplementedInstanceServiceServer() {}\nfunc (UnimplementedInstanceServiceServer) testEmbeddedByValue()                         {}\n\n// UnsafeInstanceServiceServer may be embedded to opt out of forward compatibility for this service.\n// Use of this interface is not recommended, as added methods to InstanceServiceServer will\n// result in compilation errors.\ntype UnsafeInstanceServiceServer interface {\n\tmustEmbedUnimplementedInstanceServiceServer()\n}\n\nfunc RegisterInstanceServiceServer(s grpc.ServiceRegistrar, srv InstanceServiceServer) {\n\t// If the following call panics, it indicates UnimplementedInstanceServiceServer was\n\t// embedded by pointer and is nil.  This will cause panics if an\n\t// unimplemented method is ever invoked, so we test this at initialization\n\t// time to prevent it from happening at runtime later due to I/O.\n\tif t, ok := srv.(interface{ testEmbeddedByValue() }); ok {\n\t\tt.testEmbeddedByValue()\n\t}\n\ts.RegisterService(&InstanceService_ServiceDesc, srv)\n}\n\nfunc _InstanceService_GetInstanceProfile_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(GetInstanceProfileRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(InstanceServiceServer).GetInstanceProfile(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: InstanceService_GetInstanceProfile_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(InstanceServiceServer).GetInstanceProfile(ctx, req.(*GetInstanceProfileRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _InstanceService_GetInstanceSetting_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(GetInstanceSettingRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(InstanceServiceServer).GetInstanceSetting(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: InstanceService_GetInstanceSetting_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(InstanceServiceServer).GetInstanceSetting(ctx, req.(*GetInstanceSettingRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _InstanceService_UpdateInstanceSetting_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(UpdateInstanceSettingRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(InstanceServiceServer).UpdateInstanceSetting(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: InstanceService_UpdateInstanceSetting_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(InstanceServiceServer).UpdateInstanceSetting(ctx, req.(*UpdateInstanceSettingRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\n// InstanceService_ServiceDesc is the grpc.ServiceDesc for InstanceService service.\n// It's only intended for direct use with grpc.RegisterService,\n// and not to be introspected or modified (even as a copy)\nvar InstanceService_ServiceDesc = grpc.ServiceDesc{\n\tServiceName: \"memos.api.v1.InstanceService\",\n\tHandlerType: (*InstanceServiceServer)(nil),\n\tMethods: []grpc.MethodDesc{\n\t\t{\n\t\t\tMethodName: \"GetInstanceProfile\",\n\t\t\tHandler:    _InstanceService_GetInstanceProfile_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"GetInstanceSetting\",\n\t\t\tHandler:    _InstanceService_GetInstanceSetting_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"UpdateInstanceSetting\",\n\t\t\tHandler:    _InstanceService_UpdateInstanceSetting_Handler,\n\t\t},\n\t},\n\tStreams:  []grpc.StreamDesc{},\n\tMetadata: \"api/v1/instance_service.proto\",\n}\n"
  },
  {
    "path": "proto/gen/api/v1/memo_service.pb.go",
    "content": "// Code generated by protoc-gen-go. DO NOT EDIT.\n// versions:\n// \tprotoc-gen-go v1.36.11\n// \tprotoc        (unknown)\n// source: api/v1/memo_service.proto\n\npackage apiv1\n\nimport (\n\t_ \"google.golang.org/genproto/googleapis/api/annotations\"\n\tprotoreflect \"google.golang.org/protobuf/reflect/protoreflect\"\n\tprotoimpl \"google.golang.org/protobuf/runtime/protoimpl\"\n\temptypb \"google.golang.org/protobuf/types/known/emptypb\"\n\tfieldmaskpb \"google.golang.org/protobuf/types/known/fieldmaskpb\"\n\ttimestamppb \"google.golang.org/protobuf/types/known/timestamppb\"\n\treflect \"reflect\"\n\tsync \"sync\"\n\tunsafe \"unsafe\"\n)\n\nconst (\n\t// Verify that this generated code is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)\n\t// Verify that runtime/protoimpl is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)\n)\n\ntype Visibility int32\n\nconst (\n\tVisibility_VISIBILITY_UNSPECIFIED Visibility = 0\n\tVisibility_PRIVATE                Visibility = 1\n\tVisibility_PROTECTED              Visibility = 2\n\tVisibility_PUBLIC                 Visibility = 3\n)\n\n// Enum value maps for Visibility.\nvar (\n\tVisibility_name = map[int32]string{\n\t\t0: \"VISIBILITY_UNSPECIFIED\",\n\t\t1: \"PRIVATE\",\n\t\t2: \"PROTECTED\",\n\t\t3: \"PUBLIC\",\n\t}\n\tVisibility_value = map[string]int32{\n\t\t\"VISIBILITY_UNSPECIFIED\": 0,\n\t\t\"PRIVATE\":                1,\n\t\t\"PROTECTED\":              2,\n\t\t\"PUBLIC\":                 3,\n\t}\n)\n\nfunc (x Visibility) Enum() *Visibility {\n\tp := new(Visibility)\n\t*p = x\n\treturn p\n}\n\nfunc (x Visibility) String() string {\n\treturn protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))\n}\n\nfunc (Visibility) Descriptor() protoreflect.EnumDescriptor {\n\treturn file_api_v1_memo_service_proto_enumTypes[0].Descriptor()\n}\n\nfunc (Visibility) Type() protoreflect.EnumType {\n\treturn &file_api_v1_memo_service_proto_enumTypes[0]\n}\n\nfunc (x Visibility) Number() protoreflect.EnumNumber {\n\treturn protoreflect.EnumNumber(x)\n}\n\n// Deprecated: Use Visibility.Descriptor instead.\nfunc (Visibility) EnumDescriptor() ([]byte, []int) {\n\treturn file_api_v1_memo_service_proto_rawDescGZIP(), []int{0}\n}\n\n// The type of the relation.\ntype MemoRelation_Type int32\n\nconst (\n\tMemoRelation_TYPE_UNSPECIFIED MemoRelation_Type = 0\n\tMemoRelation_REFERENCE        MemoRelation_Type = 1\n\tMemoRelation_COMMENT          MemoRelation_Type = 2\n)\n\n// Enum value maps for MemoRelation_Type.\nvar (\n\tMemoRelation_Type_name = map[int32]string{\n\t\t0: \"TYPE_UNSPECIFIED\",\n\t\t1: \"REFERENCE\",\n\t\t2: \"COMMENT\",\n\t}\n\tMemoRelation_Type_value = map[string]int32{\n\t\t\"TYPE_UNSPECIFIED\": 0,\n\t\t\"REFERENCE\":        1,\n\t\t\"COMMENT\":          2,\n\t}\n)\n\nfunc (x MemoRelation_Type) Enum() *MemoRelation_Type {\n\tp := new(MemoRelation_Type)\n\t*p = x\n\treturn p\n}\n\nfunc (x MemoRelation_Type) String() string {\n\treturn protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))\n}\n\nfunc (MemoRelation_Type) Descriptor() protoreflect.EnumDescriptor {\n\treturn file_api_v1_memo_service_proto_enumTypes[1].Descriptor()\n}\n\nfunc (MemoRelation_Type) Type() protoreflect.EnumType {\n\treturn &file_api_v1_memo_service_proto_enumTypes[1]\n}\n\nfunc (x MemoRelation_Type) Number() protoreflect.EnumNumber {\n\treturn protoreflect.EnumNumber(x)\n}\n\n// Deprecated: Use MemoRelation_Type.Descriptor instead.\nfunc (MemoRelation_Type) EnumDescriptor() ([]byte, []int) {\n\treturn file_api_v1_memo_service_proto_rawDescGZIP(), []int{12, 0}\n}\n\ntype Reaction struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// The resource name of the reaction.\n\t// Format: memos/{memo}/reactions/{reaction}\n\tName string `protobuf:\"bytes,1,opt,name=name,proto3\" json:\"name,omitempty\"`\n\t// The resource name of the creator.\n\t// Format: users/{user}\n\tCreator string `protobuf:\"bytes,2,opt,name=creator,proto3\" json:\"creator,omitempty\"`\n\t// The resource name of the content.\n\t// For memo reactions, this should be the memo's resource name.\n\t// Format: memos/{memo}\n\tContentId string `protobuf:\"bytes,3,opt,name=content_id,json=contentId,proto3\" json:\"content_id,omitempty\"`\n\t// Required. The type of reaction (e.g., \"👍\", \"❤️\", \"😄\").\n\tReactionType string `protobuf:\"bytes,4,opt,name=reaction_type,json=reactionType,proto3\" json:\"reaction_type,omitempty\"`\n\t// Output only. The creation timestamp.\n\tCreateTime    *timestamppb.Timestamp `protobuf:\"bytes,5,opt,name=create_time,json=createTime,proto3\" json:\"create_time,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *Reaction) Reset() {\n\t*x = Reaction{}\n\tmi := &file_api_v1_memo_service_proto_msgTypes[0]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *Reaction) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*Reaction) ProtoMessage() {}\n\nfunc (x *Reaction) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_memo_service_proto_msgTypes[0]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use Reaction.ProtoReflect.Descriptor instead.\nfunc (*Reaction) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_memo_service_proto_rawDescGZIP(), []int{0}\n}\n\nfunc (x *Reaction) GetName() string {\n\tif x != nil {\n\t\treturn x.Name\n\t}\n\treturn \"\"\n}\n\nfunc (x *Reaction) GetCreator() string {\n\tif x != nil {\n\t\treturn x.Creator\n\t}\n\treturn \"\"\n}\n\nfunc (x *Reaction) GetContentId() string {\n\tif x != nil {\n\t\treturn x.ContentId\n\t}\n\treturn \"\"\n}\n\nfunc (x *Reaction) GetReactionType() string {\n\tif x != nil {\n\t\treturn x.ReactionType\n\t}\n\treturn \"\"\n}\n\nfunc (x *Reaction) GetCreateTime() *timestamppb.Timestamp {\n\tif x != nil {\n\t\treturn x.CreateTime\n\t}\n\treturn nil\n}\n\ntype Memo struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// The resource name of the memo.\n\t// Format: memos/{memo}, memo is the user defined id or uuid.\n\tName string `protobuf:\"bytes,1,opt,name=name,proto3\" json:\"name,omitempty\"`\n\t// The state of the memo.\n\tState State `protobuf:\"varint,2,opt,name=state,proto3,enum=memos.api.v1.State\" json:\"state,omitempty\"`\n\t// The name of the creator.\n\t// Format: users/{user}\n\tCreator string `protobuf:\"bytes,3,opt,name=creator,proto3\" json:\"creator,omitempty\"`\n\t// The creation timestamp.\n\t// If not set on creation, the server will set it to the current time.\n\tCreateTime *timestamppb.Timestamp `protobuf:\"bytes,4,opt,name=create_time,json=createTime,proto3\" json:\"create_time,omitempty\"`\n\t// The last update timestamp.\n\t// If not set on creation, the server will set it to the current time.\n\tUpdateTime *timestamppb.Timestamp `protobuf:\"bytes,5,opt,name=update_time,json=updateTime,proto3\" json:\"update_time,omitempty\"`\n\t// The display timestamp of the memo.\n\tDisplayTime *timestamppb.Timestamp `protobuf:\"bytes,6,opt,name=display_time,json=displayTime,proto3\" json:\"display_time,omitempty\"`\n\t// Required. The content of the memo in Markdown format.\n\tContent string `protobuf:\"bytes,7,opt,name=content,proto3\" json:\"content,omitempty\"`\n\t// The visibility of the memo.\n\tVisibility Visibility `protobuf:\"varint,9,opt,name=visibility,proto3,enum=memos.api.v1.Visibility\" json:\"visibility,omitempty\"`\n\t// Output only. The tags extracted from the content.\n\tTags []string `protobuf:\"bytes,10,rep,name=tags,proto3\" json:\"tags,omitempty\"`\n\t// Whether the memo is pinned.\n\tPinned bool `protobuf:\"varint,11,opt,name=pinned,proto3\" json:\"pinned,omitempty\"`\n\t// Optional. The attachments of the memo.\n\tAttachments []*Attachment `protobuf:\"bytes,12,rep,name=attachments,proto3\" json:\"attachments,omitempty\"`\n\t// Optional. The relations of the memo.\n\tRelations []*MemoRelation `protobuf:\"bytes,13,rep,name=relations,proto3\" json:\"relations,omitempty\"`\n\t// Output only. The reactions to the memo.\n\tReactions []*Reaction `protobuf:\"bytes,14,rep,name=reactions,proto3\" json:\"reactions,omitempty\"`\n\t// Output only. The computed properties of the memo.\n\tProperty *Memo_Property `protobuf:\"bytes,15,opt,name=property,proto3\" json:\"property,omitempty\"`\n\t// Output only. The name of the parent memo.\n\t// Format: memos/{memo}\n\tParent *string `protobuf:\"bytes,16,opt,name=parent,proto3,oneof\" json:\"parent,omitempty\"`\n\t// Output only. The snippet of the memo content. Plain text only.\n\tSnippet string `protobuf:\"bytes,17,opt,name=snippet,proto3\" json:\"snippet,omitempty\"`\n\t// Optional. The location of the memo.\n\tLocation      *Location `protobuf:\"bytes,18,opt,name=location,proto3,oneof\" json:\"location,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *Memo) Reset() {\n\t*x = Memo{}\n\tmi := &file_api_v1_memo_service_proto_msgTypes[1]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *Memo) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*Memo) ProtoMessage() {}\n\nfunc (x *Memo) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_memo_service_proto_msgTypes[1]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use Memo.ProtoReflect.Descriptor instead.\nfunc (*Memo) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_memo_service_proto_rawDescGZIP(), []int{1}\n}\n\nfunc (x *Memo) GetName() string {\n\tif x != nil {\n\t\treturn x.Name\n\t}\n\treturn \"\"\n}\n\nfunc (x *Memo) GetState() State {\n\tif x != nil {\n\t\treturn x.State\n\t}\n\treturn State_STATE_UNSPECIFIED\n}\n\nfunc (x *Memo) GetCreator() string {\n\tif x != nil {\n\t\treturn x.Creator\n\t}\n\treturn \"\"\n}\n\nfunc (x *Memo) GetCreateTime() *timestamppb.Timestamp {\n\tif x != nil {\n\t\treturn x.CreateTime\n\t}\n\treturn nil\n}\n\nfunc (x *Memo) GetUpdateTime() *timestamppb.Timestamp {\n\tif x != nil {\n\t\treturn x.UpdateTime\n\t}\n\treturn nil\n}\n\nfunc (x *Memo) GetDisplayTime() *timestamppb.Timestamp {\n\tif x != nil {\n\t\treturn x.DisplayTime\n\t}\n\treturn nil\n}\n\nfunc (x *Memo) GetContent() string {\n\tif x != nil {\n\t\treturn x.Content\n\t}\n\treturn \"\"\n}\n\nfunc (x *Memo) GetVisibility() Visibility {\n\tif x != nil {\n\t\treturn x.Visibility\n\t}\n\treturn Visibility_VISIBILITY_UNSPECIFIED\n}\n\nfunc (x *Memo) GetTags() []string {\n\tif x != nil {\n\t\treturn x.Tags\n\t}\n\treturn nil\n}\n\nfunc (x *Memo) GetPinned() bool {\n\tif x != nil {\n\t\treturn x.Pinned\n\t}\n\treturn false\n}\n\nfunc (x *Memo) GetAttachments() []*Attachment {\n\tif x != nil {\n\t\treturn x.Attachments\n\t}\n\treturn nil\n}\n\nfunc (x *Memo) GetRelations() []*MemoRelation {\n\tif x != nil {\n\t\treturn x.Relations\n\t}\n\treturn nil\n}\n\nfunc (x *Memo) GetReactions() []*Reaction {\n\tif x != nil {\n\t\treturn x.Reactions\n\t}\n\treturn nil\n}\n\nfunc (x *Memo) GetProperty() *Memo_Property {\n\tif x != nil {\n\t\treturn x.Property\n\t}\n\treturn nil\n}\n\nfunc (x *Memo) GetParent() string {\n\tif x != nil && x.Parent != nil {\n\t\treturn *x.Parent\n\t}\n\treturn \"\"\n}\n\nfunc (x *Memo) GetSnippet() string {\n\tif x != nil {\n\t\treturn x.Snippet\n\t}\n\treturn \"\"\n}\n\nfunc (x *Memo) GetLocation() *Location {\n\tif x != nil {\n\t\treturn x.Location\n\t}\n\treturn nil\n}\n\ntype Location struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// A placeholder text for the location.\n\tPlaceholder string `protobuf:\"bytes,1,opt,name=placeholder,proto3\" json:\"placeholder,omitempty\"`\n\t// The latitude of the location.\n\tLatitude float64 `protobuf:\"fixed64,2,opt,name=latitude,proto3\" json:\"latitude,omitempty\"`\n\t// The longitude of the location.\n\tLongitude     float64 `protobuf:\"fixed64,3,opt,name=longitude,proto3\" json:\"longitude,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *Location) Reset() {\n\t*x = Location{}\n\tmi := &file_api_v1_memo_service_proto_msgTypes[2]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *Location) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*Location) ProtoMessage() {}\n\nfunc (x *Location) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_memo_service_proto_msgTypes[2]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use Location.ProtoReflect.Descriptor instead.\nfunc (*Location) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_memo_service_proto_rawDescGZIP(), []int{2}\n}\n\nfunc (x *Location) GetPlaceholder() string {\n\tif x != nil {\n\t\treturn x.Placeholder\n\t}\n\treturn \"\"\n}\n\nfunc (x *Location) GetLatitude() float64 {\n\tif x != nil {\n\t\treturn x.Latitude\n\t}\n\treturn 0\n}\n\nfunc (x *Location) GetLongitude() float64 {\n\tif x != nil {\n\t\treturn x.Longitude\n\t}\n\treturn 0\n}\n\ntype CreateMemoRequest struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// Required. The memo to create.\n\tMemo *Memo `protobuf:\"bytes,1,opt,name=memo,proto3\" json:\"memo,omitempty\"`\n\t// Optional. The memo ID to use for this memo.\n\t// If empty, a unique ID will be generated.\n\tMemoId        string `protobuf:\"bytes,2,opt,name=memo_id,json=memoId,proto3\" json:\"memo_id,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *CreateMemoRequest) Reset() {\n\t*x = CreateMemoRequest{}\n\tmi := &file_api_v1_memo_service_proto_msgTypes[3]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *CreateMemoRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*CreateMemoRequest) ProtoMessage() {}\n\nfunc (x *CreateMemoRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_memo_service_proto_msgTypes[3]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use CreateMemoRequest.ProtoReflect.Descriptor instead.\nfunc (*CreateMemoRequest) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_memo_service_proto_rawDescGZIP(), []int{3}\n}\n\nfunc (x *CreateMemoRequest) GetMemo() *Memo {\n\tif x != nil {\n\t\treturn x.Memo\n\t}\n\treturn nil\n}\n\nfunc (x *CreateMemoRequest) GetMemoId() string {\n\tif x != nil {\n\t\treturn x.MemoId\n\t}\n\treturn \"\"\n}\n\ntype ListMemosRequest struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// Optional. The maximum number of memos to return.\n\t// The service may return fewer than this value.\n\t// If unspecified, at most 50 memos will be returned.\n\t// The maximum value is 1000; values above 1000 will be coerced to 1000.\n\tPageSize int32 `protobuf:\"varint,1,opt,name=page_size,json=pageSize,proto3\" json:\"page_size,omitempty\"`\n\t// Optional. A page token, received from a previous `ListMemos` call.\n\t// Provide this to retrieve the subsequent page.\n\tPageToken string `protobuf:\"bytes,2,opt,name=page_token,json=pageToken,proto3\" json:\"page_token,omitempty\"`\n\t// Optional. The state of the memos to list.\n\t// Default to `NORMAL`. Set to `ARCHIVED` to list archived memos.\n\tState State `protobuf:\"varint,3,opt,name=state,proto3,enum=memos.api.v1.State\" json:\"state,omitempty\"`\n\t// Optional. The order to sort results by.\n\t// Default to \"display_time desc\".\n\t// Supports comma-separated list of fields following AIP-132.\n\t// Example: \"pinned desc, display_time desc\" or \"create_time asc\"\n\t// Supported fields: pinned, display_time, create_time, update_time, name\n\tOrderBy string `protobuf:\"bytes,4,opt,name=order_by,json=orderBy,proto3\" json:\"order_by,omitempty\"`\n\t// Optional. Filter to apply to the list results.\n\t// Filter is a CEL expression to filter memos.\n\t// Refer to `Shortcut.filter`.\n\tFilter string `protobuf:\"bytes,5,opt,name=filter,proto3\" json:\"filter,omitempty\"`\n\t// Optional. If true, show deleted memos in the response.\n\tShowDeleted   bool `protobuf:\"varint,6,opt,name=show_deleted,json=showDeleted,proto3\" json:\"show_deleted,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *ListMemosRequest) Reset() {\n\t*x = ListMemosRequest{}\n\tmi := &file_api_v1_memo_service_proto_msgTypes[4]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *ListMemosRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*ListMemosRequest) ProtoMessage() {}\n\nfunc (x *ListMemosRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_memo_service_proto_msgTypes[4]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use ListMemosRequest.ProtoReflect.Descriptor instead.\nfunc (*ListMemosRequest) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_memo_service_proto_rawDescGZIP(), []int{4}\n}\n\nfunc (x *ListMemosRequest) GetPageSize() int32 {\n\tif x != nil {\n\t\treturn x.PageSize\n\t}\n\treturn 0\n}\n\nfunc (x *ListMemosRequest) GetPageToken() string {\n\tif x != nil {\n\t\treturn x.PageToken\n\t}\n\treturn \"\"\n}\n\nfunc (x *ListMemosRequest) GetState() State {\n\tif x != nil {\n\t\treturn x.State\n\t}\n\treturn State_STATE_UNSPECIFIED\n}\n\nfunc (x *ListMemosRequest) GetOrderBy() string {\n\tif x != nil {\n\t\treturn x.OrderBy\n\t}\n\treturn \"\"\n}\n\nfunc (x *ListMemosRequest) GetFilter() string {\n\tif x != nil {\n\t\treturn x.Filter\n\t}\n\treturn \"\"\n}\n\nfunc (x *ListMemosRequest) GetShowDeleted() bool {\n\tif x != nil {\n\t\treturn x.ShowDeleted\n\t}\n\treturn false\n}\n\ntype ListMemosResponse struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// The list of memos.\n\tMemos []*Memo `protobuf:\"bytes,1,rep,name=memos,proto3\" json:\"memos,omitempty\"`\n\t// A token that can be sent as `page_token` to retrieve the next page.\n\t// If this field is omitted, there are no subsequent pages.\n\tNextPageToken string `protobuf:\"bytes,2,opt,name=next_page_token,json=nextPageToken,proto3\" json:\"next_page_token,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *ListMemosResponse) Reset() {\n\t*x = ListMemosResponse{}\n\tmi := &file_api_v1_memo_service_proto_msgTypes[5]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *ListMemosResponse) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*ListMemosResponse) ProtoMessage() {}\n\nfunc (x *ListMemosResponse) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_memo_service_proto_msgTypes[5]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use ListMemosResponse.ProtoReflect.Descriptor instead.\nfunc (*ListMemosResponse) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_memo_service_proto_rawDescGZIP(), []int{5}\n}\n\nfunc (x *ListMemosResponse) GetMemos() []*Memo {\n\tif x != nil {\n\t\treturn x.Memos\n\t}\n\treturn nil\n}\n\nfunc (x *ListMemosResponse) GetNextPageToken() string {\n\tif x != nil {\n\t\treturn x.NextPageToken\n\t}\n\treturn \"\"\n}\n\ntype GetMemoRequest struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// Required. The resource name of the memo.\n\t// Format: memos/{memo}\n\tName          string `protobuf:\"bytes,1,opt,name=name,proto3\" json:\"name,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *GetMemoRequest) Reset() {\n\t*x = GetMemoRequest{}\n\tmi := &file_api_v1_memo_service_proto_msgTypes[6]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *GetMemoRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*GetMemoRequest) ProtoMessage() {}\n\nfunc (x *GetMemoRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_memo_service_proto_msgTypes[6]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use GetMemoRequest.ProtoReflect.Descriptor instead.\nfunc (*GetMemoRequest) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_memo_service_proto_rawDescGZIP(), []int{6}\n}\n\nfunc (x *GetMemoRequest) GetName() string {\n\tif x != nil {\n\t\treturn x.Name\n\t}\n\treturn \"\"\n}\n\ntype UpdateMemoRequest struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// Required. The memo to update.\n\t// The `name` field is required.\n\tMemo *Memo `protobuf:\"bytes,1,opt,name=memo,proto3\" json:\"memo,omitempty\"`\n\t// Required. The list of fields to update.\n\tUpdateMask    *fieldmaskpb.FieldMask `protobuf:\"bytes,2,opt,name=update_mask,json=updateMask,proto3\" json:\"update_mask,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *UpdateMemoRequest) Reset() {\n\t*x = UpdateMemoRequest{}\n\tmi := &file_api_v1_memo_service_proto_msgTypes[7]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *UpdateMemoRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*UpdateMemoRequest) ProtoMessage() {}\n\nfunc (x *UpdateMemoRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_memo_service_proto_msgTypes[7]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use UpdateMemoRequest.ProtoReflect.Descriptor instead.\nfunc (*UpdateMemoRequest) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_memo_service_proto_rawDescGZIP(), []int{7}\n}\n\nfunc (x *UpdateMemoRequest) GetMemo() *Memo {\n\tif x != nil {\n\t\treturn x.Memo\n\t}\n\treturn nil\n}\n\nfunc (x *UpdateMemoRequest) GetUpdateMask() *fieldmaskpb.FieldMask {\n\tif x != nil {\n\t\treturn x.UpdateMask\n\t}\n\treturn nil\n}\n\ntype DeleteMemoRequest struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// Required. The resource name of the memo to delete.\n\t// Format: memos/{memo}\n\tName string `protobuf:\"bytes,1,opt,name=name,proto3\" json:\"name,omitempty\"`\n\t// Optional. If set to true, the memo will be deleted even if it has associated data.\n\tForce         bool `protobuf:\"varint,2,opt,name=force,proto3\" json:\"force,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *DeleteMemoRequest) Reset() {\n\t*x = DeleteMemoRequest{}\n\tmi := &file_api_v1_memo_service_proto_msgTypes[8]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *DeleteMemoRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*DeleteMemoRequest) ProtoMessage() {}\n\nfunc (x *DeleteMemoRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_memo_service_proto_msgTypes[8]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use DeleteMemoRequest.ProtoReflect.Descriptor instead.\nfunc (*DeleteMemoRequest) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_memo_service_proto_rawDescGZIP(), []int{8}\n}\n\nfunc (x *DeleteMemoRequest) GetName() string {\n\tif x != nil {\n\t\treturn x.Name\n\t}\n\treturn \"\"\n}\n\nfunc (x *DeleteMemoRequest) GetForce() bool {\n\tif x != nil {\n\t\treturn x.Force\n\t}\n\treturn false\n}\n\ntype SetMemoAttachmentsRequest struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// Required. The resource name of the memo.\n\t// Format: memos/{memo}\n\tName string `protobuf:\"bytes,1,opt,name=name,proto3\" json:\"name,omitempty\"`\n\t// Required. The attachments to set for the memo.\n\tAttachments   []*Attachment `protobuf:\"bytes,2,rep,name=attachments,proto3\" json:\"attachments,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *SetMemoAttachmentsRequest) Reset() {\n\t*x = SetMemoAttachmentsRequest{}\n\tmi := &file_api_v1_memo_service_proto_msgTypes[9]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *SetMemoAttachmentsRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*SetMemoAttachmentsRequest) ProtoMessage() {}\n\nfunc (x *SetMemoAttachmentsRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_memo_service_proto_msgTypes[9]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use SetMemoAttachmentsRequest.ProtoReflect.Descriptor instead.\nfunc (*SetMemoAttachmentsRequest) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_memo_service_proto_rawDescGZIP(), []int{9}\n}\n\nfunc (x *SetMemoAttachmentsRequest) GetName() string {\n\tif x != nil {\n\t\treturn x.Name\n\t}\n\treturn \"\"\n}\n\nfunc (x *SetMemoAttachmentsRequest) GetAttachments() []*Attachment {\n\tif x != nil {\n\t\treturn x.Attachments\n\t}\n\treturn nil\n}\n\ntype ListMemoAttachmentsRequest struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// Required. The resource name of the memo.\n\t// Format: memos/{memo}\n\tName string `protobuf:\"bytes,1,opt,name=name,proto3\" json:\"name,omitempty\"`\n\t// Optional. The maximum number of attachments to return.\n\tPageSize int32 `protobuf:\"varint,2,opt,name=page_size,json=pageSize,proto3\" json:\"page_size,omitempty\"`\n\t// Optional. A page token for pagination.\n\tPageToken     string `protobuf:\"bytes,3,opt,name=page_token,json=pageToken,proto3\" json:\"page_token,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *ListMemoAttachmentsRequest) Reset() {\n\t*x = ListMemoAttachmentsRequest{}\n\tmi := &file_api_v1_memo_service_proto_msgTypes[10]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *ListMemoAttachmentsRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*ListMemoAttachmentsRequest) ProtoMessage() {}\n\nfunc (x *ListMemoAttachmentsRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_memo_service_proto_msgTypes[10]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use ListMemoAttachmentsRequest.ProtoReflect.Descriptor instead.\nfunc (*ListMemoAttachmentsRequest) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_memo_service_proto_rawDescGZIP(), []int{10}\n}\n\nfunc (x *ListMemoAttachmentsRequest) GetName() string {\n\tif x != nil {\n\t\treturn x.Name\n\t}\n\treturn \"\"\n}\n\nfunc (x *ListMemoAttachmentsRequest) GetPageSize() int32 {\n\tif x != nil {\n\t\treturn x.PageSize\n\t}\n\treturn 0\n}\n\nfunc (x *ListMemoAttachmentsRequest) GetPageToken() string {\n\tif x != nil {\n\t\treturn x.PageToken\n\t}\n\treturn \"\"\n}\n\ntype ListMemoAttachmentsResponse struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// The list of attachments.\n\tAttachments []*Attachment `protobuf:\"bytes,1,rep,name=attachments,proto3\" json:\"attachments,omitempty\"`\n\t// A token for the next page of results.\n\tNextPageToken string `protobuf:\"bytes,2,opt,name=next_page_token,json=nextPageToken,proto3\" json:\"next_page_token,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *ListMemoAttachmentsResponse) Reset() {\n\t*x = ListMemoAttachmentsResponse{}\n\tmi := &file_api_v1_memo_service_proto_msgTypes[11]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *ListMemoAttachmentsResponse) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*ListMemoAttachmentsResponse) ProtoMessage() {}\n\nfunc (x *ListMemoAttachmentsResponse) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_memo_service_proto_msgTypes[11]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use ListMemoAttachmentsResponse.ProtoReflect.Descriptor instead.\nfunc (*ListMemoAttachmentsResponse) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_memo_service_proto_rawDescGZIP(), []int{11}\n}\n\nfunc (x *ListMemoAttachmentsResponse) GetAttachments() []*Attachment {\n\tif x != nil {\n\t\treturn x.Attachments\n\t}\n\treturn nil\n}\n\nfunc (x *ListMemoAttachmentsResponse) GetNextPageToken() string {\n\tif x != nil {\n\t\treturn x.NextPageToken\n\t}\n\treturn \"\"\n}\n\ntype MemoRelation struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// The memo in the relation.\n\tMemo *MemoRelation_Memo `protobuf:\"bytes,1,opt,name=memo,proto3\" json:\"memo,omitempty\"`\n\t// The related memo.\n\tRelatedMemo   *MemoRelation_Memo `protobuf:\"bytes,2,opt,name=related_memo,json=relatedMemo,proto3\" json:\"related_memo,omitempty\"`\n\tType          MemoRelation_Type  `protobuf:\"varint,3,opt,name=type,proto3,enum=memos.api.v1.MemoRelation_Type\" json:\"type,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *MemoRelation) Reset() {\n\t*x = MemoRelation{}\n\tmi := &file_api_v1_memo_service_proto_msgTypes[12]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *MemoRelation) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*MemoRelation) ProtoMessage() {}\n\nfunc (x *MemoRelation) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_memo_service_proto_msgTypes[12]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use MemoRelation.ProtoReflect.Descriptor instead.\nfunc (*MemoRelation) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_memo_service_proto_rawDescGZIP(), []int{12}\n}\n\nfunc (x *MemoRelation) GetMemo() *MemoRelation_Memo {\n\tif x != nil {\n\t\treturn x.Memo\n\t}\n\treturn nil\n}\n\nfunc (x *MemoRelation) GetRelatedMemo() *MemoRelation_Memo {\n\tif x != nil {\n\t\treturn x.RelatedMemo\n\t}\n\treturn nil\n}\n\nfunc (x *MemoRelation) GetType() MemoRelation_Type {\n\tif x != nil {\n\t\treturn x.Type\n\t}\n\treturn MemoRelation_TYPE_UNSPECIFIED\n}\n\ntype SetMemoRelationsRequest struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// Required. The resource name of the memo.\n\t// Format: memos/{memo}\n\tName string `protobuf:\"bytes,1,opt,name=name,proto3\" json:\"name,omitempty\"`\n\t// Required. The relations to set for the memo.\n\tRelations     []*MemoRelation `protobuf:\"bytes,2,rep,name=relations,proto3\" json:\"relations,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *SetMemoRelationsRequest) Reset() {\n\t*x = SetMemoRelationsRequest{}\n\tmi := &file_api_v1_memo_service_proto_msgTypes[13]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *SetMemoRelationsRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*SetMemoRelationsRequest) ProtoMessage() {}\n\nfunc (x *SetMemoRelationsRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_memo_service_proto_msgTypes[13]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use SetMemoRelationsRequest.ProtoReflect.Descriptor instead.\nfunc (*SetMemoRelationsRequest) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_memo_service_proto_rawDescGZIP(), []int{13}\n}\n\nfunc (x *SetMemoRelationsRequest) GetName() string {\n\tif x != nil {\n\t\treturn x.Name\n\t}\n\treturn \"\"\n}\n\nfunc (x *SetMemoRelationsRequest) GetRelations() []*MemoRelation {\n\tif x != nil {\n\t\treturn x.Relations\n\t}\n\treturn nil\n}\n\ntype ListMemoRelationsRequest struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// Required. The resource name of the memo.\n\t// Format: memos/{memo}\n\tName string `protobuf:\"bytes,1,opt,name=name,proto3\" json:\"name,omitempty\"`\n\t// Optional. The maximum number of relations to return.\n\tPageSize int32 `protobuf:\"varint,2,opt,name=page_size,json=pageSize,proto3\" json:\"page_size,omitempty\"`\n\t// Optional. A page token for pagination.\n\tPageToken     string `protobuf:\"bytes,3,opt,name=page_token,json=pageToken,proto3\" json:\"page_token,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *ListMemoRelationsRequest) Reset() {\n\t*x = ListMemoRelationsRequest{}\n\tmi := &file_api_v1_memo_service_proto_msgTypes[14]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *ListMemoRelationsRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*ListMemoRelationsRequest) ProtoMessage() {}\n\nfunc (x *ListMemoRelationsRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_memo_service_proto_msgTypes[14]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use ListMemoRelationsRequest.ProtoReflect.Descriptor instead.\nfunc (*ListMemoRelationsRequest) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_memo_service_proto_rawDescGZIP(), []int{14}\n}\n\nfunc (x *ListMemoRelationsRequest) GetName() string {\n\tif x != nil {\n\t\treturn x.Name\n\t}\n\treturn \"\"\n}\n\nfunc (x *ListMemoRelationsRequest) GetPageSize() int32 {\n\tif x != nil {\n\t\treturn x.PageSize\n\t}\n\treturn 0\n}\n\nfunc (x *ListMemoRelationsRequest) GetPageToken() string {\n\tif x != nil {\n\t\treturn x.PageToken\n\t}\n\treturn \"\"\n}\n\ntype ListMemoRelationsResponse struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// The list of relations.\n\tRelations []*MemoRelation `protobuf:\"bytes,1,rep,name=relations,proto3\" json:\"relations,omitempty\"`\n\t// A token for the next page of results.\n\tNextPageToken string `protobuf:\"bytes,2,opt,name=next_page_token,json=nextPageToken,proto3\" json:\"next_page_token,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *ListMemoRelationsResponse) Reset() {\n\t*x = ListMemoRelationsResponse{}\n\tmi := &file_api_v1_memo_service_proto_msgTypes[15]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *ListMemoRelationsResponse) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*ListMemoRelationsResponse) ProtoMessage() {}\n\nfunc (x *ListMemoRelationsResponse) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_memo_service_proto_msgTypes[15]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use ListMemoRelationsResponse.ProtoReflect.Descriptor instead.\nfunc (*ListMemoRelationsResponse) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_memo_service_proto_rawDescGZIP(), []int{15}\n}\n\nfunc (x *ListMemoRelationsResponse) GetRelations() []*MemoRelation {\n\tif x != nil {\n\t\treturn x.Relations\n\t}\n\treturn nil\n}\n\nfunc (x *ListMemoRelationsResponse) GetNextPageToken() string {\n\tif x != nil {\n\t\treturn x.NextPageToken\n\t}\n\treturn \"\"\n}\n\ntype CreateMemoCommentRequest struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// Required. The resource name of the memo.\n\t// Format: memos/{memo}\n\tName string `protobuf:\"bytes,1,opt,name=name,proto3\" json:\"name,omitempty\"`\n\t// Required. The comment to create.\n\tComment *Memo `protobuf:\"bytes,2,opt,name=comment,proto3\" json:\"comment,omitempty\"`\n\t// Optional. The comment ID to use.\n\tCommentId     string `protobuf:\"bytes,3,opt,name=comment_id,json=commentId,proto3\" json:\"comment_id,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *CreateMemoCommentRequest) Reset() {\n\t*x = CreateMemoCommentRequest{}\n\tmi := &file_api_v1_memo_service_proto_msgTypes[16]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *CreateMemoCommentRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*CreateMemoCommentRequest) ProtoMessage() {}\n\nfunc (x *CreateMemoCommentRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_memo_service_proto_msgTypes[16]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use CreateMemoCommentRequest.ProtoReflect.Descriptor instead.\nfunc (*CreateMemoCommentRequest) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_memo_service_proto_rawDescGZIP(), []int{16}\n}\n\nfunc (x *CreateMemoCommentRequest) GetName() string {\n\tif x != nil {\n\t\treturn x.Name\n\t}\n\treturn \"\"\n}\n\nfunc (x *CreateMemoCommentRequest) GetComment() *Memo {\n\tif x != nil {\n\t\treturn x.Comment\n\t}\n\treturn nil\n}\n\nfunc (x *CreateMemoCommentRequest) GetCommentId() string {\n\tif x != nil {\n\t\treturn x.CommentId\n\t}\n\treturn \"\"\n}\n\ntype ListMemoCommentsRequest struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// Required. The resource name of the memo.\n\t// Format: memos/{memo}\n\tName string `protobuf:\"bytes,1,opt,name=name,proto3\" json:\"name,omitempty\"`\n\t// Optional. The maximum number of comments to return.\n\tPageSize int32 `protobuf:\"varint,2,opt,name=page_size,json=pageSize,proto3\" json:\"page_size,omitempty\"`\n\t// Optional. A page token for pagination.\n\tPageToken string `protobuf:\"bytes,3,opt,name=page_token,json=pageToken,proto3\" json:\"page_token,omitempty\"`\n\t// Optional. The order to sort results by.\n\tOrderBy       string `protobuf:\"bytes,4,opt,name=order_by,json=orderBy,proto3\" json:\"order_by,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *ListMemoCommentsRequest) Reset() {\n\t*x = ListMemoCommentsRequest{}\n\tmi := &file_api_v1_memo_service_proto_msgTypes[17]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *ListMemoCommentsRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*ListMemoCommentsRequest) ProtoMessage() {}\n\nfunc (x *ListMemoCommentsRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_memo_service_proto_msgTypes[17]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use ListMemoCommentsRequest.ProtoReflect.Descriptor instead.\nfunc (*ListMemoCommentsRequest) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_memo_service_proto_rawDescGZIP(), []int{17}\n}\n\nfunc (x *ListMemoCommentsRequest) GetName() string {\n\tif x != nil {\n\t\treturn x.Name\n\t}\n\treturn \"\"\n}\n\nfunc (x *ListMemoCommentsRequest) GetPageSize() int32 {\n\tif x != nil {\n\t\treturn x.PageSize\n\t}\n\treturn 0\n}\n\nfunc (x *ListMemoCommentsRequest) GetPageToken() string {\n\tif x != nil {\n\t\treturn x.PageToken\n\t}\n\treturn \"\"\n}\n\nfunc (x *ListMemoCommentsRequest) GetOrderBy() string {\n\tif x != nil {\n\t\treturn x.OrderBy\n\t}\n\treturn \"\"\n}\n\ntype ListMemoCommentsResponse struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// The list of comment memos.\n\tMemos []*Memo `protobuf:\"bytes,1,rep,name=memos,proto3\" json:\"memos,omitempty\"`\n\t// A token for the next page of results.\n\tNextPageToken string `protobuf:\"bytes,2,opt,name=next_page_token,json=nextPageToken,proto3\" json:\"next_page_token,omitempty\"`\n\t// The total count of comments.\n\tTotalSize     int32 `protobuf:\"varint,3,opt,name=total_size,json=totalSize,proto3\" json:\"total_size,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *ListMemoCommentsResponse) Reset() {\n\t*x = ListMemoCommentsResponse{}\n\tmi := &file_api_v1_memo_service_proto_msgTypes[18]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *ListMemoCommentsResponse) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*ListMemoCommentsResponse) ProtoMessage() {}\n\nfunc (x *ListMemoCommentsResponse) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_memo_service_proto_msgTypes[18]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use ListMemoCommentsResponse.ProtoReflect.Descriptor instead.\nfunc (*ListMemoCommentsResponse) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_memo_service_proto_rawDescGZIP(), []int{18}\n}\n\nfunc (x *ListMemoCommentsResponse) GetMemos() []*Memo {\n\tif x != nil {\n\t\treturn x.Memos\n\t}\n\treturn nil\n}\n\nfunc (x *ListMemoCommentsResponse) GetNextPageToken() string {\n\tif x != nil {\n\t\treturn x.NextPageToken\n\t}\n\treturn \"\"\n}\n\nfunc (x *ListMemoCommentsResponse) GetTotalSize() int32 {\n\tif x != nil {\n\t\treturn x.TotalSize\n\t}\n\treturn 0\n}\n\ntype ListMemoReactionsRequest struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// Required. The resource name of the memo.\n\t// Format: memos/{memo}\n\tName string `protobuf:\"bytes,1,opt,name=name,proto3\" json:\"name,omitempty\"`\n\t// Optional. The maximum number of reactions to return.\n\tPageSize int32 `protobuf:\"varint,2,opt,name=page_size,json=pageSize,proto3\" json:\"page_size,omitempty\"`\n\t// Optional. A page token for pagination.\n\tPageToken     string `protobuf:\"bytes,3,opt,name=page_token,json=pageToken,proto3\" json:\"page_token,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *ListMemoReactionsRequest) Reset() {\n\t*x = ListMemoReactionsRequest{}\n\tmi := &file_api_v1_memo_service_proto_msgTypes[19]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *ListMemoReactionsRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*ListMemoReactionsRequest) ProtoMessage() {}\n\nfunc (x *ListMemoReactionsRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_memo_service_proto_msgTypes[19]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use ListMemoReactionsRequest.ProtoReflect.Descriptor instead.\nfunc (*ListMemoReactionsRequest) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_memo_service_proto_rawDescGZIP(), []int{19}\n}\n\nfunc (x *ListMemoReactionsRequest) GetName() string {\n\tif x != nil {\n\t\treturn x.Name\n\t}\n\treturn \"\"\n}\n\nfunc (x *ListMemoReactionsRequest) GetPageSize() int32 {\n\tif x != nil {\n\t\treturn x.PageSize\n\t}\n\treturn 0\n}\n\nfunc (x *ListMemoReactionsRequest) GetPageToken() string {\n\tif x != nil {\n\t\treturn x.PageToken\n\t}\n\treturn \"\"\n}\n\ntype ListMemoReactionsResponse struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// The list of reactions.\n\tReactions []*Reaction `protobuf:\"bytes,1,rep,name=reactions,proto3\" json:\"reactions,omitempty\"`\n\t// A token for the next page of results.\n\tNextPageToken string `protobuf:\"bytes,2,opt,name=next_page_token,json=nextPageToken,proto3\" json:\"next_page_token,omitempty\"`\n\t// The total count of reactions.\n\tTotalSize     int32 `protobuf:\"varint,3,opt,name=total_size,json=totalSize,proto3\" json:\"total_size,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *ListMemoReactionsResponse) Reset() {\n\t*x = ListMemoReactionsResponse{}\n\tmi := &file_api_v1_memo_service_proto_msgTypes[20]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *ListMemoReactionsResponse) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*ListMemoReactionsResponse) ProtoMessage() {}\n\nfunc (x *ListMemoReactionsResponse) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_memo_service_proto_msgTypes[20]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use ListMemoReactionsResponse.ProtoReflect.Descriptor instead.\nfunc (*ListMemoReactionsResponse) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_memo_service_proto_rawDescGZIP(), []int{20}\n}\n\nfunc (x *ListMemoReactionsResponse) GetReactions() []*Reaction {\n\tif x != nil {\n\t\treturn x.Reactions\n\t}\n\treturn nil\n}\n\nfunc (x *ListMemoReactionsResponse) GetNextPageToken() string {\n\tif x != nil {\n\t\treturn x.NextPageToken\n\t}\n\treturn \"\"\n}\n\nfunc (x *ListMemoReactionsResponse) GetTotalSize() int32 {\n\tif x != nil {\n\t\treturn x.TotalSize\n\t}\n\treturn 0\n}\n\ntype UpsertMemoReactionRequest struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// Required. The resource name of the memo.\n\t// Format: memos/{memo}\n\tName string `protobuf:\"bytes,1,opt,name=name,proto3\" json:\"name,omitempty\"`\n\t// Required. The reaction to upsert.\n\tReaction      *Reaction `protobuf:\"bytes,2,opt,name=reaction,proto3\" json:\"reaction,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *UpsertMemoReactionRequest) Reset() {\n\t*x = UpsertMemoReactionRequest{}\n\tmi := &file_api_v1_memo_service_proto_msgTypes[21]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *UpsertMemoReactionRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*UpsertMemoReactionRequest) ProtoMessage() {}\n\nfunc (x *UpsertMemoReactionRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_memo_service_proto_msgTypes[21]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use UpsertMemoReactionRequest.ProtoReflect.Descriptor instead.\nfunc (*UpsertMemoReactionRequest) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_memo_service_proto_rawDescGZIP(), []int{21}\n}\n\nfunc (x *UpsertMemoReactionRequest) GetName() string {\n\tif x != nil {\n\t\treturn x.Name\n\t}\n\treturn \"\"\n}\n\nfunc (x *UpsertMemoReactionRequest) GetReaction() *Reaction {\n\tif x != nil {\n\t\treturn x.Reaction\n\t}\n\treturn nil\n}\n\ntype DeleteMemoReactionRequest struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// Required. The resource name of the reaction to delete.\n\t// Format: memos/{memo}/reactions/{reaction}\n\tName          string `protobuf:\"bytes,1,opt,name=name,proto3\" json:\"name,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *DeleteMemoReactionRequest) Reset() {\n\t*x = DeleteMemoReactionRequest{}\n\tmi := &file_api_v1_memo_service_proto_msgTypes[22]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *DeleteMemoReactionRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*DeleteMemoReactionRequest) ProtoMessage() {}\n\nfunc (x *DeleteMemoReactionRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_memo_service_proto_msgTypes[22]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use DeleteMemoReactionRequest.ProtoReflect.Descriptor instead.\nfunc (*DeleteMemoReactionRequest) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_memo_service_proto_rawDescGZIP(), []int{22}\n}\n\nfunc (x *DeleteMemoReactionRequest) GetName() string {\n\tif x != nil {\n\t\treturn x.Name\n\t}\n\treturn \"\"\n}\n\n// MemoShare is an access grant that permits read-only access to a memo via an opaque bearer token.\ntype MemoShare struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// The resource name of the share. Format: memos/{memo}/shares/{share}\n\t// The {share} segment is the opaque token used in the share URL.\n\tName string `protobuf:\"bytes,1,opt,name=name,proto3\" json:\"name,omitempty\"`\n\t// Output only. When this share link was created.\n\tCreateTime *timestamppb.Timestamp `protobuf:\"bytes,2,opt,name=create_time,json=createTime,proto3\" json:\"create_time,omitempty\"`\n\t// Optional. When set, the share link stops working after this time.\n\t// If unset, the link never expires.\n\tExpireTime    *timestamppb.Timestamp `protobuf:\"bytes,3,opt,name=expire_time,json=expireTime,proto3,oneof\" json:\"expire_time,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *MemoShare) Reset() {\n\t*x = MemoShare{}\n\tmi := &file_api_v1_memo_service_proto_msgTypes[23]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *MemoShare) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*MemoShare) ProtoMessage() {}\n\nfunc (x *MemoShare) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_memo_service_proto_msgTypes[23]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use MemoShare.ProtoReflect.Descriptor instead.\nfunc (*MemoShare) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_memo_service_proto_rawDescGZIP(), []int{23}\n}\n\nfunc (x *MemoShare) GetName() string {\n\tif x != nil {\n\t\treturn x.Name\n\t}\n\treturn \"\"\n}\n\nfunc (x *MemoShare) GetCreateTime() *timestamppb.Timestamp {\n\tif x != nil {\n\t\treturn x.CreateTime\n\t}\n\treturn nil\n}\n\nfunc (x *MemoShare) GetExpireTime() *timestamppb.Timestamp {\n\tif x != nil {\n\t\treturn x.ExpireTime\n\t}\n\treturn nil\n}\n\ntype CreateMemoShareRequest struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// Required. The resource name of the memo to share.\n\t// Format: memos/{memo}\n\tParent string `protobuf:\"bytes,1,opt,name=parent,proto3\" json:\"parent,omitempty\"`\n\t// Required. The share to create.\n\tMemoShare     *MemoShare `protobuf:\"bytes,2,opt,name=memo_share,json=memoShare,proto3\" json:\"memo_share,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *CreateMemoShareRequest) Reset() {\n\t*x = CreateMemoShareRequest{}\n\tmi := &file_api_v1_memo_service_proto_msgTypes[24]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *CreateMemoShareRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*CreateMemoShareRequest) ProtoMessage() {}\n\nfunc (x *CreateMemoShareRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_memo_service_proto_msgTypes[24]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use CreateMemoShareRequest.ProtoReflect.Descriptor instead.\nfunc (*CreateMemoShareRequest) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_memo_service_proto_rawDescGZIP(), []int{24}\n}\n\nfunc (x *CreateMemoShareRequest) GetParent() string {\n\tif x != nil {\n\t\treturn x.Parent\n\t}\n\treturn \"\"\n}\n\nfunc (x *CreateMemoShareRequest) GetMemoShare() *MemoShare {\n\tif x != nil {\n\t\treturn x.MemoShare\n\t}\n\treturn nil\n}\n\ntype ListMemoSharesRequest struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// Required. The resource name of the memo.\n\t// Format: memos/{memo}\n\tParent        string `protobuf:\"bytes,1,opt,name=parent,proto3\" json:\"parent,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *ListMemoSharesRequest) Reset() {\n\t*x = ListMemoSharesRequest{}\n\tmi := &file_api_v1_memo_service_proto_msgTypes[25]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *ListMemoSharesRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*ListMemoSharesRequest) ProtoMessage() {}\n\nfunc (x *ListMemoSharesRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_memo_service_proto_msgTypes[25]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use ListMemoSharesRequest.ProtoReflect.Descriptor instead.\nfunc (*ListMemoSharesRequest) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_memo_service_proto_rawDescGZIP(), []int{25}\n}\n\nfunc (x *ListMemoSharesRequest) GetParent() string {\n\tif x != nil {\n\t\treturn x.Parent\n\t}\n\treturn \"\"\n}\n\ntype ListMemoSharesResponse struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// The list of share links.\n\tMemoShares    []*MemoShare `protobuf:\"bytes,1,rep,name=memo_shares,json=memoShares,proto3\" json:\"memo_shares,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *ListMemoSharesResponse) Reset() {\n\t*x = ListMemoSharesResponse{}\n\tmi := &file_api_v1_memo_service_proto_msgTypes[26]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *ListMemoSharesResponse) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*ListMemoSharesResponse) ProtoMessage() {}\n\nfunc (x *ListMemoSharesResponse) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_memo_service_proto_msgTypes[26]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use ListMemoSharesResponse.ProtoReflect.Descriptor instead.\nfunc (*ListMemoSharesResponse) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_memo_service_proto_rawDescGZIP(), []int{26}\n}\n\nfunc (x *ListMemoSharesResponse) GetMemoShares() []*MemoShare {\n\tif x != nil {\n\t\treturn x.MemoShares\n\t}\n\treturn nil\n}\n\ntype DeleteMemoShareRequest struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// Required. The resource name of the share to delete.\n\t// Format: memos/{memo}/shares/{share}\n\tName          string `protobuf:\"bytes,1,opt,name=name,proto3\" json:\"name,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *DeleteMemoShareRequest) Reset() {\n\t*x = DeleteMemoShareRequest{}\n\tmi := &file_api_v1_memo_service_proto_msgTypes[27]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *DeleteMemoShareRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*DeleteMemoShareRequest) ProtoMessage() {}\n\nfunc (x *DeleteMemoShareRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_memo_service_proto_msgTypes[27]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use DeleteMemoShareRequest.ProtoReflect.Descriptor instead.\nfunc (*DeleteMemoShareRequest) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_memo_service_proto_rawDescGZIP(), []int{27}\n}\n\nfunc (x *DeleteMemoShareRequest) GetName() string {\n\tif x != nil {\n\t\treturn x.Name\n\t}\n\treturn \"\"\n}\n\ntype GetMemoByShareRequest struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// Required. The share token extracted from the share URL (/s/{share_id}).\n\tShareId       string `protobuf:\"bytes,1,opt,name=share_id,json=shareId,proto3\" json:\"share_id,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *GetMemoByShareRequest) Reset() {\n\t*x = GetMemoByShareRequest{}\n\tmi := &file_api_v1_memo_service_proto_msgTypes[28]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *GetMemoByShareRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*GetMemoByShareRequest) ProtoMessage() {}\n\nfunc (x *GetMemoByShareRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_memo_service_proto_msgTypes[28]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use GetMemoByShareRequest.ProtoReflect.Descriptor instead.\nfunc (*GetMemoByShareRequest) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_memo_service_proto_rawDescGZIP(), []int{28}\n}\n\nfunc (x *GetMemoByShareRequest) GetShareId() string {\n\tif x != nil {\n\t\treturn x.ShareId\n\t}\n\treturn \"\"\n}\n\n// Computed properties of a memo.\ntype Memo_Property struct {\n\tstate              protoimpl.MessageState `protogen:\"open.v1\"`\n\tHasLink            bool                   `protobuf:\"varint,1,opt,name=has_link,json=hasLink,proto3\" json:\"has_link,omitempty\"`\n\tHasTaskList        bool                   `protobuf:\"varint,2,opt,name=has_task_list,json=hasTaskList,proto3\" json:\"has_task_list,omitempty\"`\n\tHasCode            bool                   `protobuf:\"varint,3,opt,name=has_code,json=hasCode,proto3\" json:\"has_code,omitempty\"`\n\tHasIncompleteTasks bool                   `protobuf:\"varint,4,opt,name=has_incomplete_tasks,json=hasIncompleteTasks,proto3\" json:\"has_incomplete_tasks,omitempty\"`\n\t// The title extracted from the first H1 heading, if present.\n\tTitle         string `protobuf:\"bytes,5,opt,name=title,proto3\" json:\"title,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *Memo_Property) Reset() {\n\t*x = Memo_Property{}\n\tmi := &file_api_v1_memo_service_proto_msgTypes[29]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *Memo_Property) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*Memo_Property) ProtoMessage() {}\n\nfunc (x *Memo_Property) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_memo_service_proto_msgTypes[29]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use Memo_Property.ProtoReflect.Descriptor instead.\nfunc (*Memo_Property) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_memo_service_proto_rawDescGZIP(), []int{1, 0}\n}\n\nfunc (x *Memo_Property) GetHasLink() bool {\n\tif x != nil {\n\t\treturn x.HasLink\n\t}\n\treturn false\n}\n\nfunc (x *Memo_Property) GetHasTaskList() bool {\n\tif x != nil {\n\t\treturn x.HasTaskList\n\t}\n\treturn false\n}\n\nfunc (x *Memo_Property) GetHasCode() bool {\n\tif x != nil {\n\t\treturn x.HasCode\n\t}\n\treturn false\n}\n\nfunc (x *Memo_Property) GetHasIncompleteTasks() bool {\n\tif x != nil {\n\t\treturn x.HasIncompleteTasks\n\t}\n\treturn false\n}\n\nfunc (x *Memo_Property) GetTitle() string {\n\tif x != nil {\n\t\treturn x.Title\n\t}\n\treturn \"\"\n}\n\n// Memo reference in relations.\ntype MemoRelation_Memo struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// The resource name of the memo.\n\t// Format: memos/{memo}\n\tName string `protobuf:\"bytes,1,opt,name=name,proto3\" json:\"name,omitempty\"`\n\t// Output only. The snippet of the memo content. Plain text only.\n\tSnippet       string `protobuf:\"bytes,2,opt,name=snippet,proto3\" json:\"snippet,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *MemoRelation_Memo) Reset() {\n\t*x = MemoRelation_Memo{}\n\tmi := &file_api_v1_memo_service_proto_msgTypes[30]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *MemoRelation_Memo) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*MemoRelation_Memo) ProtoMessage() {}\n\nfunc (x *MemoRelation_Memo) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_memo_service_proto_msgTypes[30]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use MemoRelation_Memo.ProtoReflect.Descriptor instead.\nfunc (*MemoRelation_Memo) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_memo_service_proto_rawDescGZIP(), []int{12, 0}\n}\n\nfunc (x *MemoRelation_Memo) GetName() string {\n\tif x != nil {\n\t\treturn x.Name\n\t}\n\treturn \"\"\n}\n\nfunc (x *MemoRelation_Memo) GetSnippet() string {\n\tif x != nil {\n\t\treturn x.Snippet\n\t}\n\treturn \"\"\n}\n\nvar File_api_v1_memo_service_proto protoreflect.FileDescriptor\n\nconst file_api_v1_memo_service_proto_rawDesc = \"\" +\n\t\"\\n\" +\n\t\"\\x19api/v1/memo_service.proto\\x12\\fmemos.api.v1\\x1a\\x1fapi/v1/attachment_service.proto\\x1a\\x13api/v1/common.proto\\x1a\\x1cgoogle/api/annotations.proto\\x1a\\x17google/api/client.proto\\x1a\\x1fgoogle/api/field_behavior.proto\\x1a\\x19google/api/resource.proto\\x1a\\x1bgoogle/protobuf/empty.proto\\x1a google/protobuf/field_mask.proto\\x1a\\x1fgoogle/protobuf/timestamp.proto\\\"\\xdb\\x02\\n\" +\n\t\"\\bReaction\\x12\\x1a\\n\" +\n\t\"\\x04name\\x18\\x01 \\x01(\\tB\\x06\\xe0A\\x03\\xe0A\\bR\\x04name\\x123\\n\" +\n\t\"\\acreator\\x18\\x02 \\x01(\\tB\\x19\\xe0A\\x03\\xfaA\\x13\\n\" +\n\t\"\\x11memos.api.v1/UserR\\acreator\\x128\\n\" +\n\t\"\\n\" +\n\t\"content_id\\x18\\x03 \\x01(\\tB\\x19\\xe0A\\x02\\xfaA\\x13\\n\" +\n\t\"\\x11memos.api.v1/MemoR\\tcontentId\\x12(\\n\" +\n\t\"\\rreaction_type\\x18\\x04 \\x01(\\tB\\x03\\xe0A\\x02R\\freactionType\\x12@\\n\" +\n\t\"\\vcreate_time\\x18\\x05 \\x01(\\v2\\x1a.google.protobuf.TimestampB\\x03\\xe0A\\x03R\\n\" +\n\t\"createTime:X\\xeaAU\\n\" +\n\t\"\\x15memos.api.v1/Reaction\\x12!memos/{memo}/reactions/{reaction}\\x1a\\x04name*\\treactions2\\breaction\\\"\\xee\\b\\n\" +\n\t\"\\x04Memo\\x12\\x17\\n\" +\n\t\"\\x04name\\x18\\x01 \\x01(\\tB\\x03\\xe0A\\bR\\x04name\\x12.\\n\" +\n\t\"\\x05state\\x18\\x02 \\x01(\\x0e2\\x13.memos.api.v1.StateB\\x03\\xe0A\\x02R\\x05state\\x123\\n\" +\n\t\"\\acreator\\x18\\x03 \\x01(\\tB\\x19\\xe0A\\x03\\xfaA\\x13\\n\" +\n\t\"\\x11memos.api.v1/UserR\\acreator\\x12@\\n\" +\n\t\"\\vcreate_time\\x18\\x04 \\x01(\\v2\\x1a.google.protobuf.TimestampB\\x03\\xe0A\\x01R\\n\" +\n\t\"createTime\\x12@\\n\" +\n\t\"\\vupdate_time\\x18\\x05 \\x01(\\v2\\x1a.google.protobuf.TimestampB\\x03\\xe0A\\x01R\\n\" +\n\t\"updateTime\\x12B\\n\" +\n\t\"\\fdisplay_time\\x18\\x06 \\x01(\\v2\\x1a.google.protobuf.TimestampB\\x03\\xe0A\\x01R\\vdisplayTime\\x12\\x1d\\n\" +\n\t\"\\acontent\\x18\\a \\x01(\\tB\\x03\\xe0A\\x02R\\acontent\\x12=\\n\" +\n\t\"\\n\" +\n\t\"visibility\\x18\\t \\x01(\\x0e2\\x18.memos.api.v1.VisibilityB\\x03\\xe0A\\x02R\\n\" +\n\t\"visibility\\x12\\x17\\n\" +\n\t\"\\x04tags\\x18\\n\" +\n\t\" \\x03(\\tB\\x03\\xe0A\\x03R\\x04tags\\x12\\x1b\\n\" +\n\t\"\\x06pinned\\x18\\v \\x01(\\bB\\x03\\xe0A\\x01R\\x06pinned\\x12?\\n\" +\n\t\"\\vattachments\\x18\\f \\x03(\\v2\\x18.memos.api.v1.AttachmentB\\x03\\xe0A\\x01R\\vattachments\\x12=\\n\" +\n\t\"\\trelations\\x18\\r \\x03(\\v2\\x1a.memos.api.v1.MemoRelationB\\x03\\xe0A\\x01R\\trelations\\x129\\n\" +\n\t\"\\treactions\\x18\\x0e \\x03(\\v2\\x16.memos.api.v1.ReactionB\\x03\\xe0A\\x03R\\treactions\\x12<\\n\" +\n\t\"\\bproperty\\x18\\x0f \\x01(\\v2\\x1b.memos.api.v1.Memo.PropertyB\\x03\\xe0A\\x03R\\bproperty\\x126\\n\" +\n\t\"\\x06parent\\x18\\x10 \\x01(\\tB\\x19\\xe0A\\x03\\xfaA\\x13\\n\" +\n\t\"\\x11memos.api.v1/MemoH\\x00R\\x06parent\\x88\\x01\\x01\\x12\\x1d\\n\" +\n\t\"\\asnippet\\x18\\x11 \\x01(\\tB\\x03\\xe0A\\x03R\\asnippet\\x12<\\n\" +\n\t\"\\blocation\\x18\\x12 \\x01(\\v2\\x16.memos.api.v1.LocationB\\x03\\xe0A\\x01H\\x01R\\blocation\\x88\\x01\\x01\\x1a\\xac\\x01\\n\" +\n\t\"\\bProperty\\x12\\x19\\n\" +\n\t\"\\bhas_link\\x18\\x01 \\x01(\\bR\\ahasLink\\x12\\\"\\n\" +\n\t\"\\rhas_task_list\\x18\\x02 \\x01(\\bR\\vhasTaskList\\x12\\x19\\n\" +\n\t\"\\bhas_code\\x18\\x03 \\x01(\\bR\\ahasCode\\x120\\n\" +\n\t\"\\x14has_incomplete_tasks\\x18\\x04 \\x01(\\bR\\x12hasIncompleteTasks\\x12\\x14\\n\" +\n\t\"\\x05title\\x18\\x05 \\x01(\\tR\\x05title:7\\xeaA4\\n\" +\n\t\"\\x11memos.api.v1/Memo\\x12\\fmemos/{memo}\\x1a\\x04name*\\x05memos2\\x04memoB\\t\\n\" +\n\t\"\\a_parentB\\v\\n\" +\n\t\"\\t_location\\\"u\\n\" +\n\t\"\\bLocation\\x12%\\n\" +\n\t\"\\vplaceholder\\x18\\x01 \\x01(\\tB\\x03\\xe0A\\x01R\\vplaceholder\\x12\\x1f\\n\" +\n\t\"\\blatitude\\x18\\x02 \\x01(\\x01B\\x03\\xe0A\\x01R\\blatitude\\x12!\\n\" +\n\t\"\\tlongitude\\x18\\x03 \\x01(\\x01B\\x03\\xe0A\\x01R\\tlongitude\\\"^\\n\" +\n\t\"\\x11CreateMemoRequest\\x12+\\n\" +\n\t\"\\x04memo\\x18\\x01 \\x01(\\v2\\x12.memos.api.v1.MemoB\\x03\\xe0A\\x02R\\x04memo\\x12\\x1c\\n\" +\n\t\"\\amemo_id\\x18\\x02 \\x01(\\tB\\x03\\xe0A\\x01R\\x06memoId\\\"\\xed\\x01\\n\" +\n\t\"\\x10ListMemosRequest\\x12 \\n\" +\n\t\"\\tpage_size\\x18\\x01 \\x01(\\x05B\\x03\\xe0A\\x01R\\bpageSize\\x12\\\"\\n\" +\n\t\"\\n\" +\n\t\"page_token\\x18\\x02 \\x01(\\tB\\x03\\xe0A\\x01R\\tpageToken\\x12.\\n\" +\n\t\"\\x05state\\x18\\x03 \\x01(\\x0e2\\x13.memos.api.v1.StateB\\x03\\xe0A\\x01R\\x05state\\x12\\x1e\\n\" +\n\t\"\\border_by\\x18\\x04 \\x01(\\tB\\x03\\xe0A\\x01R\\aorderBy\\x12\\x1b\\n\" +\n\t\"\\x06filter\\x18\\x05 \\x01(\\tB\\x03\\xe0A\\x01R\\x06filter\\x12&\\n\" +\n\t\"\\fshow_deleted\\x18\\x06 \\x01(\\bB\\x03\\xe0A\\x01R\\vshowDeleted\\\"e\\n\" +\n\t\"\\x11ListMemosResponse\\x12(\\n\" +\n\t\"\\x05memos\\x18\\x01 \\x03(\\v2\\x12.memos.api.v1.MemoR\\x05memos\\x12&\\n\" +\n\t\"\\x0fnext_page_token\\x18\\x02 \\x01(\\tR\\rnextPageToken\\\"?\\n\" +\n\t\"\\x0eGetMemoRequest\\x12-\\n\" +\n\t\"\\x04name\\x18\\x01 \\x01(\\tB\\x19\\xe0A\\x02\\xfaA\\x13\\n\" +\n\t\"\\x11memos.api.v1/MemoR\\x04name\\\"\\x82\\x01\\n\" +\n\t\"\\x11UpdateMemoRequest\\x12+\\n\" +\n\t\"\\x04memo\\x18\\x01 \\x01(\\v2\\x12.memos.api.v1.MemoB\\x03\\xe0A\\x02R\\x04memo\\x12@\\n\" +\n\t\"\\vupdate_mask\\x18\\x02 \\x01(\\v2\\x1a.google.protobuf.FieldMaskB\\x03\\xe0A\\x02R\\n\" +\n\t\"updateMask\\\"]\\n\" +\n\t\"\\x11DeleteMemoRequest\\x12-\\n\" +\n\t\"\\x04name\\x18\\x01 \\x01(\\tB\\x19\\xe0A\\x02\\xfaA\\x13\\n\" +\n\t\"\\x11memos.api.v1/MemoR\\x04name\\x12\\x19\\n\" +\n\t\"\\x05force\\x18\\x02 \\x01(\\bB\\x03\\xe0A\\x01R\\x05force\\\"\\x8b\\x01\\n\" +\n\t\"\\x19SetMemoAttachmentsRequest\\x12-\\n\" +\n\t\"\\x04name\\x18\\x01 \\x01(\\tB\\x19\\xe0A\\x02\\xfaA\\x13\\n\" +\n\t\"\\x11memos.api.v1/MemoR\\x04name\\x12?\\n\" +\n\t\"\\vattachments\\x18\\x02 \\x03(\\v2\\x18.memos.api.v1.AttachmentB\\x03\\xe0A\\x02R\\vattachments\\\"\\x91\\x01\\n\" +\n\t\"\\x1aListMemoAttachmentsRequest\\x12-\\n\" +\n\t\"\\x04name\\x18\\x01 \\x01(\\tB\\x19\\xe0A\\x02\\xfaA\\x13\\n\" +\n\t\"\\x11memos.api.v1/MemoR\\x04name\\x12 \\n\" +\n\t\"\\tpage_size\\x18\\x02 \\x01(\\x05B\\x03\\xe0A\\x01R\\bpageSize\\x12\\\"\\n\" +\n\t\"\\n\" +\n\t\"page_token\\x18\\x03 \\x01(\\tB\\x03\\xe0A\\x01R\\tpageToken\\\"\\x81\\x01\\n\" +\n\t\"\\x1bListMemoAttachmentsResponse\\x12:\\n\" +\n\t\"\\vattachments\\x18\\x01 \\x03(\\v2\\x18.memos.api.v1.AttachmentR\\vattachments\\x12&\\n\" +\n\t\"\\x0fnext_page_token\\x18\\x02 \\x01(\\tR\\rnextPageToken\\\"\\xdb\\x02\\n\" +\n\t\"\\fMemoRelation\\x128\\n\" +\n\t\"\\x04memo\\x18\\x01 \\x01(\\v2\\x1f.memos.api.v1.MemoRelation.MemoB\\x03\\xe0A\\x02R\\x04memo\\x12G\\n\" +\n\t\"\\frelated_memo\\x18\\x02 \\x01(\\v2\\x1f.memos.api.v1.MemoRelation.MemoB\\x03\\xe0A\\x02R\\vrelatedMemo\\x128\\n\" +\n\t\"\\x04type\\x18\\x03 \\x01(\\x0e2\\x1f.memos.api.v1.MemoRelation.TypeB\\x03\\xe0A\\x02R\\x04type\\x1aT\\n\" +\n\t\"\\x04Memo\\x12-\\n\" +\n\t\"\\x04name\\x18\\x01 \\x01(\\tB\\x19\\xe0A\\x02\\xfaA\\x13\\n\" +\n\t\"\\x11memos.api.v1/MemoR\\x04name\\x12\\x1d\\n\" +\n\t\"\\asnippet\\x18\\x02 \\x01(\\tB\\x03\\xe0A\\x03R\\asnippet\\\"8\\n\" +\n\t\"\\x04Type\\x12\\x14\\n\" +\n\t\"\\x10TYPE_UNSPECIFIED\\x10\\x00\\x12\\r\\n\" +\n\t\"\\tREFERENCE\\x10\\x01\\x12\\v\\n\" +\n\t\"\\aCOMMENT\\x10\\x02\\\"\\x87\\x01\\n\" +\n\t\"\\x17SetMemoRelationsRequest\\x12-\\n\" +\n\t\"\\x04name\\x18\\x01 \\x01(\\tB\\x19\\xe0A\\x02\\xfaA\\x13\\n\" +\n\t\"\\x11memos.api.v1/MemoR\\x04name\\x12=\\n\" +\n\t\"\\trelations\\x18\\x02 \\x03(\\v2\\x1a.memos.api.v1.MemoRelationB\\x03\\xe0A\\x02R\\trelations\\\"\\x8f\\x01\\n\" +\n\t\"\\x18ListMemoRelationsRequest\\x12-\\n\" +\n\t\"\\x04name\\x18\\x01 \\x01(\\tB\\x19\\xe0A\\x02\\xfaA\\x13\\n\" +\n\t\"\\x11memos.api.v1/MemoR\\x04name\\x12 \\n\" +\n\t\"\\tpage_size\\x18\\x02 \\x01(\\x05B\\x03\\xe0A\\x01R\\bpageSize\\x12\\\"\\n\" +\n\t\"\\n\" +\n\t\"page_token\\x18\\x03 \\x01(\\tB\\x03\\xe0A\\x01R\\tpageToken\\\"}\\n\" +\n\t\"\\x19ListMemoRelationsResponse\\x128\\n\" +\n\t\"\\trelations\\x18\\x01 \\x03(\\v2\\x1a.memos.api.v1.MemoRelationR\\trelations\\x12&\\n\" +\n\t\"\\x0fnext_page_token\\x18\\x02 \\x01(\\tR\\rnextPageToken\\\"\\xa0\\x01\\n\" +\n\t\"\\x18CreateMemoCommentRequest\\x12-\\n\" +\n\t\"\\x04name\\x18\\x01 \\x01(\\tB\\x19\\xe0A\\x02\\xfaA\\x13\\n\" +\n\t\"\\x11memos.api.v1/MemoR\\x04name\\x121\\n\" +\n\t\"\\acomment\\x18\\x02 \\x01(\\v2\\x12.memos.api.v1.MemoB\\x03\\xe0A\\x02R\\acomment\\x12\\\"\\n\" +\n\t\"\\n\" +\n\t\"comment_id\\x18\\x03 \\x01(\\tB\\x03\\xe0A\\x01R\\tcommentId\\\"\\xae\\x01\\n\" +\n\t\"\\x17ListMemoCommentsRequest\\x12-\\n\" +\n\t\"\\x04name\\x18\\x01 \\x01(\\tB\\x19\\xe0A\\x02\\xfaA\\x13\\n\" +\n\t\"\\x11memos.api.v1/MemoR\\x04name\\x12 \\n\" +\n\t\"\\tpage_size\\x18\\x02 \\x01(\\x05B\\x03\\xe0A\\x01R\\bpageSize\\x12\\\"\\n\" +\n\t\"\\n\" +\n\t\"page_token\\x18\\x03 \\x01(\\tB\\x03\\xe0A\\x01R\\tpageToken\\x12\\x1e\\n\" +\n\t\"\\border_by\\x18\\x04 \\x01(\\tB\\x03\\xe0A\\x01R\\aorderBy\\\"\\x8b\\x01\\n\" +\n\t\"\\x18ListMemoCommentsResponse\\x12(\\n\" +\n\t\"\\x05memos\\x18\\x01 \\x03(\\v2\\x12.memos.api.v1.MemoR\\x05memos\\x12&\\n\" +\n\t\"\\x0fnext_page_token\\x18\\x02 \\x01(\\tR\\rnextPageToken\\x12\\x1d\\n\" +\n\t\"\\n\" +\n\t\"total_size\\x18\\x03 \\x01(\\x05R\\ttotalSize\\\"\\x8f\\x01\\n\" +\n\t\"\\x18ListMemoReactionsRequest\\x12-\\n\" +\n\t\"\\x04name\\x18\\x01 \\x01(\\tB\\x19\\xe0A\\x02\\xfaA\\x13\\n\" +\n\t\"\\x11memos.api.v1/MemoR\\x04name\\x12 \\n\" +\n\t\"\\tpage_size\\x18\\x02 \\x01(\\x05B\\x03\\xe0A\\x01R\\bpageSize\\x12\\\"\\n\" +\n\t\"\\n\" +\n\t\"page_token\\x18\\x03 \\x01(\\tB\\x03\\xe0A\\x01R\\tpageToken\\\"\\x98\\x01\\n\" +\n\t\"\\x19ListMemoReactionsResponse\\x124\\n\" +\n\t\"\\treactions\\x18\\x01 \\x03(\\v2\\x16.memos.api.v1.ReactionR\\treactions\\x12&\\n\" +\n\t\"\\x0fnext_page_token\\x18\\x02 \\x01(\\tR\\rnextPageToken\\x12\\x1d\\n\" +\n\t\"\\n\" +\n\t\"total_size\\x18\\x03 \\x01(\\x05R\\ttotalSize\\\"\\x83\\x01\\n\" +\n\t\"\\x19UpsertMemoReactionRequest\\x12-\\n\" +\n\t\"\\x04name\\x18\\x01 \\x01(\\tB\\x19\\xe0A\\x02\\xfaA\\x13\\n\" +\n\t\"\\x11memos.api.v1/MemoR\\x04name\\x127\\n\" +\n\t\"\\breaction\\x18\\x02 \\x01(\\v2\\x16.memos.api.v1.ReactionB\\x03\\xe0A\\x02R\\breaction\\\"N\\n\" +\n\t\"\\x19DeleteMemoReactionRequest\\x121\\n\" +\n\t\"\\x04name\\x18\\x01 \\x01(\\tB\\x1d\\xe0A\\x02\\xfaA\\x17\\n\" +\n\t\"\\x15memos.api.v1/ReactionR\\x04name\\\"\\x86\\x02\\n\" +\n\t\"\\tMemoShare\\x12\\x17\\n\" +\n\t\"\\x04name\\x18\\x01 \\x01(\\tB\\x03\\xe0A\\bR\\x04name\\x12@\\n\" +\n\t\"\\vcreate_time\\x18\\x02 \\x01(\\v2\\x1a.google.protobuf.TimestampB\\x03\\xe0A\\x03R\\n\" +\n\t\"createTime\\x12E\\n\" +\n\t\"\\vexpire_time\\x18\\x03 \\x01(\\v2\\x1a.google.protobuf.TimestampB\\x03\\xe0A\\x01H\\x00R\\n\" +\n\t\"expireTime\\x88\\x01\\x01:G\\xeaAD\\n\" +\n\t\"\\x16memos.api.v1/MemoShare\\x12\\x1bmemos/{memo}/shares/{share}*\\x06shares2\\x05shareB\\x0e\\n\" +\n\t\"\\f_expire_time\\\"\\x88\\x01\\n\" +\n\t\"\\x16CreateMemoShareRequest\\x121\\n\" +\n\t\"\\x06parent\\x18\\x01 \\x01(\\tB\\x19\\xe0A\\x02\\xfaA\\x13\\n\" +\n\t\"\\x11memos.api.v1/MemoR\\x06parent\\x12;\\n\" +\n\t\"\\n\" +\n\t\"memo_share\\x18\\x02 \\x01(\\v2\\x17.memos.api.v1.MemoShareB\\x03\\xe0A\\x02R\\tmemoShare\\\"J\\n\" +\n\t\"\\x15ListMemoSharesRequest\\x121\\n\" +\n\t\"\\x06parent\\x18\\x01 \\x01(\\tB\\x19\\xe0A\\x02\\xfaA\\x13\\n\" +\n\t\"\\x11memos.api.v1/MemoR\\x06parent\\\"R\\n\" +\n\t\"\\x16ListMemoSharesResponse\\x128\\n\" +\n\t\"\\vmemo_shares\\x18\\x01 \\x03(\\v2\\x17.memos.api.v1.MemoShareR\\n\" +\n\t\"memoShares\\\"L\\n\" +\n\t\"\\x16DeleteMemoShareRequest\\x122\\n\" +\n\t\"\\x04name\\x18\\x01 \\x01(\\tB\\x1e\\xe0A\\x02\\xfaA\\x18\\n\" +\n\t\"\\x16memos.api.v1/MemoShareR\\x04name\\\"7\\n\" +\n\t\"\\x15GetMemoByShareRequest\\x12\\x1e\\n\" +\n\t\"\\bshare_id\\x18\\x01 \\x01(\\tB\\x03\\xe0A\\x02R\\ashareId*P\\n\" +\n\t\"\\n\" +\n\t\"Visibility\\x12\\x1a\\n\" +\n\t\"\\x16VISIBILITY_UNSPECIFIED\\x10\\x00\\x12\\v\\n\" +\n\t\"\\aPRIVATE\\x10\\x01\\x12\\r\\n\" +\n\t\"\\tPROTECTED\\x10\\x02\\x12\\n\" +\n\t\"\\n\" +\n\t\"\\x06PUBLIC\\x10\\x032\\xee\\x12\\n\" +\n\t\"\\vMemoService\\x12e\\n\" +\n\t\"\\n\" +\n\t\"CreateMemo\\x12\\x1f.memos.api.v1.CreateMemoRequest\\x1a\\x12.memos.api.v1.Memo\\\"\\\"\\xdaA\\x04memo\\x82\\xd3\\xe4\\x93\\x02\\x15:\\x04memo\\\"\\r/api/v1/memos\\x12f\\n\" +\n\t\"\\tListMemos\\x12\\x1e.memos.api.v1.ListMemosRequest\\x1a\\x1f.memos.api.v1.ListMemosResponse\\\"\\x18\\xdaA\\x00\\x82\\xd3\\xe4\\x93\\x02\\x0f\\x12\\r/api/v1/memos\\x12b\\n\" +\n\t\"\\aGetMemo\\x12\\x1c.memos.api.v1.GetMemoRequest\\x1a\\x12.memos.api.v1.Memo\\\"%\\xdaA\\x04name\\x82\\xd3\\xe4\\x93\\x02\\x18\\x12\\x16/api/v1/{name=memos/*}\\x12\\x7f\\n\" +\n\t\"\\n\" +\n\t\"UpdateMemo\\x12\\x1f.memos.api.v1.UpdateMemoRequest\\x1a\\x12.memos.api.v1.Memo\\\"<\\xdaA\\x10memo,update_mask\\x82\\xd3\\xe4\\x93\\x02#:\\x04memo2\\x1b/api/v1/{memo.name=memos/*}\\x12l\\n\" +\n\t\"\\n\" +\n\t\"DeleteMemo\\x12\\x1f.memos.api.v1.DeleteMemoRequest\\x1a\\x16.google.protobuf.Empty\\\"%\\xdaA\\x04name\\x82\\xd3\\xe4\\x93\\x02\\x18*\\x16/api/v1/{name=memos/*}\\x12\\x8b\\x01\\n\" +\n\t\"\\x12SetMemoAttachments\\x12'.memos.api.v1.SetMemoAttachmentsRequest\\x1a\\x16.google.protobuf.Empty\\\"4\\xdaA\\x04name\\x82\\xd3\\xe4\\x93\\x02':\\x01*2\\\"/api/v1/{name=memos/*}/attachments\\x12\\x9d\\x01\\n\" +\n\t\"\\x13ListMemoAttachments\\x12(.memos.api.v1.ListMemoAttachmentsRequest\\x1a).memos.api.v1.ListMemoAttachmentsResponse\\\"1\\xdaA\\x04name\\x82\\xd3\\xe4\\x93\\x02$\\x12\\\"/api/v1/{name=memos/*}/attachments\\x12\\x85\\x01\\n\" +\n\t\"\\x10SetMemoRelations\\x12%.memos.api.v1.SetMemoRelationsRequest\\x1a\\x16.google.protobuf.Empty\\\"2\\xdaA\\x04name\\x82\\xd3\\xe4\\x93\\x02%:\\x01*2 /api/v1/{name=memos/*}/relations\\x12\\x95\\x01\\n\" +\n\t\"\\x11ListMemoRelations\\x12&.memos.api.v1.ListMemoRelationsRequest\\x1a'.memos.api.v1.ListMemoRelationsResponse\\\"/\\xdaA\\x04name\\x82\\xd3\\xe4\\x93\\x02\\\"\\x12 /api/v1/{name=memos/*}/relations\\x12\\x90\\x01\\n\" +\n\t\"\\x11CreateMemoComment\\x12&.memos.api.v1.CreateMemoCommentRequest\\x1a\\x12.memos.api.v1.Memo\\\"?\\xdaA\\fname,comment\\x82\\xd3\\xe4\\x93\\x02*:\\acomment\\\"\\x1f/api/v1/{name=memos/*}/comments\\x12\\x91\\x01\\n\" +\n\t\"\\x10ListMemoComments\\x12%.memos.api.v1.ListMemoCommentsRequest\\x1a&.memos.api.v1.ListMemoCommentsResponse\\\".\\xdaA\\x04name\\x82\\xd3\\xe4\\x93\\x02!\\x12\\x1f/api/v1/{name=memos/*}/comments\\x12\\x95\\x01\\n\" +\n\t\"\\x11ListMemoReactions\\x12&.memos.api.v1.ListMemoReactionsRequest\\x1a'.memos.api.v1.ListMemoReactionsResponse\\\"/\\xdaA\\x04name\\x82\\xd3\\xe4\\x93\\x02\\\"\\x12 /api/v1/{name=memos/*}/reactions\\x12\\x89\\x01\\n\" +\n\t\"\\x12UpsertMemoReaction\\x12'.memos.api.v1.UpsertMemoReactionRequest\\x1a\\x16.memos.api.v1.Reaction\\\"2\\xdaA\\x04name\\x82\\xd3\\xe4\\x93\\x02%:\\x01*\\\" /api/v1/{name=memos/*}/reactions\\x12\\x88\\x01\\n\" +\n\t\"\\x12DeleteMemoReaction\\x12'.memos.api.v1.DeleteMemoReactionRequest\\x1a\\x16.google.protobuf.Empty\\\"1\\xdaA\\x04name\\x82\\xd3\\xe4\\x93\\x02$*\\\"/api/v1/{name=memos/*/reactions/*}\\x12\\x99\\x01\\n\" +\n\t\"\\x0fCreateMemoShare\\x12$.memos.api.v1.CreateMemoShareRequest\\x1a\\x17.memos.api.v1.MemoShare\\\"G\\xdaA\\x11parent,memo_share\\x82\\xd3\\xe4\\x93\\x02-:\\n\" +\n\t\"memo_share\\\"\\x1f/api/v1/{parent=memos/*}/shares\\x12\\x8d\\x01\\n\" +\n\t\"\\x0eListMemoShares\\x12#.memos.api.v1.ListMemoSharesRequest\\x1a$.memos.api.v1.ListMemoSharesResponse\\\"0\\xdaA\\x06parent\\x82\\xd3\\xe4\\x93\\x02!\\x12\\x1f/api/v1/{parent=memos/*}/shares\\x12\\x7f\\n\" +\n\t\"\\x0fDeleteMemoShare\\x12$.memos.api.v1.DeleteMemoShareRequest\\x1a\\x16.google.protobuf.Empty\\\".\\xdaA\\x04name\\x82\\xd3\\xe4\\x93\\x02!*\\x1f/api/v1/{name=memos/*/shares/*}\\x12l\\n\" +\n\t\"\\x0eGetMemoByShare\\x12#.memos.api.v1.GetMemoByShareRequest\\x1a\\x12.memos.api.v1.Memo\\\"!\\x82\\xd3\\xe4\\x93\\x02\\x1b\\x12\\x19/api/v1/shares/{share_id}B\\xa8\\x01\\n\" +\n\t\"\\x10com.memos.api.v1B\\x10MemoServiceProtoP\\x01Z0github.com/usememos/memos/proto/gen/api/v1;apiv1\\xa2\\x02\\x03MAX\\xaa\\x02\\fMemos.Api.V1\\xca\\x02\\fMemos\\\\Api\\\\V1\\xe2\\x02\\x18Memos\\\\Api\\\\V1\\\\GPBMetadata\\xea\\x02\\x0eMemos::Api::V1b\\x06proto3\"\n\nvar (\n\tfile_api_v1_memo_service_proto_rawDescOnce sync.Once\n\tfile_api_v1_memo_service_proto_rawDescData []byte\n)\n\nfunc file_api_v1_memo_service_proto_rawDescGZIP() []byte {\n\tfile_api_v1_memo_service_proto_rawDescOnce.Do(func() {\n\t\tfile_api_v1_memo_service_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_api_v1_memo_service_proto_rawDesc), len(file_api_v1_memo_service_proto_rawDesc)))\n\t})\n\treturn file_api_v1_memo_service_proto_rawDescData\n}\n\nvar file_api_v1_memo_service_proto_enumTypes = make([]protoimpl.EnumInfo, 2)\nvar file_api_v1_memo_service_proto_msgTypes = make([]protoimpl.MessageInfo, 31)\nvar file_api_v1_memo_service_proto_goTypes = []any{\n\t(Visibility)(0),                     // 0: memos.api.v1.Visibility\n\t(MemoRelation_Type)(0),              // 1: memos.api.v1.MemoRelation.Type\n\t(*Reaction)(nil),                    // 2: memos.api.v1.Reaction\n\t(*Memo)(nil),                        // 3: memos.api.v1.Memo\n\t(*Location)(nil),                    // 4: memos.api.v1.Location\n\t(*CreateMemoRequest)(nil),           // 5: memos.api.v1.CreateMemoRequest\n\t(*ListMemosRequest)(nil),            // 6: memos.api.v1.ListMemosRequest\n\t(*ListMemosResponse)(nil),           // 7: memos.api.v1.ListMemosResponse\n\t(*GetMemoRequest)(nil),              // 8: memos.api.v1.GetMemoRequest\n\t(*UpdateMemoRequest)(nil),           // 9: memos.api.v1.UpdateMemoRequest\n\t(*DeleteMemoRequest)(nil),           // 10: memos.api.v1.DeleteMemoRequest\n\t(*SetMemoAttachmentsRequest)(nil),   // 11: memos.api.v1.SetMemoAttachmentsRequest\n\t(*ListMemoAttachmentsRequest)(nil),  // 12: memos.api.v1.ListMemoAttachmentsRequest\n\t(*ListMemoAttachmentsResponse)(nil), // 13: memos.api.v1.ListMemoAttachmentsResponse\n\t(*MemoRelation)(nil),                // 14: memos.api.v1.MemoRelation\n\t(*SetMemoRelationsRequest)(nil),     // 15: memos.api.v1.SetMemoRelationsRequest\n\t(*ListMemoRelationsRequest)(nil),    // 16: memos.api.v1.ListMemoRelationsRequest\n\t(*ListMemoRelationsResponse)(nil),   // 17: memos.api.v1.ListMemoRelationsResponse\n\t(*CreateMemoCommentRequest)(nil),    // 18: memos.api.v1.CreateMemoCommentRequest\n\t(*ListMemoCommentsRequest)(nil),     // 19: memos.api.v1.ListMemoCommentsRequest\n\t(*ListMemoCommentsResponse)(nil),    // 20: memos.api.v1.ListMemoCommentsResponse\n\t(*ListMemoReactionsRequest)(nil),    // 21: memos.api.v1.ListMemoReactionsRequest\n\t(*ListMemoReactionsResponse)(nil),   // 22: memos.api.v1.ListMemoReactionsResponse\n\t(*UpsertMemoReactionRequest)(nil),   // 23: memos.api.v1.UpsertMemoReactionRequest\n\t(*DeleteMemoReactionRequest)(nil),   // 24: memos.api.v1.DeleteMemoReactionRequest\n\t(*MemoShare)(nil),                   // 25: memos.api.v1.MemoShare\n\t(*CreateMemoShareRequest)(nil),      // 26: memos.api.v1.CreateMemoShareRequest\n\t(*ListMemoSharesRequest)(nil),       // 27: memos.api.v1.ListMemoSharesRequest\n\t(*ListMemoSharesResponse)(nil),      // 28: memos.api.v1.ListMemoSharesResponse\n\t(*DeleteMemoShareRequest)(nil),      // 29: memos.api.v1.DeleteMemoShareRequest\n\t(*GetMemoByShareRequest)(nil),       // 30: memos.api.v1.GetMemoByShareRequest\n\t(*Memo_Property)(nil),               // 31: memos.api.v1.Memo.Property\n\t(*MemoRelation_Memo)(nil),           // 32: memos.api.v1.MemoRelation.Memo\n\t(*timestamppb.Timestamp)(nil),       // 33: google.protobuf.Timestamp\n\t(State)(0),                          // 34: memos.api.v1.State\n\t(*Attachment)(nil),                  // 35: memos.api.v1.Attachment\n\t(*fieldmaskpb.FieldMask)(nil),       // 36: google.protobuf.FieldMask\n\t(*emptypb.Empty)(nil),               // 37: google.protobuf.Empty\n}\nvar file_api_v1_memo_service_proto_depIdxs = []int32{\n\t33, // 0: memos.api.v1.Reaction.create_time:type_name -> google.protobuf.Timestamp\n\t34, // 1: memos.api.v1.Memo.state:type_name -> memos.api.v1.State\n\t33, // 2: memos.api.v1.Memo.create_time:type_name -> google.protobuf.Timestamp\n\t33, // 3: memos.api.v1.Memo.update_time:type_name -> google.protobuf.Timestamp\n\t33, // 4: memos.api.v1.Memo.display_time:type_name -> google.protobuf.Timestamp\n\t0,  // 5: memos.api.v1.Memo.visibility:type_name -> memos.api.v1.Visibility\n\t35, // 6: memos.api.v1.Memo.attachments:type_name -> memos.api.v1.Attachment\n\t14, // 7: memos.api.v1.Memo.relations:type_name -> memos.api.v1.MemoRelation\n\t2,  // 8: memos.api.v1.Memo.reactions:type_name -> memos.api.v1.Reaction\n\t31, // 9: memos.api.v1.Memo.property:type_name -> memos.api.v1.Memo.Property\n\t4,  // 10: memos.api.v1.Memo.location:type_name -> memos.api.v1.Location\n\t3,  // 11: memos.api.v1.CreateMemoRequest.memo:type_name -> memos.api.v1.Memo\n\t34, // 12: memos.api.v1.ListMemosRequest.state:type_name -> memos.api.v1.State\n\t3,  // 13: memos.api.v1.ListMemosResponse.memos:type_name -> memos.api.v1.Memo\n\t3,  // 14: memos.api.v1.UpdateMemoRequest.memo:type_name -> memos.api.v1.Memo\n\t36, // 15: memos.api.v1.UpdateMemoRequest.update_mask:type_name -> google.protobuf.FieldMask\n\t35, // 16: memos.api.v1.SetMemoAttachmentsRequest.attachments:type_name -> memos.api.v1.Attachment\n\t35, // 17: memos.api.v1.ListMemoAttachmentsResponse.attachments:type_name -> memos.api.v1.Attachment\n\t32, // 18: memos.api.v1.MemoRelation.memo:type_name -> memos.api.v1.MemoRelation.Memo\n\t32, // 19: memos.api.v1.MemoRelation.related_memo:type_name -> memos.api.v1.MemoRelation.Memo\n\t1,  // 20: memos.api.v1.MemoRelation.type:type_name -> memos.api.v1.MemoRelation.Type\n\t14, // 21: memos.api.v1.SetMemoRelationsRequest.relations:type_name -> memos.api.v1.MemoRelation\n\t14, // 22: memos.api.v1.ListMemoRelationsResponse.relations:type_name -> memos.api.v1.MemoRelation\n\t3,  // 23: memos.api.v1.CreateMemoCommentRequest.comment:type_name -> memos.api.v1.Memo\n\t3,  // 24: memos.api.v1.ListMemoCommentsResponse.memos:type_name -> memos.api.v1.Memo\n\t2,  // 25: memos.api.v1.ListMemoReactionsResponse.reactions:type_name -> memos.api.v1.Reaction\n\t2,  // 26: memos.api.v1.UpsertMemoReactionRequest.reaction:type_name -> memos.api.v1.Reaction\n\t33, // 27: memos.api.v1.MemoShare.create_time:type_name -> google.protobuf.Timestamp\n\t33, // 28: memos.api.v1.MemoShare.expire_time:type_name -> google.protobuf.Timestamp\n\t25, // 29: memos.api.v1.CreateMemoShareRequest.memo_share:type_name -> memos.api.v1.MemoShare\n\t25, // 30: memos.api.v1.ListMemoSharesResponse.memo_shares:type_name -> memos.api.v1.MemoShare\n\t5,  // 31: memos.api.v1.MemoService.CreateMemo:input_type -> memos.api.v1.CreateMemoRequest\n\t6,  // 32: memos.api.v1.MemoService.ListMemos:input_type -> memos.api.v1.ListMemosRequest\n\t8,  // 33: memos.api.v1.MemoService.GetMemo:input_type -> memos.api.v1.GetMemoRequest\n\t9,  // 34: memos.api.v1.MemoService.UpdateMemo:input_type -> memos.api.v1.UpdateMemoRequest\n\t10, // 35: memos.api.v1.MemoService.DeleteMemo:input_type -> memos.api.v1.DeleteMemoRequest\n\t11, // 36: memos.api.v1.MemoService.SetMemoAttachments:input_type -> memos.api.v1.SetMemoAttachmentsRequest\n\t12, // 37: memos.api.v1.MemoService.ListMemoAttachments:input_type -> memos.api.v1.ListMemoAttachmentsRequest\n\t15, // 38: memos.api.v1.MemoService.SetMemoRelations:input_type -> memos.api.v1.SetMemoRelationsRequest\n\t16, // 39: memos.api.v1.MemoService.ListMemoRelations:input_type -> memos.api.v1.ListMemoRelationsRequest\n\t18, // 40: memos.api.v1.MemoService.CreateMemoComment:input_type -> memos.api.v1.CreateMemoCommentRequest\n\t19, // 41: memos.api.v1.MemoService.ListMemoComments:input_type -> memos.api.v1.ListMemoCommentsRequest\n\t21, // 42: memos.api.v1.MemoService.ListMemoReactions:input_type -> memos.api.v1.ListMemoReactionsRequest\n\t23, // 43: memos.api.v1.MemoService.UpsertMemoReaction:input_type -> memos.api.v1.UpsertMemoReactionRequest\n\t24, // 44: memos.api.v1.MemoService.DeleteMemoReaction:input_type -> memos.api.v1.DeleteMemoReactionRequest\n\t26, // 45: memos.api.v1.MemoService.CreateMemoShare:input_type -> memos.api.v1.CreateMemoShareRequest\n\t27, // 46: memos.api.v1.MemoService.ListMemoShares:input_type -> memos.api.v1.ListMemoSharesRequest\n\t29, // 47: memos.api.v1.MemoService.DeleteMemoShare:input_type -> memos.api.v1.DeleteMemoShareRequest\n\t30, // 48: memos.api.v1.MemoService.GetMemoByShare:input_type -> memos.api.v1.GetMemoByShareRequest\n\t3,  // 49: memos.api.v1.MemoService.CreateMemo:output_type -> memos.api.v1.Memo\n\t7,  // 50: memos.api.v1.MemoService.ListMemos:output_type -> memos.api.v1.ListMemosResponse\n\t3,  // 51: memos.api.v1.MemoService.GetMemo:output_type -> memos.api.v1.Memo\n\t3,  // 52: memos.api.v1.MemoService.UpdateMemo:output_type -> memos.api.v1.Memo\n\t37, // 53: memos.api.v1.MemoService.DeleteMemo:output_type -> google.protobuf.Empty\n\t37, // 54: memos.api.v1.MemoService.SetMemoAttachments:output_type -> google.protobuf.Empty\n\t13, // 55: memos.api.v1.MemoService.ListMemoAttachments:output_type -> memos.api.v1.ListMemoAttachmentsResponse\n\t37, // 56: memos.api.v1.MemoService.SetMemoRelations:output_type -> google.protobuf.Empty\n\t17, // 57: memos.api.v1.MemoService.ListMemoRelations:output_type -> memos.api.v1.ListMemoRelationsResponse\n\t3,  // 58: memos.api.v1.MemoService.CreateMemoComment:output_type -> memos.api.v1.Memo\n\t20, // 59: memos.api.v1.MemoService.ListMemoComments:output_type -> memos.api.v1.ListMemoCommentsResponse\n\t22, // 60: memos.api.v1.MemoService.ListMemoReactions:output_type -> memos.api.v1.ListMemoReactionsResponse\n\t2,  // 61: memos.api.v1.MemoService.UpsertMemoReaction:output_type -> memos.api.v1.Reaction\n\t37, // 62: memos.api.v1.MemoService.DeleteMemoReaction:output_type -> google.protobuf.Empty\n\t25, // 63: memos.api.v1.MemoService.CreateMemoShare:output_type -> memos.api.v1.MemoShare\n\t28, // 64: memos.api.v1.MemoService.ListMemoShares:output_type -> memos.api.v1.ListMemoSharesResponse\n\t37, // 65: memos.api.v1.MemoService.DeleteMemoShare:output_type -> google.protobuf.Empty\n\t3,  // 66: memos.api.v1.MemoService.GetMemoByShare:output_type -> memos.api.v1.Memo\n\t49, // [49:67] is the sub-list for method output_type\n\t31, // [31:49] is the sub-list for method input_type\n\t31, // [31:31] is the sub-list for extension type_name\n\t31, // [31:31] is the sub-list for extension extendee\n\t0,  // [0:31] is the sub-list for field type_name\n}\n\nfunc init() { file_api_v1_memo_service_proto_init() }\nfunc file_api_v1_memo_service_proto_init() {\n\tif File_api_v1_memo_service_proto != nil {\n\t\treturn\n\t}\n\tfile_api_v1_attachment_service_proto_init()\n\tfile_api_v1_common_proto_init()\n\tfile_api_v1_memo_service_proto_msgTypes[1].OneofWrappers = []any{}\n\tfile_api_v1_memo_service_proto_msgTypes[23].OneofWrappers = []any{}\n\ttype x struct{}\n\tout := protoimpl.TypeBuilder{\n\t\tFile: protoimpl.DescBuilder{\n\t\t\tGoPackagePath: reflect.TypeOf(x{}).PkgPath(),\n\t\t\tRawDescriptor: unsafe.Slice(unsafe.StringData(file_api_v1_memo_service_proto_rawDesc), len(file_api_v1_memo_service_proto_rawDesc)),\n\t\t\tNumEnums:      2,\n\t\t\tNumMessages:   31,\n\t\t\tNumExtensions: 0,\n\t\t\tNumServices:   1,\n\t\t},\n\t\tGoTypes:           file_api_v1_memo_service_proto_goTypes,\n\t\tDependencyIndexes: file_api_v1_memo_service_proto_depIdxs,\n\t\tEnumInfos:         file_api_v1_memo_service_proto_enumTypes,\n\t\tMessageInfos:      file_api_v1_memo_service_proto_msgTypes,\n\t}.Build()\n\tFile_api_v1_memo_service_proto = out.File\n\tfile_api_v1_memo_service_proto_goTypes = nil\n\tfile_api_v1_memo_service_proto_depIdxs = nil\n}\n"
  },
  {
    "path": "proto/gen/api/v1/memo_service.pb.gw.go",
    "content": "// Code generated by protoc-gen-grpc-gateway. DO NOT EDIT.\n// source: api/v1/memo_service.proto\n\n/*\nPackage apiv1 is a reverse proxy.\n\nIt translates gRPC into RESTful JSON APIs.\n*/\npackage apiv1\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"io\"\n\t\"net/http\"\n\n\t\"github.com/grpc-ecosystem/grpc-gateway/v2/runtime\"\n\t\"github.com/grpc-ecosystem/grpc-gateway/v2/utilities\"\n\t\"google.golang.org/grpc\"\n\t\"google.golang.org/grpc/codes\"\n\t\"google.golang.org/grpc/grpclog\"\n\t\"google.golang.org/grpc/metadata\"\n\t\"google.golang.org/grpc/status\"\n\t\"google.golang.org/protobuf/proto\"\n)\n\n// Suppress \"imported and not used\" errors\nvar (\n\t_ codes.Code\n\t_ io.Reader\n\t_ status.Status\n\t_ = errors.New\n\t_ = runtime.String\n\t_ = utilities.NewDoubleArray\n\t_ = metadata.Join\n)\n\nvar filter_MemoService_CreateMemo_0 = &utilities.DoubleArray{Encoding: map[string]int{\"memo\": 0}, Base: []int{1, 1, 0}, Check: []int{0, 1, 2}}\n\nfunc request_MemoService_CreateMemo_0(ctx context.Context, marshaler runtime.Marshaler, client MemoServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq CreateMemoRequest\n\t\tmetadata runtime.ServerMetadata\n\t)\n\tif err := marshaler.NewDecoder(req.Body).Decode(&protoReq.Memo); err != nil && !errors.Is(err, io.EOF) {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tif req.Body != nil {\n\t\t_, _ = io.Copy(io.Discard, req.Body)\n\t}\n\tif err := req.ParseForm(); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tif err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_MemoService_CreateMemo_0); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tmsg, err := client.CreateMemo(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))\n\treturn msg, metadata, err\n}\n\nfunc local_request_MemoService_CreateMemo_0(ctx context.Context, marshaler runtime.Marshaler, server MemoServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq CreateMemoRequest\n\t\tmetadata runtime.ServerMetadata\n\t)\n\tif err := marshaler.NewDecoder(req.Body).Decode(&protoReq.Memo); err != nil && !errors.Is(err, io.EOF) {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tif err := req.ParseForm(); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tif err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_MemoService_CreateMemo_0); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tmsg, err := server.CreateMemo(ctx, &protoReq)\n\treturn msg, metadata, err\n}\n\nvar filter_MemoService_ListMemos_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)}\n\nfunc request_MemoService_ListMemos_0(ctx context.Context, marshaler runtime.Marshaler, client MemoServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq ListMemosRequest\n\t\tmetadata runtime.ServerMetadata\n\t)\n\tif req.Body != nil {\n\t\t_, _ = io.Copy(io.Discard, req.Body)\n\t}\n\tif err := req.ParseForm(); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tif err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_MemoService_ListMemos_0); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tmsg, err := client.ListMemos(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))\n\treturn msg, metadata, err\n}\n\nfunc local_request_MemoService_ListMemos_0(ctx context.Context, marshaler runtime.Marshaler, server MemoServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq ListMemosRequest\n\t\tmetadata runtime.ServerMetadata\n\t)\n\tif err := req.ParseForm(); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tif err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_MemoService_ListMemos_0); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tmsg, err := server.ListMemos(ctx, &protoReq)\n\treturn msg, metadata, err\n}\n\nfunc request_MemoService_GetMemo_0(ctx context.Context, marshaler runtime.Marshaler, client MemoServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq GetMemoRequest\n\t\tmetadata runtime.ServerMetadata\n\t\terr      error\n\t)\n\tif req.Body != nil {\n\t\t_, _ = io.Copy(io.Discard, req.Body)\n\t}\n\tval, ok := pathParams[\"name\"]\n\tif !ok {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"missing parameter %s\", \"name\")\n\t}\n\tprotoReq.Name, err = runtime.String(val)\n\tif err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"type mismatch, parameter: %s, error: %v\", \"name\", err)\n\t}\n\tmsg, err := client.GetMemo(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))\n\treturn msg, metadata, err\n}\n\nfunc local_request_MemoService_GetMemo_0(ctx context.Context, marshaler runtime.Marshaler, server MemoServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq GetMemoRequest\n\t\tmetadata runtime.ServerMetadata\n\t\terr      error\n\t)\n\tval, ok := pathParams[\"name\"]\n\tif !ok {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"missing parameter %s\", \"name\")\n\t}\n\tprotoReq.Name, err = runtime.String(val)\n\tif err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"type mismatch, parameter: %s, error: %v\", \"name\", err)\n\t}\n\tmsg, err := server.GetMemo(ctx, &protoReq)\n\treturn msg, metadata, err\n}\n\nvar filter_MemoService_UpdateMemo_0 = &utilities.DoubleArray{Encoding: map[string]int{\"memo\": 0, \"name\": 1}, Base: []int{1, 2, 1, 0, 0}, Check: []int{0, 1, 2, 3, 2}}\n\nfunc request_MemoService_UpdateMemo_0(ctx context.Context, marshaler runtime.Marshaler, client MemoServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq UpdateMemoRequest\n\t\tmetadata runtime.ServerMetadata\n\t\terr      error\n\t)\n\tnewReader, berr := utilities.IOReaderFactory(req.Body)\n\tif berr != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", berr)\n\t}\n\tif err := marshaler.NewDecoder(newReader()).Decode(&protoReq.Memo); err != nil && !errors.Is(err, io.EOF) {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tif req.Body != nil {\n\t\t_, _ = io.Copy(io.Discard, req.Body)\n\t}\n\tif protoReq.UpdateMask == nil || len(protoReq.UpdateMask.GetPaths()) == 0 {\n\t\tif fieldMask, err := runtime.FieldMaskFromRequestBody(newReader(), protoReq.Memo); err != nil {\n\t\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t\t} else {\n\t\t\tprotoReq.UpdateMask = fieldMask\n\t\t}\n\t}\n\tval, ok := pathParams[\"memo.name\"]\n\tif !ok {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"missing parameter %s\", \"memo.name\")\n\t}\n\terr = runtime.PopulateFieldFromPath(&protoReq, \"memo.name\", val)\n\tif err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"type mismatch, parameter: %s, error: %v\", \"memo.name\", err)\n\t}\n\tif err := req.ParseForm(); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tif err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_MemoService_UpdateMemo_0); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tmsg, err := client.UpdateMemo(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))\n\treturn msg, metadata, err\n}\n\nfunc local_request_MemoService_UpdateMemo_0(ctx context.Context, marshaler runtime.Marshaler, server MemoServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq UpdateMemoRequest\n\t\tmetadata runtime.ServerMetadata\n\t\terr      error\n\t)\n\tnewReader, berr := utilities.IOReaderFactory(req.Body)\n\tif berr != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", berr)\n\t}\n\tif err := marshaler.NewDecoder(newReader()).Decode(&protoReq.Memo); err != nil && !errors.Is(err, io.EOF) {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tif protoReq.UpdateMask == nil || len(protoReq.UpdateMask.GetPaths()) == 0 {\n\t\tif fieldMask, err := runtime.FieldMaskFromRequestBody(newReader(), protoReq.Memo); err != nil {\n\t\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t\t} else {\n\t\t\tprotoReq.UpdateMask = fieldMask\n\t\t}\n\t}\n\tval, ok := pathParams[\"memo.name\"]\n\tif !ok {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"missing parameter %s\", \"memo.name\")\n\t}\n\terr = runtime.PopulateFieldFromPath(&protoReq, \"memo.name\", val)\n\tif err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"type mismatch, parameter: %s, error: %v\", \"memo.name\", err)\n\t}\n\tif err := req.ParseForm(); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tif err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_MemoService_UpdateMemo_0); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tmsg, err := server.UpdateMemo(ctx, &protoReq)\n\treturn msg, metadata, err\n}\n\nvar filter_MemoService_DeleteMemo_0 = &utilities.DoubleArray{Encoding: map[string]int{\"name\": 0}, Base: []int{1, 1, 0}, Check: []int{0, 1, 2}}\n\nfunc request_MemoService_DeleteMemo_0(ctx context.Context, marshaler runtime.Marshaler, client MemoServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq DeleteMemoRequest\n\t\tmetadata runtime.ServerMetadata\n\t\terr      error\n\t)\n\tif req.Body != nil {\n\t\t_, _ = io.Copy(io.Discard, req.Body)\n\t}\n\tval, ok := pathParams[\"name\"]\n\tif !ok {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"missing parameter %s\", \"name\")\n\t}\n\tprotoReq.Name, err = runtime.String(val)\n\tif err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"type mismatch, parameter: %s, error: %v\", \"name\", err)\n\t}\n\tif err := req.ParseForm(); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tif err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_MemoService_DeleteMemo_0); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tmsg, err := client.DeleteMemo(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))\n\treturn msg, metadata, err\n}\n\nfunc local_request_MemoService_DeleteMemo_0(ctx context.Context, marshaler runtime.Marshaler, server MemoServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq DeleteMemoRequest\n\t\tmetadata runtime.ServerMetadata\n\t\terr      error\n\t)\n\tval, ok := pathParams[\"name\"]\n\tif !ok {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"missing parameter %s\", \"name\")\n\t}\n\tprotoReq.Name, err = runtime.String(val)\n\tif err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"type mismatch, parameter: %s, error: %v\", \"name\", err)\n\t}\n\tif err := req.ParseForm(); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tif err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_MemoService_DeleteMemo_0); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tmsg, err := server.DeleteMemo(ctx, &protoReq)\n\treturn msg, metadata, err\n}\n\nfunc request_MemoService_SetMemoAttachments_0(ctx context.Context, marshaler runtime.Marshaler, client MemoServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq SetMemoAttachmentsRequest\n\t\tmetadata runtime.ServerMetadata\n\t\terr      error\n\t)\n\tif err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tif req.Body != nil {\n\t\t_, _ = io.Copy(io.Discard, req.Body)\n\t}\n\tval, ok := pathParams[\"name\"]\n\tif !ok {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"missing parameter %s\", \"name\")\n\t}\n\tprotoReq.Name, err = runtime.String(val)\n\tif err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"type mismatch, parameter: %s, error: %v\", \"name\", err)\n\t}\n\tmsg, err := client.SetMemoAttachments(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))\n\treturn msg, metadata, err\n}\n\nfunc local_request_MemoService_SetMemoAttachments_0(ctx context.Context, marshaler runtime.Marshaler, server MemoServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq SetMemoAttachmentsRequest\n\t\tmetadata runtime.ServerMetadata\n\t\terr      error\n\t)\n\tif err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tval, ok := pathParams[\"name\"]\n\tif !ok {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"missing parameter %s\", \"name\")\n\t}\n\tprotoReq.Name, err = runtime.String(val)\n\tif err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"type mismatch, parameter: %s, error: %v\", \"name\", err)\n\t}\n\tmsg, err := server.SetMemoAttachments(ctx, &protoReq)\n\treturn msg, metadata, err\n}\n\nvar filter_MemoService_ListMemoAttachments_0 = &utilities.DoubleArray{Encoding: map[string]int{\"name\": 0}, Base: []int{1, 1, 0}, Check: []int{0, 1, 2}}\n\nfunc request_MemoService_ListMemoAttachments_0(ctx context.Context, marshaler runtime.Marshaler, client MemoServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq ListMemoAttachmentsRequest\n\t\tmetadata runtime.ServerMetadata\n\t\terr      error\n\t)\n\tif req.Body != nil {\n\t\t_, _ = io.Copy(io.Discard, req.Body)\n\t}\n\tval, ok := pathParams[\"name\"]\n\tif !ok {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"missing parameter %s\", \"name\")\n\t}\n\tprotoReq.Name, err = runtime.String(val)\n\tif err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"type mismatch, parameter: %s, error: %v\", \"name\", err)\n\t}\n\tif err := req.ParseForm(); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tif err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_MemoService_ListMemoAttachments_0); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tmsg, err := client.ListMemoAttachments(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))\n\treturn msg, metadata, err\n}\n\nfunc local_request_MemoService_ListMemoAttachments_0(ctx context.Context, marshaler runtime.Marshaler, server MemoServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq ListMemoAttachmentsRequest\n\t\tmetadata runtime.ServerMetadata\n\t\terr      error\n\t)\n\tval, ok := pathParams[\"name\"]\n\tif !ok {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"missing parameter %s\", \"name\")\n\t}\n\tprotoReq.Name, err = runtime.String(val)\n\tif err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"type mismatch, parameter: %s, error: %v\", \"name\", err)\n\t}\n\tif err := req.ParseForm(); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tif err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_MemoService_ListMemoAttachments_0); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tmsg, err := server.ListMemoAttachments(ctx, &protoReq)\n\treturn msg, metadata, err\n}\n\nfunc request_MemoService_SetMemoRelations_0(ctx context.Context, marshaler runtime.Marshaler, client MemoServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq SetMemoRelationsRequest\n\t\tmetadata runtime.ServerMetadata\n\t\terr      error\n\t)\n\tif err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tif req.Body != nil {\n\t\t_, _ = io.Copy(io.Discard, req.Body)\n\t}\n\tval, ok := pathParams[\"name\"]\n\tif !ok {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"missing parameter %s\", \"name\")\n\t}\n\tprotoReq.Name, err = runtime.String(val)\n\tif err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"type mismatch, parameter: %s, error: %v\", \"name\", err)\n\t}\n\tmsg, err := client.SetMemoRelations(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))\n\treturn msg, metadata, err\n}\n\nfunc local_request_MemoService_SetMemoRelations_0(ctx context.Context, marshaler runtime.Marshaler, server MemoServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq SetMemoRelationsRequest\n\t\tmetadata runtime.ServerMetadata\n\t\terr      error\n\t)\n\tif err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tval, ok := pathParams[\"name\"]\n\tif !ok {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"missing parameter %s\", \"name\")\n\t}\n\tprotoReq.Name, err = runtime.String(val)\n\tif err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"type mismatch, parameter: %s, error: %v\", \"name\", err)\n\t}\n\tmsg, err := server.SetMemoRelations(ctx, &protoReq)\n\treturn msg, metadata, err\n}\n\nvar filter_MemoService_ListMemoRelations_0 = &utilities.DoubleArray{Encoding: map[string]int{\"name\": 0}, Base: []int{1, 1, 0}, Check: []int{0, 1, 2}}\n\nfunc request_MemoService_ListMemoRelations_0(ctx context.Context, marshaler runtime.Marshaler, client MemoServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq ListMemoRelationsRequest\n\t\tmetadata runtime.ServerMetadata\n\t\terr      error\n\t)\n\tif req.Body != nil {\n\t\t_, _ = io.Copy(io.Discard, req.Body)\n\t}\n\tval, ok := pathParams[\"name\"]\n\tif !ok {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"missing parameter %s\", \"name\")\n\t}\n\tprotoReq.Name, err = runtime.String(val)\n\tif err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"type mismatch, parameter: %s, error: %v\", \"name\", err)\n\t}\n\tif err := req.ParseForm(); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tif err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_MemoService_ListMemoRelations_0); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tmsg, err := client.ListMemoRelations(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))\n\treturn msg, metadata, err\n}\n\nfunc local_request_MemoService_ListMemoRelations_0(ctx context.Context, marshaler runtime.Marshaler, server MemoServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq ListMemoRelationsRequest\n\t\tmetadata runtime.ServerMetadata\n\t\terr      error\n\t)\n\tval, ok := pathParams[\"name\"]\n\tif !ok {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"missing parameter %s\", \"name\")\n\t}\n\tprotoReq.Name, err = runtime.String(val)\n\tif err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"type mismatch, parameter: %s, error: %v\", \"name\", err)\n\t}\n\tif err := req.ParseForm(); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tif err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_MemoService_ListMemoRelations_0); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tmsg, err := server.ListMemoRelations(ctx, &protoReq)\n\treturn msg, metadata, err\n}\n\nvar filter_MemoService_CreateMemoComment_0 = &utilities.DoubleArray{Encoding: map[string]int{\"comment\": 0, \"name\": 1}, Base: []int{1, 1, 2, 0, 0}, Check: []int{0, 1, 1, 2, 3}}\n\nfunc request_MemoService_CreateMemoComment_0(ctx context.Context, marshaler runtime.Marshaler, client MemoServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq CreateMemoCommentRequest\n\t\tmetadata runtime.ServerMetadata\n\t\terr      error\n\t)\n\tif err := marshaler.NewDecoder(req.Body).Decode(&protoReq.Comment); err != nil && !errors.Is(err, io.EOF) {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tif req.Body != nil {\n\t\t_, _ = io.Copy(io.Discard, req.Body)\n\t}\n\tval, ok := pathParams[\"name\"]\n\tif !ok {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"missing parameter %s\", \"name\")\n\t}\n\tprotoReq.Name, err = runtime.String(val)\n\tif err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"type mismatch, parameter: %s, error: %v\", \"name\", err)\n\t}\n\tif err := req.ParseForm(); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tif err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_MemoService_CreateMemoComment_0); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tmsg, err := client.CreateMemoComment(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))\n\treturn msg, metadata, err\n}\n\nfunc local_request_MemoService_CreateMemoComment_0(ctx context.Context, marshaler runtime.Marshaler, server MemoServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq CreateMemoCommentRequest\n\t\tmetadata runtime.ServerMetadata\n\t\terr      error\n\t)\n\tif err := marshaler.NewDecoder(req.Body).Decode(&protoReq.Comment); err != nil && !errors.Is(err, io.EOF) {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tval, ok := pathParams[\"name\"]\n\tif !ok {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"missing parameter %s\", \"name\")\n\t}\n\tprotoReq.Name, err = runtime.String(val)\n\tif err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"type mismatch, parameter: %s, error: %v\", \"name\", err)\n\t}\n\tif err := req.ParseForm(); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tif err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_MemoService_CreateMemoComment_0); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tmsg, err := server.CreateMemoComment(ctx, &protoReq)\n\treturn msg, metadata, err\n}\n\nvar filter_MemoService_ListMemoComments_0 = &utilities.DoubleArray{Encoding: map[string]int{\"name\": 0}, Base: []int{1, 1, 0}, Check: []int{0, 1, 2}}\n\nfunc request_MemoService_ListMemoComments_0(ctx context.Context, marshaler runtime.Marshaler, client MemoServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq ListMemoCommentsRequest\n\t\tmetadata runtime.ServerMetadata\n\t\terr      error\n\t)\n\tif req.Body != nil {\n\t\t_, _ = io.Copy(io.Discard, req.Body)\n\t}\n\tval, ok := pathParams[\"name\"]\n\tif !ok {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"missing parameter %s\", \"name\")\n\t}\n\tprotoReq.Name, err = runtime.String(val)\n\tif err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"type mismatch, parameter: %s, error: %v\", \"name\", err)\n\t}\n\tif err := req.ParseForm(); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tif err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_MemoService_ListMemoComments_0); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tmsg, err := client.ListMemoComments(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))\n\treturn msg, metadata, err\n}\n\nfunc local_request_MemoService_ListMemoComments_0(ctx context.Context, marshaler runtime.Marshaler, server MemoServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq ListMemoCommentsRequest\n\t\tmetadata runtime.ServerMetadata\n\t\terr      error\n\t)\n\tval, ok := pathParams[\"name\"]\n\tif !ok {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"missing parameter %s\", \"name\")\n\t}\n\tprotoReq.Name, err = runtime.String(val)\n\tif err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"type mismatch, parameter: %s, error: %v\", \"name\", err)\n\t}\n\tif err := req.ParseForm(); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tif err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_MemoService_ListMemoComments_0); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tmsg, err := server.ListMemoComments(ctx, &protoReq)\n\treturn msg, metadata, err\n}\n\nvar filter_MemoService_ListMemoReactions_0 = &utilities.DoubleArray{Encoding: map[string]int{\"name\": 0}, Base: []int{1, 1, 0}, Check: []int{0, 1, 2}}\n\nfunc request_MemoService_ListMemoReactions_0(ctx context.Context, marshaler runtime.Marshaler, client MemoServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq ListMemoReactionsRequest\n\t\tmetadata runtime.ServerMetadata\n\t\terr      error\n\t)\n\tif req.Body != nil {\n\t\t_, _ = io.Copy(io.Discard, req.Body)\n\t}\n\tval, ok := pathParams[\"name\"]\n\tif !ok {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"missing parameter %s\", \"name\")\n\t}\n\tprotoReq.Name, err = runtime.String(val)\n\tif err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"type mismatch, parameter: %s, error: %v\", \"name\", err)\n\t}\n\tif err := req.ParseForm(); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tif err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_MemoService_ListMemoReactions_0); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tmsg, err := client.ListMemoReactions(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))\n\treturn msg, metadata, err\n}\n\nfunc local_request_MemoService_ListMemoReactions_0(ctx context.Context, marshaler runtime.Marshaler, server MemoServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq ListMemoReactionsRequest\n\t\tmetadata runtime.ServerMetadata\n\t\terr      error\n\t)\n\tval, ok := pathParams[\"name\"]\n\tif !ok {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"missing parameter %s\", \"name\")\n\t}\n\tprotoReq.Name, err = runtime.String(val)\n\tif err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"type mismatch, parameter: %s, error: %v\", \"name\", err)\n\t}\n\tif err := req.ParseForm(); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tif err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_MemoService_ListMemoReactions_0); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tmsg, err := server.ListMemoReactions(ctx, &protoReq)\n\treturn msg, metadata, err\n}\n\nfunc request_MemoService_UpsertMemoReaction_0(ctx context.Context, marshaler runtime.Marshaler, client MemoServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq UpsertMemoReactionRequest\n\t\tmetadata runtime.ServerMetadata\n\t\terr      error\n\t)\n\tif err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tif req.Body != nil {\n\t\t_, _ = io.Copy(io.Discard, req.Body)\n\t}\n\tval, ok := pathParams[\"name\"]\n\tif !ok {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"missing parameter %s\", \"name\")\n\t}\n\tprotoReq.Name, err = runtime.String(val)\n\tif err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"type mismatch, parameter: %s, error: %v\", \"name\", err)\n\t}\n\tmsg, err := client.UpsertMemoReaction(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))\n\treturn msg, metadata, err\n}\n\nfunc local_request_MemoService_UpsertMemoReaction_0(ctx context.Context, marshaler runtime.Marshaler, server MemoServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq UpsertMemoReactionRequest\n\t\tmetadata runtime.ServerMetadata\n\t\terr      error\n\t)\n\tif err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tval, ok := pathParams[\"name\"]\n\tif !ok {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"missing parameter %s\", \"name\")\n\t}\n\tprotoReq.Name, err = runtime.String(val)\n\tif err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"type mismatch, parameter: %s, error: %v\", \"name\", err)\n\t}\n\tmsg, err := server.UpsertMemoReaction(ctx, &protoReq)\n\treturn msg, metadata, err\n}\n\nfunc request_MemoService_DeleteMemoReaction_0(ctx context.Context, marshaler runtime.Marshaler, client MemoServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq DeleteMemoReactionRequest\n\t\tmetadata runtime.ServerMetadata\n\t\terr      error\n\t)\n\tif req.Body != nil {\n\t\t_, _ = io.Copy(io.Discard, req.Body)\n\t}\n\tval, ok := pathParams[\"name\"]\n\tif !ok {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"missing parameter %s\", \"name\")\n\t}\n\tprotoReq.Name, err = runtime.String(val)\n\tif err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"type mismatch, parameter: %s, error: %v\", \"name\", err)\n\t}\n\tmsg, err := client.DeleteMemoReaction(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))\n\treturn msg, metadata, err\n}\n\nfunc local_request_MemoService_DeleteMemoReaction_0(ctx context.Context, marshaler runtime.Marshaler, server MemoServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq DeleteMemoReactionRequest\n\t\tmetadata runtime.ServerMetadata\n\t\terr      error\n\t)\n\tval, ok := pathParams[\"name\"]\n\tif !ok {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"missing parameter %s\", \"name\")\n\t}\n\tprotoReq.Name, err = runtime.String(val)\n\tif err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"type mismatch, parameter: %s, error: %v\", \"name\", err)\n\t}\n\tmsg, err := server.DeleteMemoReaction(ctx, &protoReq)\n\treturn msg, metadata, err\n}\n\nfunc request_MemoService_CreateMemoShare_0(ctx context.Context, marshaler runtime.Marshaler, client MemoServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq CreateMemoShareRequest\n\t\tmetadata runtime.ServerMetadata\n\t\terr      error\n\t)\n\tif err := marshaler.NewDecoder(req.Body).Decode(&protoReq.MemoShare); err != nil && !errors.Is(err, io.EOF) {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tif req.Body != nil {\n\t\t_, _ = io.Copy(io.Discard, req.Body)\n\t}\n\tval, ok := pathParams[\"parent\"]\n\tif !ok {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"missing parameter %s\", \"parent\")\n\t}\n\tprotoReq.Parent, err = runtime.String(val)\n\tif err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"type mismatch, parameter: %s, error: %v\", \"parent\", err)\n\t}\n\tmsg, err := client.CreateMemoShare(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))\n\treturn msg, metadata, err\n}\n\nfunc local_request_MemoService_CreateMemoShare_0(ctx context.Context, marshaler runtime.Marshaler, server MemoServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq CreateMemoShareRequest\n\t\tmetadata runtime.ServerMetadata\n\t\terr      error\n\t)\n\tif err := marshaler.NewDecoder(req.Body).Decode(&protoReq.MemoShare); err != nil && !errors.Is(err, io.EOF) {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tval, ok := pathParams[\"parent\"]\n\tif !ok {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"missing parameter %s\", \"parent\")\n\t}\n\tprotoReq.Parent, err = runtime.String(val)\n\tif err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"type mismatch, parameter: %s, error: %v\", \"parent\", err)\n\t}\n\tmsg, err := server.CreateMemoShare(ctx, &protoReq)\n\treturn msg, metadata, err\n}\n\nfunc request_MemoService_ListMemoShares_0(ctx context.Context, marshaler runtime.Marshaler, client MemoServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq ListMemoSharesRequest\n\t\tmetadata runtime.ServerMetadata\n\t\terr      error\n\t)\n\tif req.Body != nil {\n\t\t_, _ = io.Copy(io.Discard, req.Body)\n\t}\n\tval, ok := pathParams[\"parent\"]\n\tif !ok {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"missing parameter %s\", \"parent\")\n\t}\n\tprotoReq.Parent, err = runtime.String(val)\n\tif err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"type mismatch, parameter: %s, error: %v\", \"parent\", err)\n\t}\n\tmsg, err := client.ListMemoShares(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))\n\treturn msg, metadata, err\n}\n\nfunc local_request_MemoService_ListMemoShares_0(ctx context.Context, marshaler runtime.Marshaler, server MemoServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq ListMemoSharesRequest\n\t\tmetadata runtime.ServerMetadata\n\t\terr      error\n\t)\n\tval, ok := pathParams[\"parent\"]\n\tif !ok {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"missing parameter %s\", \"parent\")\n\t}\n\tprotoReq.Parent, err = runtime.String(val)\n\tif err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"type mismatch, parameter: %s, error: %v\", \"parent\", err)\n\t}\n\tmsg, err := server.ListMemoShares(ctx, &protoReq)\n\treturn msg, metadata, err\n}\n\nfunc request_MemoService_DeleteMemoShare_0(ctx context.Context, marshaler runtime.Marshaler, client MemoServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq DeleteMemoShareRequest\n\t\tmetadata runtime.ServerMetadata\n\t\terr      error\n\t)\n\tif req.Body != nil {\n\t\t_, _ = io.Copy(io.Discard, req.Body)\n\t}\n\tval, ok := pathParams[\"name\"]\n\tif !ok {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"missing parameter %s\", \"name\")\n\t}\n\tprotoReq.Name, err = runtime.String(val)\n\tif err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"type mismatch, parameter: %s, error: %v\", \"name\", err)\n\t}\n\tmsg, err := client.DeleteMemoShare(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))\n\treturn msg, metadata, err\n}\n\nfunc local_request_MemoService_DeleteMemoShare_0(ctx context.Context, marshaler runtime.Marshaler, server MemoServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq DeleteMemoShareRequest\n\t\tmetadata runtime.ServerMetadata\n\t\terr      error\n\t)\n\tval, ok := pathParams[\"name\"]\n\tif !ok {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"missing parameter %s\", \"name\")\n\t}\n\tprotoReq.Name, err = runtime.String(val)\n\tif err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"type mismatch, parameter: %s, error: %v\", \"name\", err)\n\t}\n\tmsg, err := server.DeleteMemoShare(ctx, &protoReq)\n\treturn msg, metadata, err\n}\n\nfunc request_MemoService_GetMemoByShare_0(ctx context.Context, marshaler runtime.Marshaler, client MemoServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq GetMemoByShareRequest\n\t\tmetadata runtime.ServerMetadata\n\t\terr      error\n\t)\n\tif req.Body != nil {\n\t\t_, _ = io.Copy(io.Discard, req.Body)\n\t}\n\tval, ok := pathParams[\"share_id\"]\n\tif !ok {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"missing parameter %s\", \"share_id\")\n\t}\n\tprotoReq.ShareId, err = runtime.String(val)\n\tif err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"type mismatch, parameter: %s, error: %v\", \"share_id\", err)\n\t}\n\tmsg, err := client.GetMemoByShare(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))\n\treturn msg, metadata, err\n}\n\nfunc local_request_MemoService_GetMemoByShare_0(ctx context.Context, marshaler runtime.Marshaler, server MemoServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq GetMemoByShareRequest\n\t\tmetadata runtime.ServerMetadata\n\t\terr      error\n\t)\n\tval, ok := pathParams[\"share_id\"]\n\tif !ok {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"missing parameter %s\", \"share_id\")\n\t}\n\tprotoReq.ShareId, err = runtime.String(val)\n\tif err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"type mismatch, parameter: %s, error: %v\", \"share_id\", err)\n\t}\n\tmsg, err := server.GetMemoByShare(ctx, &protoReq)\n\treturn msg, metadata, err\n}\n\n// RegisterMemoServiceHandlerServer registers the http handlers for service MemoService to \"mux\".\n// UnaryRPC     :call MemoServiceServer directly.\n// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906.\n// Note that using this registration option will cause many gRPC library features to stop working. Consider using RegisterMemoServiceHandlerFromEndpoint instead.\n// GRPC interceptors will not work for this type of registration. To use interceptors, you must use the \"runtime.WithMiddlewares\" option in the \"runtime.NewServeMux\" call.\nfunc RegisterMemoServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux, server MemoServiceServer) error {\n\tmux.Handle(http.MethodPost, pattern_MemoService_CreateMemo_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tvar stream runtime.ServerTransportStream\n\t\tctx = grpc.NewContextWithServerTransportStream(ctx, &stream)\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, \"/memos.api.v1.MemoService/CreateMemo\", runtime.WithHTTPPathPattern(\"/api/v1/memos\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := local_request_MemoService_CreateMemo_0(annotatedContext, inboundMarshaler, server, req, pathParams)\n\t\tmd.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_MemoService_CreateMemo_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodGet, pattern_MemoService_ListMemos_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tvar stream runtime.ServerTransportStream\n\t\tctx = grpc.NewContextWithServerTransportStream(ctx, &stream)\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, \"/memos.api.v1.MemoService/ListMemos\", runtime.WithHTTPPathPattern(\"/api/v1/memos\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := local_request_MemoService_ListMemos_0(annotatedContext, inboundMarshaler, server, req, pathParams)\n\t\tmd.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_MemoService_ListMemos_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodGet, pattern_MemoService_GetMemo_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tvar stream runtime.ServerTransportStream\n\t\tctx = grpc.NewContextWithServerTransportStream(ctx, &stream)\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, \"/memos.api.v1.MemoService/GetMemo\", runtime.WithHTTPPathPattern(\"/api/v1/{name=memos/*}\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := local_request_MemoService_GetMemo_0(annotatedContext, inboundMarshaler, server, req, pathParams)\n\t\tmd.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_MemoService_GetMemo_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodPatch, pattern_MemoService_UpdateMemo_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tvar stream runtime.ServerTransportStream\n\t\tctx = grpc.NewContextWithServerTransportStream(ctx, &stream)\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, \"/memos.api.v1.MemoService/UpdateMemo\", runtime.WithHTTPPathPattern(\"/api/v1/{memo.name=memos/*}\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := local_request_MemoService_UpdateMemo_0(annotatedContext, inboundMarshaler, server, req, pathParams)\n\t\tmd.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_MemoService_UpdateMemo_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodDelete, pattern_MemoService_DeleteMemo_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tvar stream runtime.ServerTransportStream\n\t\tctx = grpc.NewContextWithServerTransportStream(ctx, &stream)\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, \"/memos.api.v1.MemoService/DeleteMemo\", runtime.WithHTTPPathPattern(\"/api/v1/{name=memos/*}\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := local_request_MemoService_DeleteMemo_0(annotatedContext, inboundMarshaler, server, req, pathParams)\n\t\tmd.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_MemoService_DeleteMemo_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodPatch, pattern_MemoService_SetMemoAttachments_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tvar stream runtime.ServerTransportStream\n\t\tctx = grpc.NewContextWithServerTransportStream(ctx, &stream)\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, \"/memos.api.v1.MemoService/SetMemoAttachments\", runtime.WithHTTPPathPattern(\"/api/v1/{name=memos/*}/attachments\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := local_request_MemoService_SetMemoAttachments_0(annotatedContext, inboundMarshaler, server, req, pathParams)\n\t\tmd.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_MemoService_SetMemoAttachments_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodGet, pattern_MemoService_ListMemoAttachments_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tvar stream runtime.ServerTransportStream\n\t\tctx = grpc.NewContextWithServerTransportStream(ctx, &stream)\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, \"/memos.api.v1.MemoService/ListMemoAttachments\", runtime.WithHTTPPathPattern(\"/api/v1/{name=memos/*}/attachments\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := local_request_MemoService_ListMemoAttachments_0(annotatedContext, inboundMarshaler, server, req, pathParams)\n\t\tmd.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_MemoService_ListMemoAttachments_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodPatch, pattern_MemoService_SetMemoRelations_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tvar stream runtime.ServerTransportStream\n\t\tctx = grpc.NewContextWithServerTransportStream(ctx, &stream)\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, \"/memos.api.v1.MemoService/SetMemoRelations\", runtime.WithHTTPPathPattern(\"/api/v1/{name=memos/*}/relations\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := local_request_MemoService_SetMemoRelations_0(annotatedContext, inboundMarshaler, server, req, pathParams)\n\t\tmd.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_MemoService_SetMemoRelations_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodGet, pattern_MemoService_ListMemoRelations_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tvar stream runtime.ServerTransportStream\n\t\tctx = grpc.NewContextWithServerTransportStream(ctx, &stream)\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, \"/memos.api.v1.MemoService/ListMemoRelations\", runtime.WithHTTPPathPattern(\"/api/v1/{name=memos/*}/relations\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := local_request_MemoService_ListMemoRelations_0(annotatedContext, inboundMarshaler, server, req, pathParams)\n\t\tmd.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_MemoService_ListMemoRelations_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodPost, pattern_MemoService_CreateMemoComment_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tvar stream runtime.ServerTransportStream\n\t\tctx = grpc.NewContextWithServerTransportStream(ctx, &stream)\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, \"/memos.api.v1.MemoService/CreateMemoComment\", runtime.WithHTTPPathPattern(\"/api/v1/{name=memos/*}/comments\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := local_request_MemoService_CreateMemoComment_0(annotatedContext, inboundMarshaler, server, req, pathParams)\n\t\tmd.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_MemoService_CreateMemoComment_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodGet, pattern_MemoService_ListMemoComments_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tvar stream runtime.ServerTransportStream\n\t\tctx = grpc.NewContextWithServerTransportStream(ctx, &stream)\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, \"/memos.api.v1.MemoService/ListMemoComments\", runtime.WithHTTPPathPattern(\"/api/v1/{name=memos/*}/comments\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := local_request_MemoService_ListMemoComments_0(annotatedContext, inboundMarshaler, server, req, pathParams)\n\t\tmd.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_MemoService_ListMemoComments_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodGet, pattern_MemoService_ListMemoReactions_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tvar stream runtime.ServerTransportStream\n\t\tctx = grpc.NewContextWithServerTransportStream(ctx, &stream)\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, \"/memos.api.v1.MemoService/ListMemoReactions\", runtime.WithHTTPPathPattern(\"/api/v1/{name=memos/*}/reactions\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := local_request_MemoService_ListMemoReactions_0(annotatedContext, inboundMarshaler, server, req, pathParams)\n\t\tmd.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_MemoService_ListMemoReactions_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodPost, pattern_MemoService_UpsertMemoReaction_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tvar stream runtime.ServerTransportStream\n\t\tctx = grpc.NewContextWithServerTransportStream(ctx, &stream)\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, \"/memos.api.v1.MemoService/UpsertMemoReaction\", runtime.WithHTTPPathPattern(\"/api/v1/{name=memos/*}/reactions\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := local_request_MemoService_UpsertMemoReaction_0(annotatedContext, inboundMarshaler, server, req, pathParams)\n\t\tmd.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_MemoService_UpsertMemoReaction_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodDelete, pattern_MemoService_DeleteMemoReaction_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tvar stream runtime.ServerTransportStream\n\t\tctx = grpc.NewContextWithServerTransportStream(ctx, &stream)\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, \"/memos.api.v1.MemoService/DeleteMemoReaction\", runtime.WithHTTPPathPattern(\"/api/v1/{name=memos/*/reactions/*}\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := local_request_MemoService_DeleteMemoReaction_0(annotatedContext, inboundMarshaler, server, req, pathParams)\n\t\tmd.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_MemoService_DeleteMemoReaction_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodPost, pattern_MemoService_CreateMemoShare_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tvar stream runtime.ServerTransportStream\n\t\tctx = grpc.NewContextWithServerTransportStream(ctx, &stream)\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, \"/memos.api.v1.MemoService/CreateMemoShare\", runtime.WithHTTPPathPattern(\"/api/v1/{parent=memos/*}/shares\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := local_request_MemoService_CreateMemoShare_0(annotatedContext, inboundMarshaler, server, req, pathParams)\n\t\tmd.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_MemoService_CreateMemoShare_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodGet, pattern_MemoService_ListMemoShares_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tvar stream runtime.ServerTransportStream\n\t\tctx = grpc.NewContextWithServerTransportStream(ctx, &stream)\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, \"/memos.api.v1.MemoService/ListMemoShares\", runtime.WithHTTPPathPattern(\"/api/v1/{parent=memos/*}/shares\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := local_request_MemoService_ListMemoShares_0(annotatedContext, inboundMarshaler, server, req, pathParams)\n\t\tmd.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_MemoService_ListMemoShares_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodDelete, pattern_MemoService_DeleteMemoShare_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tvar stream runtime.ServerTransportStream\n\t\tctx = grpc.NewContextWithServerTransportStream(ctx, &stream)\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, \"/memos.api.v1.MemoService/DeleteMemoShare\", runtime.WithHTTPPathPattern(\"/api/v1/{name=memos/*/shares/*}\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := local_request_MemoService_DeleteMemoShare_0(annotatedContext, inboundMarshaler, server, req, pathParams)\n\t\tmd.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_MemoService_DeleteMemoShare_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodGet, pattern_MemoService_GetMemoByShare_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tvar stream runtime.ServerTransportStream\n\t\tctx = grpc.NewContextWithServerTransportStream(ctx, &stream)\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, \"/memos.api.v1.MemoService/GetMemoByShare\", runtime.WithHTTPPathPattern(\"/api/v1/shares/{share_id}\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := local_request_MemoService_GetMemoByShare_0(annotatedContext, inboundMarshaler, server, req, pathParams)\n\t\tmd.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_MemoService_GetMemoByShare_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\n\treturn nil\n}\n\n// RegisterMemoServiceHandlerFromEndpoint is same as RegisterMemoServiceHandler but\n// automatically dials to \"endpoint\" and closes the connection when \"ctx\" gets done.\nfunc RegisterMemoServiceHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) {\n\tconn, err := grpc.NewClient(endpoint, opts...)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer func() {\n\t\tif err != nil {\n\t\t\tif cerr := conn.Close(); cerr != nil {\n\t\t\t\tgrpclog.Errorf(\"Failed to close conn to %s: %v\", endpoint, cerr)\n\t\t\t}\n\t\t\treturn\n\t\t}\n\t\tgo func() {\n\t\t\t<-ctx.Done()\n\t\t\tif cerr := conn.Close(); cerr != nil {\n\t\t\t\tgrpclog.Errorf(\"Failed to close conn to %s: %v\", endpoint, cerr)\n\t\t\t}\n\t\t}()\n\t}()\n\treturn RegisterMemoServiceHandler(ctx, mux, conn)\n}\n\n// RegisterMemoServiceHandler registers the http handlers for service MemoService to \"mux\".\n// The handlers forward requests to the grpc endpoint over \"conn\".\nfunc RegisterMemoServiceHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error {\n\treturn RegisterMemoServiceHandlerClient(ctx, mux, NewMemoServiceClient(conn))\n}\n\n// RegisterMemoServiceHandlerClient registers the http handlers for service MemoService\n// to \"mux\". The handlers forward requests to the grpc endpoint over the given implementation of \"MemoServiceClient\".\n// Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in \"MemoServiceClient\"\n// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in\n// \"MemoServiceClient\" to call the correct interceptors. This client ignores the HTTP middlewares.\nfunc RegisterMemoServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux, client MemoServiceClient) error {\n\tmux.Handle(http.MethodPost, pattern_MemoService_CreateMemo_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateContext(ctx, mux, req, \"/memos.api.v1.MemoService/CreateMemo\", runtime.WithHTTPPathPattern(\"/api/v1/memos\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := request_MemoService_CreateMemo_0(annotatedContext, inboundMarshaler, client, req, pathParams)\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_MemoService_CreateMemo_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodGet, pattern_MemoService_ListMemos_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateContext(ctx, mux, req, \"/memos.api.v1.MemoService/ListMemos\", runtime.WithHTTPPathPattern(\"/api/v1/memos\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := request_MemoService_ListMemos_0(annotatedContext, inboundMarshaler, client, req, pathParams)\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_MemoService_ListMemos_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodGet, pattern_MemoService_GetMemo_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateContext(ctx, mux, req, \"/memos.api.v1.MemoService/GetMemo\", runtime.WithHTTPPathPattern(\"/api/v1/{name=memos/*}\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := request_MemoService_GetMemo_0(annotatedContext, inboundMarshaler, client, req, pathParams)\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_MemoService_GetMemo_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodPatch, pattern_MemoService_UpdateMemo_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateContext(ctx, mux, req, \"/memos.api.v1.MemoService/UpdateMemo\", runtime.WithHTTPPathPattern(\"/api/v1/{memo.name=memos/*}\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := request_MemoService_UpdateMemo_0(annotatedContext, inboundMarshaler, client, req, pathParams)\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_MemoService_UpdateMemo_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodDelete, pattern_MemoService_DeleteMemo_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateContext(ctx, mux, req, \"/memos.api.v1.MemoService/DeleteMemo\", runtime.WithHTTPPathPattern(\"/api/v1/{name=memos/*}\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := request_MemoService_DeleteMemo_0(annotatedContext, inboundMarshaler, client, req, pathParams)\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_MemoService_DeleteMemo_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodPatch, pattern_MemoService_SetMemoAttachments_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateContext(ctx, mux, req, \"/memos.api.v1.MemoService/SetMemoAttachments\", runtime.WithHTTPPathPattern(\"/api/v1/{name=memos/*}/attachments\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := request_MemoService_SetMemoAttachments_0(annotatedContext, inboundMarshaler, client, req, pathParams)\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_MemoService_SetMemoAttachments_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodGet, pattern_MemoService_ListMemoAttachments_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateContext(ctx, mux, req, \"/memos.api.v1.MemoService/ListMemoAttachments\", runtime.WithHTTPPathPattern(\"/api/v1/{name=memos/*}/attachments\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := request_MemoService_ListMemoAttachments_0(annotatedContext, inboundMarshaler, client, req, pathParams)\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_MemoService_ListMemoAttachments_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodPatch, pattern_MemoService_SetMemoRelations_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateContext(ctx, mux, req, \"/memos.api.v1.MemoService/SetMemoRelations\", runtime.WithHTTPPathPattern(\"/api/v1/{name=memos/*}/relations\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := request_MemoService_SetMemoRelations_0(annotatedContext, inboundMarshaler, client, req, pathParams)\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_MemoService_SetMemoRelations_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodGet, pattern_MemoService_ListMemoRelations_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateContext(ctx, mux, req, \"/memos.api.v1.MemoService/ListMemoRelations\", runtime.WithHTTPPathPattern(\"/api/v1/{name=memos/*}/relations\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := request_MemoService_ListMemoRelations_0(annotatedContext, inboundMarshaler, client, req, pathParams)\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_MemoService_ListMemoRelations_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodPost, pattern_MemoService_CreateMemoComment_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateContext(ctx, mux, req, \"/memos.api.v1.MemoService/CreateMemoComment\", runtime.WithHTTPPathPattern(\"/api/v1/{name=memos/*}/comments\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := request_MemoService_CreateMemoComment_0(annotatedContext, inboundMarshaler, client, req, pathParams)\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_MemoService_CreateMemoComment_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodGet, pattern_MemoService_ListMemoComments_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateContext(ctx, mux, req, \"/memos.api.v1.MemoService/ListMemoComments\", runtime.WithHTTPPathPattern(\"/api/v1/{name=memos/*}/comments\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := request_MemoService_ListMemoComments_0(annotatedContext, inboundMarshaler, client, req, pathParams)\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_MemoService_ListMemoComments_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodGet, pattern_MemoService_ListMemoReactions_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateContext(ctx, mux, req, \"/memos.api.v1.MemoService/ListMemoReactions\", runtime.WithHTTPPathPattern(\"/api/v1/{name=memos/*}/reactions\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := request_MemoService_ListMemoReactions_0(annotatedContext, inboundMarshaler, client, req, pathParams)\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_MemoService_ListMemoReactions_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodPost, pattern_MemoService_UpsertMemoReaction_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateContext(ctx, mux, req, \"/memos.api.v1.MemoService/UpsertMemoReaction\", runtime.WithHTTPPathPattern(\"/api/v1/{name=memos/*}/reactions\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := request_MemoService_UpsertMemoReaction_0(annotatedContext, inboundMarshaler, client, req, pathParams)\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_MemoService_UpsertMemoReaction_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodDelete, pattern_MemoService_DeleteMemoReaction_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateContext(ctx, mux, req, \"/memos.api.v1.MemoService/DeleteMemoReaction\", runtime.WithHTTPPathPattern(\"/api/v1/{name=memos/*/reactions/*}\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := request_MemoService_DeleteMemoReaction_0(annotatedContext, inboundMarshaler, client, req, pathParams)\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_MemoService_DeleteMemoReaction_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodPost, pattern_MemoService_CreateMemoShare_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateContext(ctx, mux, req, \"/memos.api.v1.MemoService/CreateMemoShare\", runtime.WithHTTPPathPattern(\"/api/v1/{parent=memos/*}/shares\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := request_MemoService_CreateMemoShare_0(annotatedContext, inboundMarshaler, client, req, pathParams)\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_MemoService_CreateMemoShare_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodGet, pattern_MemoService_ListMemoShares_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateContext(ctx, mux, req, \"/memos.api.v1.MemoService/ListMemoShares\", runtime.WithHTTPPathPattern(\"/api/v1/{parent=memos/*}/shares\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := request_MemoService_ListMemoShares_0(annotatedContext, inboundMarshaler, client, req, pathParams)\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_MemoService_ListMemoShares_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodDelete, pattern_MemoService_DeleteMemoShare_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateContext(ctx, mux, req, \"/memos.api.v1.MemoService/DeleteMemoShare\", runtime.WithHTTPPathPattern(\"/api/v1/{name=memos/*/shares/*}\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := request_MemoService_DeleteMemoShare_0(annotatedContext, inboundMarshaler, client, req, pathParams)\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_MemoService_DeleteMemoShare_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodGet, pattern_MemoService_GetMemoByShare_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateContext(ctx, mux, req, \"/memos.api.v1.MemoService/GetMemoByShare\", runtime.WithHTTPPathPattern(\"/api/v1/shares/{share_id}\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := request_MemoService_GetMemoByShare_0(annotatedContext, inboundMarshaler, client, req, pathParams)\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_MemoService_GetMemoByShare_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\treturn nil\n}\n\nvar (\n\tpattern_MemoService_CreateMemo_0          = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{\"api\", \"v1\", \"memos\"}, \"\"))\n\tpattern_MemoService_ListMemos_0           = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{\"api\", \"v1\", \"memos\"}, \"\"))\n\tpattern_MemoService_GetMemo_0             = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3}, []string{\"api\", \"v1\", \"memos\", \"name\"}, \"\"))\n\tpattern_MemoService_UpdateMemo_0          = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3}, []string{\"api\", \"v1\", \"memos\", \"memo.name\"}, \"\"))\n\tpattern_MemoService_DeleteMemo_0          = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3}, []string{\"api\", \"v1\", \"memos\", \"name\"}, \"\"))\n\tpattern_MemoService_SetMemoAttachments_0  = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3, 2, 4}, []string{\"api\", \"v1\", \"memos\", \"name\", \"attachments\"}, \"\"))\n\tpattern_MemoService_ListMemoAttachments_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3, 2, 4}, []string{\"api\", \"v1\", \"memos\", \"name\", \"attachments\"}, \"\"))\n\tpattern_MemoService_SetMemoRelations_0    = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3, 2, 4}, []string{\"api\", \"v1\", \"memos\", \"name\", \"relations\"}, \"\"))\n\tpattern_MemoService_ListMemoRelations_0   = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3, 2, 4}, []string{\"api\", \"v1\", \"memos\", \"name\", \"relations\"}, \"\"))\n\tpattern_MemoService_CreateMemoComment_0   = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3, 2, 4}, []string{\"api\", \"v1\", \"memos\", \"name\", \"comments\"}, \"\"))\n\tpattern_MemoService_ListMemoComments_0    = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3, 2, 4}, []string{\"api\", \"v1\", \"memos\", \"name\", \"comments\"}, \"\"))\n\tpattern_MemoService_ListMemoReactions_0   = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3, 2, 4}, []string{\"api\", \"v1\", \"memos\", \"name\", \"reactions\"}, \"\"))\n\tpattern_MemoService_UpsertMemoReaction_0  = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3, 2, 4}, []string{\"api\", \"v1\", \"memos\", \"name\", \"reactions\"}, \"\"))\n\tpattern_MemoService_DeleteMemoReaction_0  = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 2, 3, 1, 0, 4, 4, 5, 4}, []string{\"api\", \"v1\", \"memos\", \"reactions\", \"name\"}, \"\"))\n\tpattern_MemoService_CreateMemoShare_0     = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3, 2, 4}, []string{\"api\", \"v1\", \"memos\", \"parent\", \"shares\"}, \"\"))\n\tpattern_MemoService_ListMemoShares_0      = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3, 2, 4}, []string{\"api\", \"v1\", \"memos\", \"parent\", \"shares\"}, \"\"))\n\tpattern_MemoService_DeleteMemoShare_0     = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 2, 3, 1, 0, 4, 4, 5, 4}, []string{\"api\", \"v1\", \"memos\", \"shares\", \"name\"}, \"\"))\n\tpattern_MemoService_GetMemoByShare_0      = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3}, []string{\"api\", \"v1\", \"shares\", \"share_id\"}, \"\"))\n)\n\nvar (\n\tforward_MemoService_CreateMemo_0          = runtime.ForwardResponseMessage\n\tforward_MemoService_ListMemos_0           = runtime.ForwardResponseMessage\n\tforward_MemoService_GetMemo_0             = runtime.ForwardResponseMessage\n\tforward_MemoService_UpdateMemo_0          = runtime.ForwardResponseMessage\n\tforward_MemoService_DeleteMemo_0          = runtime.ForwardResponseMessage\n\tforward_MemoService_SetMemoAttachments_0  = runtime.ForwardResponseMessage\n\tforward_MemoService_ListMemoAttachments_0 = runtime.ForwardResponseMessage\n\tforward_MemoService_SetMemoRelations_0    = runtime.ForwardResponseMessage\n\tforward_MemoService_ListMemoRelations_0   = runtime.ForwardResponseMessage\n\tforward_MemoService_CreateMemoComment_0   = runtime.ForwardResponseMessage\n\tforward_MemoService_ListMemoComments_0    = runtime.ForwardResponseMessage\n\tforward_MemoService_ListMemoReactions_0   = runtime.ForwardResponseMessage\n\tforward_MemoService_UpsertMemoReaction_0  = runtime.ForwardResponseMessage\n\tforward_MemoService_DeleteMemoReaction_0  = runtime.ForwardResponseMessage\n\tforward_MemoService_CreateMemoShare_0     = runtime.ForwardResponseMessage\n\tforward_MemoService_ListMemoShares_0      = runtime.ForwardResponseMessage\n\tforward_MemoService_DeleteMemoShare_0     = runtime.ForwardResponseMessage\n\tforward_MemoService_GetMemoByShare_0      = runtime.ForwardResponseMessage\n)\n"
  },
  {
    "path": "proto/gen/api/v1/memo_service_grpc.pb.go",
    "content": "// Code generated by protoc-gen-go-grpc. DO NOT EDIT.\n// versions:\n// - protoc-gen-go-grpc v1.6.1\n// - protoc             (unknown)\n// source: api/v1/memo_service.proto\n\npackage apiv1\n\nimport (\n\tcontext \"context\"\n\tgrpc \"google.golang.org/grpc\"\n\tcodes \"google.golang.org/grpc/codes\"\n\tstatus \"google.golang.org/grpc/status\"\n\temptypb \"google.golang.org/protobuf/types/known/emptypb\"\n)\n\n// This is a compile-time assertion to ensure that this generated file\n// is compatible with the grpc package it is being compiled against.\n// Requires gRPC-Go v1.64.0 or later.\nconst _ = grpc.SupportPackageIsVersion9\n\nconst (\n\tMemoService_CreateMemo_FullMethodName          = \"/memos.api.v1.MemoService/CreateMemo\"\n\tMemoService_ListMemos_FullMethodName           = \"/memos.api.v1.MemoService/ListMemos\"\n\tMemoService_GetMemo_FullMethodName             = \"/memos.api.v1.MemoService/GetMemo\"\n\tMemoService_UpdateMemo_FullMethodName          = \"/memos.api.v1.MemoService/UpdateMemo\"\n\tMemoService_DeleteMemo_FullMethodName          = \"/memos.api.v1.MemoService/DeleteMemo\"\n\tMemoService_SetMemoAttachments_FullMethodName  = \"/memos.api.v1.MemoService/SetMemoAttachments\"\n\tMemoService_ListMemoAttachments_FullMethodName = \"/memos.api.v1.MemoService/ListMemoAttachments\"\n\tMemoService_SetMemoRelations_FullMethodName    = \"/memos.api.v1.MemoService/SetMemoRelations\"\n\tMemoService_ListMemoRelations_FullMethodName   = \"/memos.api.v1.MemoService/ListMemoRelations\"\n\tMemoService_CreateMemoComment_FullMethodName   = \"/memos.api.v1.MemoService/CreateMemoComment\"\n\tMemoService_ListMemoComments_FullMethodName    = \"/memos.api.v1.MemoService/ListMemoComments\"\n\tMemoService_ListMemoReactions_FullMethodName   = \"/memos.api.v1.MemoService/ListMemoReactions\"\n\tMemoService_UpsertMemoReaction_FullMethodName  = \"/memos.api.v1.MemoService/UpsertMemoReaction\"\n\tMemoService_DeleteMemoReaction_FullMethodName  = \"/memos.api.v1.MemoService/DeleteMemoReaction\"\n\tMemoService_CreateMemoShare_FullMethodName     = \"/memos.api.v1.MemoService/CreateMemoShare\"\n\tMemoService_ListMemoShares_FullMethodName      = \"/memos.api.v1.MemoService/ListMemoShares\"\n\tMemoService_DeleteMemoShare_FullMethodName     = \"/memos.api.v1.MemoService/DeleteMemoShare\"\n\tMemoService_GetMemoByShare_FullMethodName      = \"/memos.api.v1.MemoService/GetMemoByShare\"\n)\n\n// MemoServiceClient is the client API for MemoService service.\n//\n// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.\ntype MemoServiceClient interface {\n\t// CreateMemo creates a memo.\n\tCreateMemo(ctx context.Context, in *CreateMemoRequest, opts ...grpc.CallOption) (*Memo, error)\n\t// ListMemos lists memos with pagination and filter.\n\tListMemos(ctx context.Context, in *ListMemosRequest, opts ...grpc.CallOption) (*ListMemosResponse, error)\n\t// GetMemo gets a memo.\n\tGetMemo(ctx context.Context, in *GetMemoRequest, opts ...grpc.CallOption) (*Memo, error)\n\t// UpdateMemo updates a memo.\n\tUpdateMemo(ctx context.Context, in *UpdateMemoRequest, opts ...grpc.CallOption) (*Memo, error)\n\t// DeleteMemo deletes a memo.\n\tDeleteMemo(ctx context.Context, in *DeleteMemoRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)\n\t// SetMemoAttachments sets attachments for a memo.\n\tSetMemoAttachments(ctx context.Context, in *SetMemoAttachmentsRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)\n\t// ListMemoAttachments lists attachments for a memo.\n\tListMemoAttachments(ctx context.Context, in *ListMemoAttachmentsRequest, opts ...grpc.CallOption) (*ListMemoAttachmentsResponse, error)\n\t// SetMemoRelations sets relations for a memo.\n\tSetMemoRelations(ctx context.Context, in *SetMemoRelationsRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)\n\t// ListMemoRelations lists relations for a memo.\n\tListMemoRelations(ctx context.Context, in *ListMemoRelationsRequest, opts ...grpc.CallOption) (*ListMemoRelationsResponse, error)\n\t// CreateMemoComment creates a comment for a memo.\n\tCreateMemoComment(ctx context.Context, in *CreateMemoCommentRequest, opts ...grpc.CallOption) (*Memo, error)\n\t// ListMemoComments lists comments for a memo.\n\tListMemoComments(ctx context.Context, in *ListMemoCommentsRequest, opts ...grpc.CallOption) (*ListMemoCommentsResponse, error)\n\t// ListMemoReactions lists reactions for a memo.\n\tListMemoReactions(ctx context.Context, in *ListMemoReactionsRequest, opts ...grpc.CallOption) (*ListMemoReactionsResponse, error)\n\t// UpsertMemoReaction upserts a reaction for a memo.\n\tUpsertMemoReaction(ctx context.Context, in *UpsertMemoReactionRequest, opts ...grpc.CallOption) (*Reaction, error)\n\t// DeleteMemoReaction deletes a reaction for a memo.\n\tDeleteMemoReaction(ctx context.Context, in *DeleteMemoReactionRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)\n\t// CreateMemoShare creates a share link for a memo. Requires authentication as the memo creator.\n\tCreateMemoShare(ctx context.Context, in *CreateMemoShareRequest, opts ...grpc.CallOption) (*MemoShare, error)\n\t// ListMemoShares lists all share links for a memo. Requires authentication as the memo creator.\n\tListMemoShares(ctx context.Context, in *ListMemoSharesRequest, opts ...grpc.CallOption) (*ListMemoSharesResponse, error)\n\t// DeleteMemoShare revokes a share link. Requires authentication as the memo creator.\n\tDeleteMemoShare(ctx context.Context, in *DeleteMemoShareRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)\n\t// GetMemoByShare resolves a share token to its memo. No authentication required.\n\t// Returns NOT_FOUND if the token is invalid or expired.\n\tGetMemoByShare(ctx context.Context, in *GetMemoByShareRequest, opts ...grpc.CallOption) (*Memo, error)\n}\n\ntype memoServiceClient struct {\n\tcc grpc.ClientConnInterface\n}\n\nfunc NewMemoServiceClient(cc grpc.ClientConnInterface) MemoServiceClient {\n\treturn &memoServiceClient{cc}\n}\n\nfunc (c *memoServiceClient) CreateMemo(ctx context.Context, in *CreateMemoRequest, opts ...grpc.CallOption) (*Memo, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(Memo)\n\terr := c.cc.Invoke(ctx, MemoService_CreateMemo_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *memoServiceClient) ListMemos(ctx context.Context, in *ListMemosRequest, opts ...grpc.CallOption) (*ListMemosResponse, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(ListMemosResponse)\n\terr := c.cc.Invoke(ctx, MemoService_ListMemos_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *memoServiceClient) GetMemo(ctx context.Context, in *GetMemoRequest, opts ...grpc.CallOption) (*Memo, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(Memo)\n\terr := c.cc.Invoke(ctx, MemoService_GetMemo_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *memoServiceClient) UpdateMemo(ctx context.Context, in *UpdateMemoRequest, opts ...grpc.CallOption) (*Memo, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(Memo)\n\terr := c.cc.Invoke(ctx, MemoService_UpdateMemo_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *memoServiceClient) DeleteMemo(ctx context.Context, in *DeleteMemoRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(emptypb.Empty)\n\terr := c.cc.Invoke(ctx, MemoService_DeleteMemo_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *memoServiceClient) SetMemoAttachments(ctx context.Context, in *SetMemoAttachmentsRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(emptypb.Empty)\n\terr := c.cc.Invoke(ctx, MemoService_SetMemoAttachments_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *memoServiceClient) ListMemoAttachments(ctx context.Context, in *ListMemoAttachmentsRequest, opts ...grpc.CallOption) (*ListMemoAttachmentsResponse, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(ListMemoAttachmentsResponse)\n\terr := c.cc.Invoke(ctx, MemoService_ListMemoAttachments_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *memoServiceClient) SetMemoRelations(ctx context.Context, in *SetMemoRelationsRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(emptypb.Empty)\n\terr := c.cc.Invoke(ctx, MemoService_SetMemoRelations_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *memoServiceClient) ListMemoRelations(ctx context.Context, in *ListMemoRelationsRequest, opts ...grpc.CallOption) (*ListMemoRelationsResponse, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(ListMemoRelationsResponse)\n\terr := c.cc.Invoke(ctx, MemoService_ListMemoRelations_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *memoServiceClient) CreateMemoComment(ctx context.Context, in *CreateMemoCommentRequest, opts ...grpc.CallOption) (*Memo, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(Memo)\n\terr := c.cc.Invoke(ctx, MemoService_CreateMemoComment_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *memoServiceClient) ListMemoComments(ctx context.Context, in *ListMemoCommentsRequest, opts ...grpc.CallOption) (*ListMemoCommentsResponse, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(ListMemoCommentsResponse)\n\terr := c.cc.Invoke(ctx, MemoService_ListMemoComments_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *memoServiceClient) ListMemoReactions(ctx context.Context, in *ListMemoReactionsRequest, opts ...grpc.CallOption) (*ListMemoReactionsResponse, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(ListMemoReactionsResponse)\n\terr := c.cc.Invoke(ctx, MemoService_ListMemoReactions_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *memoServiceClient) UpsertMemoReaction(ctx context.Context, in *UpsertMemoReactionRequest, opts ...grpc.CallOption) (*Reaction, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(Reaction)\n\terr := c.cc.Invoke(ctx, MemoService_UpsertMemoReaction_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *memoServiceClient) DeleteMemoReaction(ctx context.Context, in *DeleteMemoReactionRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(emptypb.Empty)\n\terr := c.cc.Invoke(ctx, MemoService_DeleteMemoReaction_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *memoServiceClient) CreateMemoShare(ctx context.Context, in *CreateMemoShareRequest, opts ...grpc.CallOption) (*MemoShare, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(MemoShare)\n\terr := c.cc.Invoke(ctx, MemoService_CreateMemoShare_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *memoServiceClient) ListMemoShares(ctx context.Context, in *ListMemoSharesRequest, opts ...grpc.CallOption) (*ListMemoSharesResponse, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(ListMemoSharesResponse)\n\terr := c.cc.Invoke(ctx, MemoService_ListMemoShares_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *memoServiceClient) DeleteMemoShare(ctx context.Context, in *DeleteMemoShareRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(emptypb.Empty)\n\terr := c.cc.Invoke(ctx, MemoService_DeleteMemoShare_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *memoServiceClient) GetMemoByShare(ctx context.Context, in *GetMemoByShareRequest, opts ...grpc.CallOption) (*Memo, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(Memo)\n\terr := c.cc.Invoke(ctx, MemoService_GetMemoByShare_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\n// MemoServiceServer is the server API for MemoService service.\n// All implementations must embed UnimplementedMemoServiceServer\n// for forward compatibility.\ntype MemoServiceServer interface {\n\t// CreateMemo creates a memo.\n\tCreateMemo(context.Context, *CreateMemoRequest) (*Memo, error)\n\t// ListMemos lists memos with pagination and filter.\n\tListMemos(context.Context, *ListMemosRequest) (*ListMemosResponse, error)\n\t// GetMemo gets a memo.\n\tGetMemo(context.Context, *GetMemoRequest) (*Memo, error)\n\t// UpdateMemo updates a memo.\n\tUpdateMemo(context.Context, *UpdateMemoRequest) (*Memo, error)\n\t// DeleteMemo deletes a memo.\n\tDeleteMemo(context.Context, *DeleteMemoRequest) (*emptypb.Empty, error)\n\t// SetMemoAttachments sets attachments for a memo.\n\tSetMemoAttachments(context.Context, *SetMemoAttachmentsRequest) (*emptypb.Empty, error)\n\t// ListMemoAttachments lists attachments for a memo.\n\tListMemoAttachments(context.Context, *ListMemoAttachmentsRequest) (*ListMemoAttachmentsResponse, error)\n\t// SetMemoRelations sets relations for a memo.\n\tSetMemoRelations(context.Context, *SetMemoRelationsRequest) (*emptypb.Empty, error)\n\t// ListMemoRelations lists relations for a memo.\n\tListMemoRelations(context.Context, *ListMemoRelationsRequest) (*ListMemoRelationsResponse, error)\n\t// CreateMemoComment creates a comment for a memo.\n\tCreateMemoComment(context.Context, *CreateMemoCommentRequest) (*Memo, error)\n\t// ListMemoComments lists comments for a memo.\n\tListMemoComments(context.Context, *ListMemoCommentsRequest) (*ListMemoCommentsResponse, error)\n\t// ListMemoReactions lists reactions for a memo.\n\tListMemoReactions(context.Context, *ListMemoReactionsRequest) (*ListMemoReactionsResponse, error)\n\t// UpsertMemoReaction upserts a reaction for a memo.\n\tUpsertMemoReaction(context.Context, *UpsertMemoReactionRequest) (*Reaction, error)\n\t// DeleteMemoReaction deletes a reaction for a memo.\n\tDeleteMemoReaction(context.Context, *DeleteMemoReactionRequest) (*emptypb.Empty, error)\n\t// CreateMemoShare creates a share link for a memo. Requires authentication as the memo creator.\n\tCreateMemoShare(context.Context, *CreateMemoShareRequest) (*MemoShare, error)\n\t// ListMemoShares lists all share links for a memo. Requires authentication as the memo creator.\n\tListMemoShares(context.Context, *ListMemoSharesRequest) (*ListMemoSharesResponse, error)\n\t// DeleteMemoShare revokes a share link. Requires authentication as the memo creator.\n\tDeleteMemoShare(context.Context, *DeleteMemoShareRequest) (*emptypb.Empty, error)\n\t// GetMemoByShare resolves a share token to its memo. No authentication required.\n\t// Returns NOT_FOUND if the token is invalid or expired.\n\tGetMemoByShare(context.Context, *GetMemoByShareRequest) (*Memo, error)\n\tmustEmbedUnimplementedMemoServiceServer()\n}\n\n// UnimplementedMemoServiceServer must be embedded to have\n// forward compatible implementations.\n//\n// NOTE: this should be embedded by value instead of pointer to avoid a nil\n// pointer dereference when methods are called.\ntype UnimplementedMemoServiceServer struct{}\n\nfunc (UnimplementedMemoServiceServer) CreateMemo(context.Context, *CreateMemoRequest) (*Memo, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method CreateMemo not implemented\")\n}\nfunc (UnimplementedMemoServiceServer) ListMemos(context.Context, *ListMemosRequest) (*ListMemosResponse, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method ListMemos not implemented\")\n}\nfunc (UnimplementedMemoServiceServer) GetMemo(context.Context, *GetMemoRequest) (*Memo, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method GetMemo not implemented\")\n}\nfunc (UnimplementedMemoServiceServer) UpdateMemo(context.Context, *UpdateMemoRequest) (*Memo, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method UpdateMemo not implemented\")\n}\nfunc (UnimplementedMemoServiceServer) DeleteMemo(context.Context, *DeleteMemoRequest) (*emptypb.Empty, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method DeleteMemo not implemented\")\n}\nfunc (UnimplementedMemoServiceServer) SetMemoAttachments(context.Context, *SetMemoAttachmentsRequest) (*emptypb.Empty, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method SetMemoAttachments not implemented\")\n}\nfunc (UnimplementedMemoServiceServer) ListMemoAttachments(context.Context, *ListMemoAttachmentsRequest) (*ListMemoAttachmentsResponse, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method ListMemoAttachments not implemented\")\n}\nfunc (UnimplementedMemoServiceServer) SetMemoRelations(context.Context, *SetMemoRelationsRequest) (*emptypb.Empty, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method SetMemoRelations not implemented\")\n}\nfunc (UnimplementedMemoServiceServer) ListMemoRelations(context.Context, *ListMemoRelationsRequest) (*ListMemoRelationsResponse, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method ListMemoRelations not implemented\")\n}\nfunc (UnimplementedMemoServiceServer) CreateMemoComment(context.Context, *CreateMemoCommentRequest) (*Memo, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method CreateMemoComment not implemented\")\n}\nfunc (UnimplementedMemoServiceServer) ListMemoComments(context.Context, *ListMemoCommentsRequest) (*ListMemoCommentsResponse, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method ListMemoComments not implemented\")\n}\nfunc (UnimplementedMemoServiceServer) ListMemoReactions(context.Context, *ListMemoReactionsRequest) (*ListMemoReactionsResponse, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method ListMemoReactions not implemented\")\n}\nfunc (UnimplementedMemoServiceServer) UpsertMemoReaction(context.Context, *UpsertMemoReactionRequest) (*Reaction, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method UpsertMemoReaction not implemented\")\n}\nfunc (UnimplementedMemoServiceServer) DeleteMemoReaction(context.Context, *DeleteMemoReactionRequest) (*emptypb.Empty, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method DeleteMemoReaction not implemented\")\n}\nfunc (UnimplementedMemoServiceServer) CreateMemoShare(context.Context, *CreateMemoShareRequest) (*MemoShare, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method CreateMemoShare not implemented\")\n}\nfunc (UnimplementedMemoServiceServer) ListMemoShares(context.Context, *ListMemoSharesRequest) (*ListMemoSharesResponse, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method ListMemoShares not implemented\")\n}\nfunc (UnimplementedMemoServiceServer) DeleteMemoShare(context.Context, *DeleteMemoShareRequest) (*emptypb.Empty, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method DeleteMemoShare not implemented\")\n}\nfunc (UnimplementedMemoServiceServer) GetMemoByShare(context.Context, *GetMemoByShareRequest) (*Memo, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method GetMemoByShare not implemented\")\n}\nfunc (UnimplementedMemoServiceServer) mustEmbedUnimplementedMemoServiceServer() {}\nfunc (UnimplementedMemoServiceServer) testEmbeddedByValue()                     {}\n\n// UnsafeMemoServiceServer may be embedded to opt out of forward compatibility for this service.\n// Use of this interface is not recommended, as added methods to MemoServiceServer will\n// result in compilation errors.\ntype UnsafeMemoServiceServer interface {\n\tmustEmbedUnimplementedMemoServiceServer()\n}\n\nfunc RegisterMemoServiceServer(s grpc.ServiceRegistrar, srv MemoServiceServer) {\n\t// If the following call panics, it indicates UnimplementedMemoServiceServer was\n\t// embedded by pointer and is nil.  This will cause panics if an\n\t// unimplemented method is ever invoked, so we test this at initialization\n\t// time to prevent it from happening at runtime later due to I/O.\n\tif t, ok := srv.(interface{ testEmbeddedByValue() }); ok {\n\t\tt.testEmbeddedByValue()\n\t}\n\ts.RegisterService(&MemoService_ServiceDesc, srv)\n}\n\nfunc _MemoService_CreateMemo_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(CreateMemoRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(MemoServiceServer).CreateMemo(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: MemoService_CreateMemo_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(MemoServiceServer).CreateMemo(ctx, req.(*CreateMemoRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _MemoService_ListMemos_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(ListMemosRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(MemoServiceServer).ListMemos(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: MemoService_ListMemos_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(MemoServiceServer).ListMemos(ctx, req.(*ListMemosRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _MemoService_GetMemo_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(GetMemoRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(MemoServiceServer).GetMemo(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: MemoService_GetMemo_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(MemoServiceServer).GetMemo(ctx, req.(*GetMemoRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _MemoService_UpdateMemo_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(UpdateMemoRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(MemoServiceServer).UpdateMemo(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: MemoService_UpdateMemo_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(MemoServiceServer).UpdateMemo(ctx, req.(*UpdateMemoRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _MemoService_DeleteMemo_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(DeleteMemoRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(MemoServiceServer).DeleteMemo(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: MemoService_DeleteMemo_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(MemoServiceServer).DeleteMemo(ctx, req.(*DeleteMemoRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _MemoService_SetMemoAttachments_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(SetMemoAttachmentsRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(MemoServiceServer).SetMemoAttachments(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: MemoService_SetMemoAttachments_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(MemoServiceServer).SetMemoAttachments(ctx, req.(*SetMemoAttachmentsRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _MemoService_ListMemoAttachments_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(ListMemoAttachmentsRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(MemoServiceServer).ListMemoAttachments(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: MemoService_ListMemoAttachments_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(MemoServiceServer).ListMemoAttachments(ctx, req.(*ListMemoAttachmentsRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _MemoService_SetMemoRelations_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(SetMemoRelationsRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(MemoServiceServer).SetMemoRelations(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: MemoService_SetMemoRelations_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(MemoServiceServer).SetMemoRelations(ctx, req.(*SetMemoRelationsRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _MemoService_ListMemoRelations_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(ListMemoRelationsRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(MemoServiceServer).ListMemoRelations(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: MemoService_ListMemoRelations_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(MemoServiceServer).ListMemoRelations(ctx, req.(*ListMemoRelationsRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _MemoService_CreateMemoComment_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(CreateMemoCommentRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(MemoServiceServer).CreateMemoComment(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: MemoService_CreateMemoComment_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(MemoServiceServer).CreateMemoComment(ctx, req.(*CreateMemoCommentRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _MemoService_ListMemoComments_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(ListMemoCommentsRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(MemoServiceServer).ListMemoComments(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: MemoService_ListMemoComments_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(MemoServiceServer).ListMemoComments(ctx, req.(*ListMemoCommentsRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _MemoService_ListMemoReactions_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(ListMemoReactionsRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(MemoServiceServer).ListMemoReactions(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: MemoService_ListMemoReactions_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(MemoServiceServer).ListMemoReactions(ctx, req.(*ListMemoReactionsRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _MemoService_UpsertMemoReaction_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(UpsertMemoReactionRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(MemoServiceServer).UpsertMemoReaction(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: MemoService_UpsertMemoReaction_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(MemoServiceServer).UpsertMemoReaction(ctx, req.(*UpsertMemoReactionRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _MemoService_DeleteMemoReaction_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(DeleteMemoReactionRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(MemoServiceServer).DeleteMemoReaction(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: MemoService_DeleteMemoReaction_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(MemoServiceServer).DeleteMemoReaction(ctx, req.(*DeleteMemoReactionRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _MemoService_CreateMemoShare_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(CreateMemoShareRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(MemoServiceServer).CreateMemoShare(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: MemoService_CreateMemoShare_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(MemoServiceServer).CreateMemoShare(ctx, req.(*CreateMemoShareRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _MemoService_ListMemoShares_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(ListMemoSharesRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(MemoServiceServer).ListMemoShares(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: MemoService_ListMemoShares_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(MemoServiceServer).ListMemoShares(ctx, req.(*ListMemoSharesRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _MemoService_DeleteMemoShare_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(DeleteMemoShareRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(MemoServiceServer).DeleteMemoShare(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: MemoService_DeleteMemoShare_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(MemoServiceServer).DeleteMemoShare(ctx, req.(*DeleteMemoShareRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _MemoService_GetMemoByShare_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(GetMemoByShareRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(MemoServiceServer).GetMemoByShare(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: MemoService_GetMemoByShare_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(MemoServiceServer).GetMemoByShare(ctx, req.(*GetMemoByShareRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\n// MemoService_ServiceDesc is the grpc.ServiceDesc for MemoService service.\n// It's only intended for direct use with grpc.RegisterService,\n// and not to be introspected or modified (even as a copy)\nvar MemoService_ServiceDesc = grpc.ServiceDesc{\n\tServiceName: \"memos.api.v1.MemoService\",\n\tHandlerType: (*MemoServiceServer)(nil),\n\tMethods: []grpc.MethodDesc{\n\t\t{\n\t\t\tMethodName: \"CreateMemo\",\n\t\t\tHandler:    _MemoService_CreateMemo_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"ListMemos\",\n\t\t\tHandler:    _MemoService_ListMemos_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"GetMemo\",\n\t\t\tHandler:    _MemoService_GetMemo_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"UpdateMemo\",\n\t\t\tHandler:    _MemoService_UpdateMemo_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"DeleteMemo\",\n\t\t\tHandler:    _MemoService_DeleteMemo_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"SetMemoAttachments\",\n\t\t\tHandler:    _MemoService_SetMemoAttachments_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"ListMemoAttachments\",\n\t\t\tHandler:    _MemoService_ListMemoAttachments_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"SetMemoRelations\",\n\t\t\tHandler:    _MemoService_SetMemoRelations_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"ListMemoRelations\",\n\t\t\tHandler:    _MemoService_ListMemoRelations_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"CreateMemoComment\",\n\t\t\tHandler:    _MemoService_CreateMemoComment_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"ListMemoComments\",\n\t\t\tHandler:    _MemoService_ListMemoComments_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"ListMemoReactions\",\n\t\t\tHandler:    _MemoService_ListMemoReactions_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"UpsertMemoReaction\",\n\t\t\tHandler:    _MemoService_UpsertMemoReaction_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"DeleteMemoReaction\",\n\t\t\tHandler:    _MemoService_DeleteMemoReaction_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"CreateMemoShare\",\n\t\t\tHandler:    _MemoService_CreateMemoShare_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"ListMemoShares\",\n\t\t\tHandler:    _MemoService_ListMemoShares_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"DeleteMemoShare\",\n\t\t\tHandler:    _MemoService_DeleteMemoShare_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"GetMemoByShare\",\n\t\t\tHandler:    _MemoService_GetMemoByShare_Handler,\n\t\t},\n\t},\n\tStreams:  []grpc.StreamDesc{},\n\tMetadata: \"api/v1/memo_service.proto\",\n}\n"
  },
  {
    "path": "proto/gen/api/v1/shortcut_service.pb.go",
    "content": "// Code generated by protoc-gen-go. DO NOT EDIT.\n// versions:\n// \tprotoc-gen-go v1.36.11\n// \tprotoc        (unknown)\n// source: api/v1/shortcut_service.proto\n\npackage apiv1\n\nimport (\n\t_ \"google.golang.org/genproto/googleapis/api/annotations\"\n\tprotoreflect \"google.golang.org/protobuf/reflect/protoreflect\"\n\tprotoimpl \"google.golang.org/protobuf/runtime/protoimpl\"\n\temptypb \"google.golang.org/protobuf/types/known/emptypb\"\n\tfieldmaskpb \"google.golang.org/protobuf/types/known/fieldmaskpb\"\n\treflect \"reflect\"\n\tsync \"sync\"\n\tunsafe \"unsafe\"\n)\n\nconst (\n\t// Verify that this generated code is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)\n\t// Verify that runtime/protoimpl is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)\n)\n\ntype Shortcut struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// The resource name of the shortcut.\n\t// Format: users/{user}/shortcuts/{shortcut}\n\tName string `protobuf:\"bytes,1,opt,name=name,proto3\" json:\"name,omitempty\"`\n\t// The title of the shortcut.\n\tTitle string `protobuf:\"bytes,2,opt,name=title,proto3\" json:\"title,omitempty\"`\n\t// The filter expression for the shortcut.\n\tFilter        string `protobuf:\"bytes,3,opt,name=filter,proto3\" json:\"filter,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *Shortcut) Reset() {\n\t*x = Shortcut{}\n\tmi := &file_api_v1_shortcut_service_proto_msgTypes[0]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *Shortcut) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*Shortcut) ProtoMessage() {}\n\nfunc (x *Shortcut) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_shortcut_service_proto_msgTypes[0]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use Shortcut.ProtoReflect.Descriptor instead.\nfunc (*Shortcut) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_shortcut_service_proto_rawDescGZIP(), []int{0}\n}\n\nfunc (x *Shortcut) GetName() string {\n\tif x != nil {\n\t\treturn x.Name\n\t}\n\treturn \"\"\n}\n\nfunc (x *Shortcut) GetTitle() string {\n\tif x != nil {\n\t\treturn x.Title\n\t}\n\treturn \"\"\n}\n\nfunc (x *Shortcut) GetFilter() string {\n\tif x != nil {\n\t\treturn x.Filter\n\t}\n\treturn \"\"\n}\n\ntype ListShortcutsRequest struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// Required. The parent resource where shortcuts are listed.\n\t// Format: users/{user}\n\tParent        string `protobuf:\"bytes,1,opt,name=parent,proto3\" json:\"parent,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *ListShortcutsRequest) Reset() {\n\t*x = ListShortcutsRequest{}\n\tmi := &file_api_v1_shortcut_service_proto_msgTypes[1]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *ListShortcutsRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*ListShortcutsRequest) ProtoMessage() {}\n\nfunc (x *ListShortcutsRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_shortcut_service_proto_msgTypes[1]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use ListShortcutsRequest.ProtoReflect.Descriptor instead.\nfunc (*ListShortcutsRequest) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_shortcut_service_proto_rawDescGZIP(), []int{1}\n}\n\nfunc (x *ListShortcutsRequest) GetParent() string {\n\tif x != nil {\n\t\treturn x.Parent\n\t}\n\treturn \"\"\n}\n\ntype ListShortcutsResponse struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// The list of shortcuts.\n\tShortcuts     []*Shortcut `protobuf:\"bytes,1,rep,name=shortcuts,proto3\" json:\"shortcuts,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *ListShortcutsResponse) Reset() {\n\t*x = ListShortcutsResponse{}\n\tmi := &file_api_v1_shortcut_service_proto_msgTypes[2]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *ListShortcutsResponse) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*ListShortcutsResponse) ProtoMessage() {}\n\nfunc (x *ListShortcutsResponse) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_shortcut_service_proto_msgTypes[2]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use ListShortcutsResponse.ProtoReflect.Descriptor instead.\nfunc (*ListShortcutsResponse) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_shortcut_service_proto_rawDescGZIP(), []int{2}\n}\n\nfunc (x *ListShortcutsResponse) GetShortcuts() []*Shortcut {\n\tif x != nil {\n\t\treturn x.Shortcuts\n\t}\n\treturn nil\n}\n\ntype GetShortcutRequest struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// Required. The resource name of the shortcut to retrieve.\n\t// Format: users/{user}/shortcuts/{shortcut}\n\tName          string `protobuf:\"bytes,1,opt,name=name,proto3\" json:\"name,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *GetShortcutRequest) Reset() {\n\t*x = GetShortcutRequest{}\n\tmi := &file_api_v1_shortcut_service_proto_msgTypes[3]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *GetShortcutRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*GetShortcutRequest) ProtoMessage() {}\n\nfunc (x *GetShortcutRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_shortcut_service_proto_msgTypes[3]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use GetShortcutRequest.ProtoReflect.Descriptor instead.\nfunc (*GetShortcutRequest) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_shortcut_service_proto_rawDescGZIP(), []int{3}\n}\n\nfunc (x *GetShortcutRequest) GetName() string {\n\tif x != nil {\n\t\treturn x.Name\n\t}\n\treturn \"\"\n}\n\ntype CreateShortcutRequest struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// Required. The parent resource where this shortcut will be created.\n\t// Format: users/{user}\n\tParent string `protobuf:\"bytes,1,opt,name=parent,proto3\" json:\"parent,omitempty\"`\n\t// Required. The shortcut to create.\n\tShortcut *Shortcut `protobuf:\"bytes,2,opt,name=shortcut,proto3\" json:\"shortcut,omitempty\"`\n\t// Optional. If set, validate the request, but do not actually create the shortcut.\n\tValidateOnly  bool `protobuf:\"varint,3,opt,name=validate_only,json=validateOnly,proto3\" json:\"validate_only,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *CreateShortcutRequest) Reset() {\n\t*x = CreateShortcutRequest{}\n\tmi := &file_api_v1_shortcut_service_proto_msgTypes[4]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *CreateShortcutRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*CreateShortcutRequest) ProtoMessage() {}\n\nfunc (x *CreateShortcutRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_shortcut_service_proto_msgTypes[4]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use CreateShortcutRequest.ProtoReflect.Descriptor instead.\nfunc (*CreateShortcutRequest) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_shortcut_service_proto_rawDescGZIP(), []int{4}\n}\n\nfunc (x *CreateShortcutRequest) GetParent() string {\n\tif x != nil {\n\t\treturn x.Parent\n\t}\n\treturn \"\"\n}\n\nfunc (x *CreateShortcutRequest) GetShortcut() *Shortcut {\n\tif x != nil {\n\t\treturn x.Shortcut\n\t}\n\treturn nil\n}\n\nfunc (x *CreateShortcutRequest) GetValidateOnly() bool {\n\tif x != nil {\n\t\treturn x.ValidateOnly\n\t}\n\treturn false\n}\n\ntype UpdateShortcutRequest struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// Required. The shortcut resource which replaces the resource on the server.\n\tShortcut *Shortcut `protobuf:\"bytes,1,opt,name=shortcut,proto3\" json:\"shortcut,omitempty\"`\n\t// Optional. The list of fields to update.\n\tUpdateMask    *fieldmaskpb.FieldMask `protobuf:\"bytes,2,opt,name=update_mask,json=updateMask,proto3\" json:\"update_mask,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *UpdateShortcutRequest) Reset() {\n\t*x = UpdateShortcutRequest{}\n\tmi := &file_api_v1_shortcut_service_proto_msgTypes[5]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *UpdateShortcutRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*UpdateShortcutRequest) ProtoMessage() {}\n\nfunc (x *UpdateShortcutRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_shortcut_service_proto_msgTypes[5]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use UpdateShortcutRequest.ProtoReflect.Descriptor instead.\nfunc (*UpdateShortcutRequest) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_shortcut_service_proto_rawDescGZIP(), []int{5}\n}\n\nfunc (x *UpdateShortcutRequest) GetShortcut() *Shortcut {\n\tif x != nil {\n\t\treturn x.Shortcut\n\t}\n\treturn nil\n}\n\nfunc (x *UpdateShortcutRequest) GetUpdateMask() *fieldmaskpb.FieldMask {\n\tif x != nil {\n\t\treturn x.UpdateMask\n\t}\n\treturn nil\n}\n\ntype DeleteShortcutRequest struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// Required. The resource name of the shortcut to delete.\n\t// Format: users/{user}/shortcuts/{shortcut}\n\tName          string `protobuf:\"bytes,1,opt,name=name,proto3\" json:\"name,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *DeleteShortcutRequest) Reset() {\n\t*x = DeleteShortcutRequest{}\n\tmi := &file_api_v1_shortcut_service_proto_msgTypes[6]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *DeleteShortcutRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*DeleteShortcutRequest) ProtoMessage() {}\n\nfunc (x *DeleteShortcutRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_shortcut_service_proto_msgTypes[6]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use DeleteShortcutRequest.ProtoReflect.Descriptor instead.\nfunc (*DeleteShortcutRequest) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_shortcut_service_proto_rawDescGZIP(), []int{6}\n}\n\nfunc (x *DeleteShortcutRequest) GetName() string {\n\tif x != nil {\n\t\treturn x.Name\n\t}\n\treturn \"\"\n}\n\nvar File_api_v1_shortcut_service_proto protoreflect.FileDescriptor\n\nconst file_api_v1_shortcut_service_proto_rawDesc = \"\" +\n\t\"\\n\" +\n\t\"\\x1dapi/v1/shortcut_service.proto\\x12\\fmemos.api.v1\\x1a\\x1cgoogle/api/annotations.proto\\x1a\\x17google/api/client.proto\\x1a\\x1fgoogle/api/field_behavior.proto\\x1a\\x19google/api/resource.proto\\x1a\\x1bgoogle/protobuf/empty.proto\\x1a google/protobuf/field_mask.proto\\\"\\xaf\\x01\\n\" +\n\t\"\\bShortcut\\x12\\x17\\n\" +\n\t\"\\x04name\\x18\\x01 \\x01(\\tB\\x03\\xe0A\\bR\\x04name\\x12\\x19\\n\" +\n\t\"\\x05title\\x18\\x02 \\x01(\\tB\\x03\\xe0A\\x02R\\x05title\\x12\\x1b\\n\" +\n\t\"\\x06filter\\x18\\x03 \\x01(\\tB\\x03\\xe0A\\x01R\\x06filter:R\\xeaAO\\n\" +\n\t\"\\x15memos.api.v1/Shortcut\\x12!users/{user}/shortcuts/{shortcut}*\\tshortcuts2\\bshortcut\\\"M\\n\" +\n\t\"\\x14ListShortcutsRequest\\x125\\n\" +\n\t\"\\x06parent\\x18\\x01 \\x01(\\tB\\x1d\\xe0A\\x02\\xfaA\\x17\\x12\\x15memos.api.v1/ShortcutR\\x06parent\\\"M\\n\" +\n\t\"\\x15ListShortcutsResponse\\x124\\n\" +\n\t\"\\tshortcuts\\x18\\x01 \\x03(\\v2\\x16.memos.api.v1.ShortcutR\\tshortcuts\\\"G\\n\" +\n\t\"\\x12GetShortcutRequest\\x121\\n\" +\n\t\"\\x04name\\x18\\x01 \\x01(\\tB\\x1d\\xe0A\\x02\\xfaA\\x17\\n\" +\n\t\"\\x15memos.api.v1/ShortcutR\\x04name\\\"\\xb1\\x01\\n\" +\n\t\"\\x15CreateShortcutRequest\\x125\\n\" +\n\t\"\\x06parent\\x18\\x01 \\x01(\\tB\\x1d\\xe0A\\x02\\xfaA\\x17\\x12\\x15memos.api.v1/ShortcutR\\x06parent\\x127\\n\" +\n\t\"\\bshortcut\\x18\\x02 \\x01(\\v2\\x16.memos.api.v1.ShortcutB\\x03\\xe0A\\x02R\\bshortcut\\x12(\\n\" +\n\t\"\\rvalidate_only\\x18\\x03 \\x01(\\bB\\x03\\xe0A\\x01R\\fvalidateOnly\\\"\\x92\\x01\\n\" +\n\t\"\\x15UpdateShortcutRequest\\x127\\n\" +\n\t\"\\bshortcut\\x18\\x01 \\x01(\\v2\\x16.memos.api.v1.ShortcutB\\x03\\xe0A\\x02R\\bshortcut\\x12@\\n\" +\n\t\"\\vupdate_mask\\x18\\x02 \\x01(\\v2\\x1a.google.protobuf.FieldMaskB\\x03\\xe0A\\x01R\\n\" +\n\t\"updateMask\\\"J\\n\" +\n\t\"\\x15DeleteShortcutRequest\\x121\\n\" +\n\t\"\\x04name\\x18\\x01 \\x01(\\tB\\x1d\\xe0A\\x02\\xfaA\\x17\\n\" +\n\t\"\\x15memos.api.v1/ShortcutR\\x04name2\\xde\\x05\\n\" +\n\t\"\\x0fShortcutService\\x12\\x8d\\x01\\n\" +\n\t\"\\rListShortcuts\\x12\\\".memos.api.v1.ListShortcutsRequest\\x1a#.memos.api.v1.ListShortcutsResponse\\\"3\\xdaA\\x06parent\\x82\\xd3\\xe4\\x93\\x02$\\x12\\\"/api/v1/{parent=users/*}/shortcuts\\x12z\\n\" +\n\t\"\\vGetShortcut\\x12 .memos.api.v1.GetShortcutRequest\\x1a\\x16.memos.api.v1.Shortcut\\\"1\\xdaA\\x04name\\x82\\xd3\\xe4\\x93\\x02$\\x12\\\"/api/v1/{name=users/*/shortcuts/*}\\x12\\x95\\x01\\n\" +\n\t\"\\x0eCreateShortcut\\x12#.memos.api.v1.CreateShortcutRequest\\x1a\\x16.memos.api.v1.Shortcut\\\"F\\xdaA\\x0fparent,shortcut\\x82\\xd3\\xe4\\x93\\x02.:\\bshortcut\\\"\\\"/api/v1/{parent=users/*}/shortcuts\\x12\\xa3\\x01\\n\" +\n\t\"\\x0eUpdateShortcut\\x12#.memos.api.v1.UpdateShortcutRequest\\x1a\\x16.memos.api.v1.Shortcut\\\"T\\xdaA\\x14shortcut,update_mask\\x82\\xd3\\xe4\\x93\\x027:\\bshortcut2+/api/v1/{shortcut.name=users/*/shortcuts/*}\\x12\\x80\\x01\\n\" +\n\t\"\\x0eDeleteShortcut\\x12#.memos.api.v1.DeleteShortcutRequest\\x1a\\x16.google.protobuf.Empty\\\"1\\xdaA\\x04name\\x82\\xd3\\xe4\\x93\\x02$*\\\"/api/v1/{name=users/*/shortcuts/*}B\\xac\\x01\\n\" +\n\t\"\\x10com.memos.api.v1B\\x14ShortcutServiceProtoP\\x01Z0github.com/usememos/memos/proto/gen/api/v1;apiv1\\xa2\\x02\\x03MAX\\xaa\\x02\\fMemos.Api.V1\\xca\\x02\\fMemos\\\\Api\\\\V1\\xe2\\x02\\x18Memos\\\\Api\\\\V1\\\\GPBMetadata\\xea\\x02\\x0eMemos::Api::V1b\\x06proto3\"\n\nvar (\n\tfile_api_v1_shortcut_service_proto_rawDescOnce sync.Once\n\tfile_api_v1_shortcut_service_proto_rawDescData []byte\n)\n\nfunc file_api_v1_shortcut_service_proto_rawDescGZIP() []byte {\n\tfile_api_v1_shortcut_service_proto_rawDescOnce.Do(func() {\n\t\tfile_api_v1_shortcut_service_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_api_v1_shortcut_service_proto_rawDesc), len(file_api_v1_shortcut_service_proto_rawDesc)))\n\t})\n\treturn file_api_v1_shortcut_service_proto_rawDescData\n}\n\nvar file_api_v1_shortcut_service_proto_msgTypes = make([]protoimpl.MessageInfo, 7)\nvar file_api_v1_shortcut_service_proto_goTypes = []any{\n\t(*Shortcut)(nil),              // 0: memos.api.v1.Shortcut\n\t(*ListShortcutsRequest)(nil),  // 1: memos.api.v1.ListShortcutsRequest\n\t(*ListShortcutsResponse)(nil), // 2: memos.api.v1.ListShortcutsResponse\n\t(*GetShortcutRequest)(nil),    // 3: memos.api.v1.GetShortcutRequest\n\t(*CreateShortcutRequest)(nil), // 4: memos.api.v1.CreateShortcutRequest\n\t(*UpdateShortcutRequest)(nil), // 5: memos.api.v1.UpdateShortcutRequest\n\t(*DeleteShortcutRequest)(nil), // 6: memos.api.v1.DeleteShortcutRequest\n\t(*fieldmaskpb.FieldMask)(nil), // 7: google.protobuf.FieldMask\n\t(*emptypb.Empty)(nil),         // 8: google.protobuf.Empty\n}\nvar file_api_v1_shortcut_service_proto_depIdxs = []int32{\n\t0, // 0: memos.api.v1.ListShortcutsResponse.shortcuts:type_name -> memos.api.v1.Shortcut\n\t0, // 1: memos.api.v1.CreateShortcutRequest.shortcut:type_name -> memos.api.v1.Shortcut\n\t0, // 2: memos.api.v1.UpdateShortcutRequest.shortcut:type_name -> memos.api.v1.Shortcut\n\t7, // 3: memos.api.v1.UpdateShortcutRequest.update_mask:type_name -> google.protobuf.FieldMask\n\t1, // 4: memos.api.v1.ShortcutService.ListShortcuts:input_type -> memos.api.v1.ListShortcutsRequest\n\t3, // 5: memos.api.v1.ShortcutService.GetShortcut:input_type -> memos.api.v1.GetShortcutRequest\n\t4, // 6: memos.api.v1.ShortcutService.CreateShortcut:input_type -> memos.api.v1.CreateShortcutRequest\n\t5, // 7: memos.api.v1.ShortcutService.UpdateShortcut:input_type -> memos.api.v1.UpdateShortcutRequest\n\t6, // 8: memos.api.v1.ShortcutService.DeleteShortcut:input_type -> memos.api.v1.DeleteShortcutRequest\n\t2, // 9: memos.api.v1.ShortcutService.ListShortcuts:output_type -> memos.api.v1.ListShortcutsResponse\n\t0, // 10: memos.api.v1.ShortcutService.GetShortcut:output_type -> memos.api.v1.Shortcut\n\t0, // 11: memos.api.v1.ShortcutService.CreateShortcut:output_type -> memos.api.v1.Shortcut\n\t0, // 12: memos.api.v1.ShortcutService.UpdateShortcut:output_type -> memos.api.v1.Shortcut\n\t8, // 13: memos.api.v1.ShortcutService.DeleteShortcut:output_type -> google.protobuf.Empty\n\t9, // [9:14] is the sub-list for method output_type\n\t4, // [4:9] is the sub-list for method input_type\n\t4, // [4:4] is the sub-list for extension type_name\n\t4, // [4:4] is the sub-list for extension extendee\n\t0, // [0:4] is the sub-list for field type_name\n}\n\nfunc init() { file_api_v1_shortcut_service_proto_init() }\nfunc file_api_v1_shortcut_service_proto_init() {\n\tif File_api_v1_shortcut_service_proto != nil {\n\t\treturn\n\t}\n\ttype x struct{}\n\tout := protoimpl.TypeBuilder{\n\t\tFile: protoimpl.DescBuilder{\n\t\t\tGoPackagePath: reflect.TypeOf(x{}).PkgPath(),\n\t\t\tRawDescriptor: unsafe.Slice(unsafe.StringData(file_api_v1_shortcut_service_proto_rawDesc), len(file_api_v1_shortcut_service_proto_rawDesc)),\n\t\t\tNumEnums:      0,\n\t\t\tNumMessages:   7,\n\t\t\tNumExtensions: 0,\n\t\t\tNumServices:   1,\n\t\t},\n\t\tGoTypes:           file_api_v1_shortcut_service_proto_goTypes,\n\t\tDependencyIndexes: file_api_v1_shortcut_service_proto_depIdxs,\n\t\tMessageInfos:      file_api_v1_shortcut_service_proto_msgTypes,\n\t}.Build()\n\tFile_api_v1_shortcut_service_proto = out.File\n\tfile_api_v1_shortcut_service_proto_goTypes = nil\n\tfile_api_v1_shortcut_service_proto_depIdxs = nil\n}\n"
  },
  {
    "path": "proto/gen/api/v1/shortcut_service.pb.gw.go",
    "content": "// Code generated by protoc-gen-grpc-gateway. DO NOT EDIT.\n// source: api/v1/shortcut_service.proto\n\n/*\nPackage apiv1 is a reverse proxy.\n\nIt translates gRPC into RESTful JSON APIs.\n*/\npackage apiv1\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"io\"\n\t\"net/http\"\n\n\t\"github.com/grpc-ecosystem/grpc-gateway/v2/runtime\"\n\t\"github.com/grpc-ecosystem/grpc-gateway/v2/utilities\"\n\t\"google.golang.org/grpc\"\n\t\"google.golang.org/grpc/codes\"\n\t\"google.golang.org/grpc/grpclog\"\n\t\"google.golang.org/grpc/metadata\"\n\t\"google.golang.org/grpc/status\"\n\t\"google.golang.org/protobuf/proto\"\n)\n\n// Suppress \"imported and not used\" errors\nvar (\n\t_ codes.Code\n\t_ io.Reader\n\t_ status.Status\n\t_ = errors.New\n\t_ = runtime.String\n\t_ = utilities.NewDoubleArray\n\t_ = metadata.Join\n)\n\nfunc request_ShortcutService_ListShortcuts_0(ctx context.Context, marshaler runtime.Marshaler, client ShortcutServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq ListShortcutsRequest\n\t\tmetadata runtime.ServerMetadata\n\t\terr      error\n\t)\n\tif req.Body != nil {\n\t\t_, _ = io.Copy(io.Discard, req.Body)\n\t}\n\tval, ok := pathParams[\"parent\"]\n\tif !ok {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"missing parameter %s\", \"parent\")\n\t}\n\tprotoReq.Parent, err = runtime.String(val)\n\tif err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"type mismatch, parameter: %s, error: %v\", \"parent\", err)\n\t}\n\tmsg, err := client.ListShortcuts(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))\n\treturn msg, metadata, err\n}\n\nfunc local_request_ShortcutService_ListShortcuts_0(ctx context.Context, marshaler runtime.Marshaler, server ShortcutServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq ListShortcutsRequest\n\t\tmetadata runtime.ServerMetadata\n\t\terr      error\n\t)\n\tval, ok := pathParams[\"parent\"]\n\tif !ok {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"missing parameter %s\", \"parent\")\n\t}\n\tprotoReq.Parent, err = runtime.String(val)\n\tif err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"type mismatch, parameter: %s, error: %v\", \"parent\", err)\n\t}\n\tmsg, err := server.ListShortcuts(ctx, &protoReq)\n\treturn msg, metadata, err\n}\n\nfunc request_ShortcutService_GetShortcut_0(ctx context.Context, marshaler runtime.Marshaler, client ShortcutServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq GetShortcutRequest\n\t\tmetadata runtime.ServerMetadata\n\t\terr      error\n\t)\n\tif req.Body != nil {\n\t\t_, _ = io.Copy(io.Discard, req.Body)\n\t}\n\tval, ok := pathParams[\"name\"]\n\tif !ok {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"missing parameter %s\", \"name\")\n\t}\n\tprotoReq.Name, err = runtime.String(val)\n\tif err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"type mismatch, parameter: %s, error: %v\", \"name\", err)\n\t}\n\tmsg, err := client.GetShortcut(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))\n\treturn msg, metadata, err\n}\n\nfunc local_request_ShortcutService_GetShortcut_0(ctx context.Context, marshaler runtime.Marshaler, server ShortcutServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq GetShortcutRequest\n\t\tmetadata runtime.ServerMetadata\n\t\terr      error\n\t)\n\tval, ok := pathParams[\"name\"]\n\tif !ok {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"missing parameter %s\", \"name\")\n\t}\n\tprotoReq.Name, err = runtime.String(val)\n\tif err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"type mismatch, parameter: %s, error: %v\", \"name\", err)\n\t}\n\tmsg, err := server.GetShortcut(ctx, &protoReq)\n\treturn msg, metadata, err\n}\n\nvar filter_ShortcutService_CreateShortcut_0 = &utilities.DoubleArray{Encoding: map[string]int{\"shortcut\": 0, \"parent\": 1}, Base: []int{1, 1, 2, 0, 0}, Check: []int{0, 1, 1, 2, 3}}\n\nfunc request_ShortcutService_CreateShortcut_0(ctx context.Context, marshaler runtime.Marshaler, client ShortcutServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq CreateShortcutRequest\n\t\tmetadata runtime.ServerMetadata\n\t\terr      error\n\t)\n\tif err := marshaler.NewDecoder(req.Body).Decode(&protoReq.Shortcut); err != nil && !errors.Is(err, io.EOF) {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tif req.Body != nil {\n\t\t_, _ = io.Copy(io.Discard, req.Body)\n\t}\n\tval, ok := pathParams[\"parent\"]\n\tif !ok {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"missing parameter %s\", \"parent\")\n\t}\n\tprotoReq.Parent, err = runtime.String(val)\n\tif err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"type mismatch, parameter: %s, error: %v\", \"parent\", err)\n\t}\n\tif err := req.ParseForm(); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tif err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_ShortcutService_CreateShortcut_0); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tmsg, err := client.CreateShortcut(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))\n\treturn msg, metadata, err\n}\n\nfunc local_request_ShortcutService_CreateShortcut_0(ctx context.Context, marshaler runtime.Marshaler, server ShortcutServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq CreateShortcutRequest\n\t\tmetadata runtime.ServerMetadata\n\t\terr      error\n\t)\n\tif err := marshaler.NewDecoder(req.Body).Decode(&protoReq.Shortcut); err != nil && !errors.Is(err, io.EOF) {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tval, ok := pathParams[\"parent\"]\n\tif !ok {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"missing parameter %s\", \"parent\")\n\t}\n\tprotoReq.Parent, err = runtime.String(val)\n\tif err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"type mismatch, parameter: %s, error: %v\", \"parent\", err)\n\t}\n\tif err := req.ParseForm(); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tif err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_ShortcutService_CreateShortcut_0); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tmsg, err := server.CreateShortcut(ctx, &protoReq)\n\treturn msg, metadata, err\n}\n\nvar filter_ShortcutService_UpdateShortcut_0 = &utilities.DoubleArray{Encoding: map[string]int{\"shortcut\": 0, \"name\": 1}, Base: []int{1, 2, 1, 0, 0}, Check: []int{0, 1, 2, 3, 2}}\n\nfunc request_ShortcutService_UpdateShortcut_0(ctx context.Context, marshaler runtime.Marshaler, client ShortcutServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq UpdateShortcutRequest\n\t\tmetadata runtime.ServerMetadata\n\t\terr      error\n\t)\n\tnewReader, berr := utilities.IOReaderFactory(req.Body)\n\tif berr != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", berr)\n\t}\n\tif err := marshaler.NewDecoder(newReader()).Decode(&protoReq.Shortcut); err != nil && !errors.Is(err, io.EOF) {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tif req.Body != nil {\n\t\t_, _ = io.Copy(io.Discard, req.Body)\n\t}\n\tif protoReq.UpdateMask == nil || len(protoReq.UpdateMask.GetPaths()) == 0 {\n\t\tif fieldMask, err := runtime.FieldMaskFromRequestBody(newReader(), protoReq.Shortcut); err != nil {\n\t\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t\t} else {\n\t\t\tprotoReq.UpdateMask = fieldMask\n\t\t}\n\t}\n\tval, ok := pathParams[\"shortcut.name\"]\n\tif !ok {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"missing parameter %s\", \"shortcut.name\")\n\t}\n\terr = runtime.PopulateFieldFromPath(&protoReq, \"shortcut.name\", val)\n\tif err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"type mismatch, parameter: %s, error: %v\", \"shortcut.name\", err)\n\t}\n\tif err := req.ParseForm(); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tif err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_ShortcutService_UpdateShortcut_0); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tmsg, err := client.UpdateShortcut(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))\n\treturn msg, metadata, err\n}\n\nfunc local_request_ShortcutService_UpdateShortcut_0(ctx context.Context, marshaler runtime.Marshaler, server ShortcutServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq UpdateShortcutRequest\n\t\tmetadata runtime.ServerMetadata\n\t\terr      error\n\t)\n\tnewReader, berr := utilities.IOReaderFactory(req.Body)\n\tif berr != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", berr)\n\t}\n\tif err := marshaler.NewDecoder(newReader()).Decode(&protoReq.Shortcut); err != nil && !errors.Is(err, io.EOF) {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tif protoReq.UpdateMask == nil || len(protoReq.UpdateMask.GetPaths()) == 0 {\n\t\tif fieldMask, err := runtime.FieldMaskFromRequestBody(newReader(), protoReq.Shortcut); err != nil {\n\t\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t\t} else {\n\t\t\tprotoReq.UpdateMask = fieldMask\n\t\t}\n\t}\n\tval, ok := pathParams[\"shortcut.name\"]\n\tif !ok {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"missing parameter %s\", \"shortcut.name\")\n\t}\n\terr = runtime.PopulateFieldFromPath(&protoReq, \"shortcut.name\", val)\n\tif err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"type mismatch, parameter: %s, error: %v\", \"shortcut.name\", err)\n\t}\n\tif err := req.ParseForm(); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tif err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_ShortcutService_UpdateShortcut_0); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tmsg, err := server.UpdateShortcut(ctx, &protoReq)\n\treturn msg, metadata, err\n}\n\nfunc request_ShortcutService_DeleteShortcut_0(ctx context.Context, marshaler runtime.Marshaler, client ShortcutServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq DeleteShortcutRequest\n\t\tmetadata runtime.ServerMetadata\n\t\terr      error\n\t)\n\tif req.Body != nil {\n\t\t_, _ = io.Copy(io.Discard, req.Body)\n\t}\n\tval, ok := pathParams[\"name\"]\n\tif !ok {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"missing parameter %s\", \"name\")\n\t}\n\tprotoReq.Name, err = runtime.String(val)\n\tif err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"type mismatch, parameter: %s, error: %v\", \"name\", err)\n\t}\n\tmsg, err := client.DeleteShortcut(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))\n\treturn msg, metadata, err\n}\n\nfunc local_request_ShortcutService_DeleteShortcut_0(ctx context.Context, marshaler runtime.Marshaler, server ShortcutServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq DeleteShortcutRequest\n\t\tmetadata runtime.ServerMetadata\n\t\terr      error\n\t)\n\tval, ok := pathParams[\"name\"]\n\tif !ok {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"missing parameter %s\", \"name\")\n\t}\n\tprotoReq.Name, err = runtime.String(val)\n\tif err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"type mismatch, parameter: %s, error: %v\", \"name\", err)\n\t}\n\tmsg, err := server.DeleteShortcut(ctx, &protoReq)\n\treturn msg, metadata, err\n}\n\n// RegisterShortcutServiceHandlerServer registers the http handlers for service ShortcutService to \"mux\".\n// UnaryRPC     :call ShortcutServiceServer directly.\n// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906.\n// Note that using this registration option will cause many gRPC library features to stop working. Consider using RegisterShortcutServiceHandlerFromEndpoint instead.\n// GRPC interceptors will not work for this type of registration. To use interceptors, you must use the \"runtime.WithMiddlewares\" option in the \"runtime.NewServeMux\" call.\nfunc RegisterShortcutServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux, server ShortcutServiceServer) error {\n\tmux.Handle(http.MethodGet, pattern_ShortcutService_ListShortcuts_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tvar stream runtime.ServerTransportStream\n\t\tctx = grpc.NewContextWithServerTransportStream(ctx, &stream)\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, \"/memos.api.v1.ShortcutService/ListShortcuts\", runtime.WithHTTPPathPattern(\"/api/v1/{parent=users/*}/shortcuts\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := local_request_ShortcutService_ListShortcuts_0(annotatedContext, inboundMarshaler, server, req, pathParams)\n\t\tmd.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_ShortcutService_ListShortcuts_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodGet, pattern_ShortcutService_GetShortcut_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tvar stream runtime.ServerTransportStream\n\t\tctx = grpc.NewContextWithServerTransportStream(ctx, &stream)\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, \"/memos.api.v1.ShortcutService/GetShortcut\", runtime.WithHTTPPathPattern(\"/api/v1/{name=users/*/shortcuts/*}\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := local_request_ShortcutService_GetShortcut_0(annotatedContext, inboundMarshaler, server, req, pathParams)\n\t\tmd.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_ShortcutService_GetShortcut_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodPost, pattern_ShortcutService_CreateShortcut_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tvar stream runtime.ServerTransportStream\n\t\tctx = grpc.NewContextWithServerTransportStream(ctx, &stream)\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, \"/memos.api.v1.ShortcutService/CreateShortcut\", runtime.WithHTTPPathPattern(\"/api/v1/{parent=users/*}/shortcuts\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := local_request_ShortcutService_CreateShortcut_0(annotatedContext, inboundMarshaler, server, req, pathParams)\n\t\tmd.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_ShortcutService_CreateShortcut_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodPatch, pattern_ShortcutService_UpdateShortcut_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tvar stream runtime.ServerTransportStream\n\t\tctx = grpc.NewContextWithServerTransportStream(ctx, &stream)\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, \"/memos.api.v1.ShortcutService/UpdateShortcut\", runtime.WithHTTPPathPattern(\"/api/v1/{shortcut.name=users/*/shortcuts/*}\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := local_request_ShortcutService_UpdateShortcut_0(annotatedContext, inboundMarshaler, server, req, pathParams)\n\t\tmd.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_ShortcutService_UpdateShortcut_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodDelete, pattern_ShortcutService_DeleteShortcut_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tvar stream runtime.ServerTransportStream\n\t\tctx = grpc.NewContextWithServerTransportStream(ctx, &stream)\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, \"/memos.api.v1.ShortcutService/DeleteShortcut\", runtime.WithHTTPPathPattern(\"/api/v1/{name=users/*/shortcuts/*}\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := local_request_ShortcutService_DeleteShortcut_0(annotatedContext, inboundMarshaler, server, req, pathParams)\n\t\tmd.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_ShortcutService_DeleteShortcut_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\n\treturn nil\n}\n\n// RegisterShortcutServiceHandlerFromEndpoint is same as RegisterShortcutServiceHandler but\n// automatically dials to \"endpoint\" and closes the connection when \"ctx\" gets done.\nfunc RegisterShortcutServiceHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) {\n\tconn, err := grpc.NewClient(endpoint, opts...)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer func() {\n\t\tif err != nil {\n\t\t\tif cerr := conn.Close(); cerr != nil {\n\t\t\t\tgrpclog.Errorf(\"Failed to close conn to %s: %v\", endpoint, cerr)\n\t\t\t}\n\t\t\treturn\n\t\t}\n\t\tgo func() {\n\t\t\t<-ctx.Done()\n\t\t\tif cerr := conn.Close(); cerr != nil {\n\t\t\t\tgrpclog.Errorf(\"Failed to close conn to %s: %v\", endpoint, cerr)\n\t\t\t}\n\t\t}()\n\t}()\n\treturn RegisterShortcutServiceHandler(ctx, mux, conn)\n}\n\n// RegisterShortcutServiceHandler registers the http handlers for service ShortcutService to \"mux\".\n// The handlers forward requests to the grpc endpoint over \"conn\".\nfunc RegisterShortcutServiceHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error {\n\treturn RegisterShortcutServiceHandlerClient(ctx, mux, NewShortcutServiceClient(conn))\n}\n\n// RegisterShortcutServiceHandlerClient registers the http handlers for service ShortcutService\n// to \"mux\". The handlers forward requests to the grpc endpoint over the given implementation of \"ShortcutServiceClient\".\n// Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in \"ShortcutServiceClient\"\n// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in\n// \"ShortcutServiceClient\" to call the correct interceptors. This client ignores the HTTP middlewares.\nfunc RegisterShortcutServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux, client ShortcutServiceClient) error {\n\tmux.Handle(http.MethodGet, pattern_ShortcutService_ListShortcuts_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateContext(ctx, mux, req, \"/memos.api.v1.ShortcutService/ListShortcuts\", runtime.WithHTTPPathPattern(\"/api/v1/{parent=users/*}/shortcuts\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := request_ShortcutService_ListShortcuts_0(annotatedContext, inboundMarshaler, client, req, pathParams)\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_ShortcutService_ListShortcuts_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodGet, pattern_ShortcutService_GetShortcut_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateContext(ctx, mux, req, \"/memos.api.v1.ShortcutService/GetShortcut\", runtime.WithHTTPPathPattern(\"/api/v1/{name=users/*/shortcuts/*}\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := request_ShortcutService_GetShortcut_0(annotatedContext, inboundMarshaler, client, req, pathParams)\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_ShortcutService_GetShortcut_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodPost, pattern_ShortcutService_CreateShortcut_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateContext(ctx, mux, req, \"/memos.api.v1.ShortcutService/CreateShortcut\", runtime.WithHTTPPathPattern(\"/api/v1/{parent=users/*}/shortcuts\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := request_ShortcutService_CreateShortcut_0(annotatedContext, inboundMarshaler, client, req, pathParams)\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_ShortcutService_CreateShortcut_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodPatch, pattern_ShortcutService_UpdateShortcut_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateContext(ctx, mux, req, \"/memos.api.v1.ShortcutService/UpdateShortcut\", runtime.WithHTTPPathPattern(\"/api/v1/{shortcut.name=users/*/shortcuts/*}\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := request_ShortcutService_UpdateShortcut_0(annotatedContext, inboundMarshaler, client, req, pathParams)\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_ShortcutService_UpdateShortcut_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodDelete, pattern_ShortcutService_DeleteShortcut_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateContext(ctx, mux, req, \"/memos.api.v1.ShortcutService/DeleteShortcut\", runtime.WithHTTPPathPattern(\"/api/v1/{name=users/*/shortcuts/*}\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := request_ShortcutService_DeleteShortcut_0(annotatedContext, inboundMarshaler, client, req, pathParams)\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_ShortcutService_DeleteShortcut_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\treturn nil\n}\n\nvar (\n\tpattern_ShortcutService_ListShortcuts_0  = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3, 2, 4}, []string{\"api\", \"v1\", \"users\", \"parent\", \"shortcuts\"}, \"\"))\n\tpattern_ShortcutService_GetShortcut_0    = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 2, 3, 1, 0, 4, 4, 5, 4}, []string{\"api\", \"v1\", \"users\", \"shortcuts\", \"name\"}, \"\"))\n\tpattern_ShortcutService_CreateShortcut_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3, 2, 4}, []string{\"api\", \"v1\", \"users\", \"parent\", \"shortcuts\"}, \"\"))\n\tpattern_ShortcutService_UpdateShortcut_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 2, 3, 1, 0, 4, 4, 5, 4}, []string{\"api\", \"v1\", \"users\", \"shortcuts\", \"shortcut.name\"}, \"\"))\n\tpattern_ShortcutService_DeleteShortcut_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 2, 3, 1, 0, 4, 4, 5, 4}, []string{\"api\", \"v1\", \"users\", \"shortcuts\", \"name\"}, \"\"))\n)\n\nvar (\n\tforward_ShortcutService_ListShortcuts_0  = runtime.ForwardResponseMessage\n\tforward_ShortcutService_GetShortcut_0    = runtime.ForwardResponseMessage\n\tforward_ShortcutService_CreateShortcut_0 = runtime.ForwardResponseMessage\n\tforward_ShortcutService_UpdateShortcut_0 = runtime.ForwardResponseMessage\n\tforward_ShortcutService_DeleteShortcut_0 = runtime.ForwardResponseMessage\n)\n"
  },
  {
    "path": "proto/gen/api/v1/shortcut_service_grpc.pb.go",
    "content": "// Code generated by protoc-gen-go-grpc. DO NOT EDIT.\n// versions:\n// - protoc-gen-go-grpc v1.6.1\n// - protoc             (unknown)\n// source: api/v1/shortcut_service.proto\n\npackage apiv1\n\nimport (\n\tcontext \"context\"\n\tgrpc \"google.golang.org/grpc\"\n\tcodes \"google.golang.org/grpc/codes\"\n\tstatus \"google.golang.org/grpc/status\"\n\temptypb \"google.golang.org/protobuf/types/known/emptypb\"\n)\n\n// This is a compile-time assertion to ensure that this generated file\n// is compatible with the grpc package it is being compiled against.\n// Requires gRPC-Go v1.64.0 or later.\nconst _ = grpc.SupportPackageIsVersion9\n\nconst (\n\tShortcutService_ListShortcuts_FullMethodName  = \"/memos.api.v1.ShortcutService/ListShortcuts\"\n\tShortcutService_GetShortcut_FullMethodName    = \"/memos.api.v1.ShortcutService/GetShortcut\"\n\tShortcutService_CreateShortcut_FullMethodName = \"/memos.api.v1.ShortcutService/CreateShortcut\"\n\tShortcutService_UpdateShortcut_FullMethodName = \"/memos.api.v1.ShortcutService/UpdateShortcut\"\n\tShortcutService_DeleteShortcut_FullMethodName = \"/memos.api.v1.ShortcutService/DeleteShortcut\"\n)\n\n// ShortcutServiceClient is the client API for ShortcutService service.\n//\n// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.\ntype ShortcutServiceClient interface {\n\t// ListShortcuts returns a list of shortcuts for a user.\n\tListShortcuts(ctx context.Context, in *ListShortcutsRequest, opts ...grpc.CallOption) (*ListShortcutsResponse, error)\n\t// GetShortcut gets a shortcut by name.\n\tGetShortcut(ctx context.Context, in *GetShortcutRequest, opts ...grpc.CallOption) (*Shortcut, error)\n\t// CreateShortcut creates a new shortcut for a user.\n\tCreateShortcut(ctx context.Context, in *CreateShortcutRequest, opts ...grpc.CallOption) (*Shortcut, error)\n\t// UpdateShortcut updates a shortcut for a user.\n\tUpdateShortcut(ctx context.Context, in *UpdateShortcutRequest, opts ...grpc.CallOption) (*Shortcut, error)\n\t// DeleteShortcut deletes a shortcut for a user.\n\tDeleteShortcut(ctx context.Context, in *DeleteShortcutRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)\n}\n\ntype shortcutServiceClient struct {\n\tcc grpc.ClientConnInterface\n}\n\nfunc NewShortcutServiceClient(cc grpc.ClientConnInterface) ShortcutServiceClient {\n\treturn &shortcutServiceClient{cc}\n}\n\nfunc (c *shortcutServiceClient) ListShortcuts(ctx context.Context, in *ListShortcutsRequest, opts ...grpc.CallOption) (*ListShortcutsResponse, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(ListShortcutsResponse)\n\terr := c.cc.Invoke(ctx, ShortcutService_ListShortcuts_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *shortcutServiceClient) GetShortcut(ctx context.Context, in *GetShortcutRequest, opts ...grpc.CallOption) (*Shortcut, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(Shortcut)\n\terr := c.cc.Invoke(ctx, ShortcutService_GetShortcut_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *shortcutServiceClient) CreateShortcut(ctx context.Context, in *CreateShortcutRequest, opts ...grpc.CallOption) (*Shortcut, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(Shortcut)\n\terr := c.cc.Invoke(ctx, ShortcutService_CreateShortcut_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *shortcutServiceClient) UpdateShortcut(ctx context.Context, in *UpdateShortcutRequest, opts ...grpc.CallOption) (*Shortcut, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(Shortcut)\n\terr := c.cc.Invoke(ctx, ShortcutService_UpdateShortcut_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *shortcutServiceClient) DeleteShortcut(ctx context.Context, in *DeleteShortcutRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(emptypb.Empty)\n\terr := c.cc.Invoke(ctx, ShortcutService_DeleteShortcut_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\n// ShortcutServiceServer is the server API for ShortcutService service.\n// All implementations must embed UnimplementedShortcutServiceServer\n// for forward compatibility.\ntype ShortcutServiceServer interface {\n\t// ListShortcuts returns a list of shortcuts for a user.\n\tListShortcuts(context.Context, *ListShortcutsRequest) (*ListShortcutsResponse, error)\n\t// GetShortcut gets a shortcut by name.\n\tGetShortcut(context.Context, *GetShortcutRequest) (*Shortcut, error)\n\t// CreateShortcut creates a new shortcut for a user.\n\tCreateShortcut(context.Context, *CreateShortcutRequest) (*Shortcut, error)\n\t// UpdateShortcut updates a shortcut for a user.\n\tUpdateShortcut(context.Context, *UpdateShortcutRequest) (*Shortcut, error)\n\t// DeleteShortcut deletes a shortcut for a user.\n\tDeleteShortcut(context.Context, *DeleteShortcutRequest) (*emptypb.Empty, error)\n\tmustEmbedUnimplementedShortcutServiceServer()\n}\n\n// UnimplementedShortcutServiceServer must be embedded to have\n// forward compatible implementations.\n//\n// NOTE: this should be embedded by value instead of pointer to avoid a nil\n// pointer dereference when methods are called.\ntype UnimplementedShortcutServiceServer struct{}\n\nfunc (UnimplementedShortcutServiceServer) ListShortcuts(context.Context, *ListShortcutsRequest) (*ListShortcutsResponse, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method ListShortcuts not implemented\")\n}\nfunc (UnimplementedShortcutServiceServer) GetShortcut(context.Context, *GetShortcutRequest) (*Shortcut, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method GetShortcut not implemented\")\n}\nfunc (UnimplementedShortcutServiceServer) CreateShortcut(context.Context, *CreateShortcutRequest) (*Shortcut, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method CreateShortcut not implemented\")\n}\nfunc (UnimplementedShortcutServiceServer) UpdateShortcut(context.Context, *UpdateShortcutRequest) (*Shortcut, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method UpdateShortcut not implemented\")\n}\nfunc (UnimplementedShortcutServiceServer) DeleteShortcut(context.Context, *DeleteShortcutRequest) (*emptypb.Empty, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method DeleteShortcut not implemented\")\n}\nfunc (UnimplementedShortcutServiceServer) mustEmbedUnimplementedShortcutServiceServer() {}\nfunc (UnimplementedShortcutServiceServer) testEmbeddedByValue()                         {}\n\n// UnsafeShortcutServiceServer may be embedded to opt out of forward compatibility for this service.\n// Use of this interface is not recommended, as added methods to ShortcutServiceServer will\n// result in compilation errors.\ntype UnsafeShortcutServiceServer interface {\n\tmustEmbedUnimplementedShortcutServiceServer()\n}\n\nfunc RegisterShortcutServiceServer(s grpc.ServiceRegistrar, srv ShortcutServiceServer) {\n\t// If the following call panics, it indicates UnimplementedShortcutServiceServer was\n\t// embedded by pointer and is nil.  This will cause panics if an\n\t// unimplemented method is ever invoked, so we test this at initialization\n\t// time to prevent it from happening at runtime later due to I/O.\n\tif t, ok := srv.(interface{ testEmbeddedByValue() }); ok {\n\t\tt.testEmbeddedByValue()\n\t}\n\ts.RegisterService(&ShortcutService_ServiceDesc, srv)\n}\n\nfunc _ShortcutService_ListShortcuts_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(ListShortcutsRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(ShortcutServiceServer).ListShortcuts(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: ShortcutService_ListShortcuts_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(ShortcutServiceServer).ListShortcuts(ctx, req.(*ListShortcutsRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _ShortcutService_GetShortcut_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(GetShortcutRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(ShortcutServiceServer).GetShortcut(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: ShortcutService_GetShortcut_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(ShortcutServiceServer).GetShortcut(ctx, req.(*GetShortcutRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _ShortcutService_CreateShortcut_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(CreateShortcutRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(ShortcutServiceServer).CreateShortcut(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: ShortcutService_CreateShortcut_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(ShortcutServiceServer).CreateShortcut(ctx, req.(*CreateShortcutRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _ShortcutService_UpdateShortcut_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(UpdateShortcutRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(ShortcutServiceServer).UpdateShortcut(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: ShortcutService_UpdateShortcut_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(ShortcutServiceServer).UpdateShortcut(ctx, req.(*UpdateShortcutRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _ShortcutService_DeleteShortcut_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(DeleteShortcutRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(ShortcutServiceServer).DeleteShortcut(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: ShortcutService_DeleteShortcut_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(ShortcutServiceServer).DeleteShortcut(ctx, req.(*DeleteShortcutRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\n// ShortcutService_ServiceDesc is the grpc.ServiceDesc for ShortcutService service.\n// It's only intended for direct use with grpc.RegisterService,\n// and not to be introspected or modified (even as a copy)\nvar ShortcutService_ServiceDesc = grpc.ServiceDesc{\n\tServiceName: \"memos.api.v1.ShortcutService\",\n\tHandlerType: (*ShortcutServiceServer)(nil),\n\tMethods: []grpc.MethodDesc{\n\t\t{\n\t\t\tMethodName: \"ListShortcuts\",\n\t\t\tHandler:    _ShortcutService_ListShortcuts_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"GetShortcut\",\n\t\t\tHandler:    _ShortcutService_GetShortcut_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"CreateShortcut\",\n\t\t\tHandler:    _ShortcutService_CreateShortcut_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"UpdateShortcut\",\n\t\t\tHandler:    _ShortcutService_UpdateShortcut_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"DeleteShortcut\",\n\t\t\tHandler:    _ShortcutService_DeleteShortcut_Handler,\n\t\t},\n\t},\n\tStreams:  []grpc.StreamDesc{},\n\tMetadata: \"api/v1/shortcut_service.proto\",\n}\n"
  },
  {
    "path": "proto/gen/api/v1/user_service.pb.go",
    "content": "// Code generated by protoc-gen-go. DO NOT EDIT.\n// versions:\n// \tprotoc-gen-go v1.36.11\n// \tprotoc        (unknown)\n// source: api/v1/user_service.proto\n\npackage apiv1\n\nimport (\n\t_ \"google.golang.org/genproto/googleapis/api/annotations\"\n\tprotoreflect \"google.golang.org/protobuf/reflect/protoreflect\"\n\tprotoimpl \"google.golang.org/protobuf/runtime/protoimpl\"\n\temptypb \"google.golang.org/protobuf/types/known/emptypb\"\n\tfieldmaskpb \"google.golang.org/protobuf/types/known/fieldmaskpb\"\n\ttimestamppb \"google.golang.org/protobuf/types/known/timestamppb\"\n\treflect \"reflect\"\n\tsync \"sync\"\n\tunsafe \"unsafe\"\n)\n\nconst (\n\t// Verify that this generated code is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)\n\t// Verify that runtime/protoimpl is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)\n)\n\n// User role enumeration.\ntype User_Role int32\n\nconst (\n\tUser_ROLE_UNSPECIFIED User_Role = 0\n\t// Admin role with system access.\n\tUser_ADMIN User_Role = 2\n\t// User role with limited access.\n\tUser_USER User_Role = 3\n)\n\n// Enum value maps for User_Role.\nvar (\n\tUser_Role_name = map[int32]string{\n\t\t0: \"ROLE_UNSPECIFIED\",\n\t\t2: \"ADMIN\",\n\t\t3: \"USER\",\n\t}\n\tUser_Role_value = map[string]int32{\n\t\t\"ROLE_UNSPECIFIED\": 0,\n\t\t\"ADMIN\":            2,\n\t\t\"USER\":             3,\n\t}\n)\n\nfunc (x User_Role) Enum() *User_Role {\n\tp := new(User_Role)\n\t*p = x\n\treturn p\n}\n\nfunc (x User_Role) String() string {\n\treturn protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))\n}\n\nfunc (User_Role) Descriptor() protoreflect.EnumDescriptor {\n\treturn file_api_v1_user_service_proto_enumTypes[0].Descriptor()\n}\n\nfunc (User_Role) Type() protoreflect.EnumType {\n\treturn &file_api_v1_user_service_proto_enumTypes[0]\n}\n\nfunc (x User_Role) Number() protoreflect.EnumNumber {\n\treturn protoreflect.EnumNumber(x)\n}\n\n// Deprecated: Use User_Role.Descriptor instead.\nfunc (User_Role) EnumDescriptor() ([]byte, []int) {\n\treturn file_api_v1_user_service_proto_rawDescGZIP(), []int{0, 0}\n}\n\n// Enumeration of user setting keys.\ntype UserSetting_Key int32\n\nconst (\n\tUserSetting_KEY_UNSPECIFIED UserSetting_Key = 0\n\t// GENERAL is the key for general user settings.\n\tUserSetting_GENERAL UserSetting_Key = 1\n\t// WEBHOOKS is the key for user webhooks.\n\tUserSetting_WEBHOOKS UserSetting_Key = 4\n)\n\n// Enum value maps for UserSetting_Key.\nvar (\n\tUserSetting_Key_name = map[int32]string{\n\t\t0: \"KEY_UNSPECIFIED\",\n\t\t1: \"GENERAL\",\n\t\t4: \"WEBHOOKS\",\n\t}\n\tUserSetting_Key_value = map[string]int32{\n\t\t\"KEY_UNSPECIFIED\": 0,\n\t\t\"GENERAL\":         1,\n\t\t\"WEBHOOKS\":        4,\n\t}\n)\n\nfunc (x UserSetting_Key) Enum() *UserSetting_Key {\n\tp := new(UserSetting_Key)\n\t*p = x\n\treturn p\n}\n\nfunc (x UserSetting_Key) String() string {\n\treturn protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))\n}\n\nfunc (UserSetting_Key) Descriptor() protoreflect.EnumDescriptor {\n\treturn file_api_v1_user_service_proto_enumTypes[1].Descriptor()\n}\n\nfunc (UserSetting_Key) Type() protoreflect.EnumType {\n\treturn &file_api_v1_user_service_proto_enumTypes[1]\n}\n\nfunc (x UserSetting_Key) Number() protoreflect.EnumNumber {\n\treturn protoreflect.EnumNumber(x)\n}\n\n// Deprecated: Use UserSetting_Key.Descriptor instead.\nfunc (UserSetting_Key) EnumDescriptor() ([]byte, []int) {\n\treturn file_api_v1_user_service_proto_rawDescGZIP(), []int{11, 0}\n}\n\ntype UserNotification_Status int32\n\nconst (\n\tUserNotification_STATUS_UNSPECIFIED UserNotification_Status = 0\n\tUserNotification_UNREAD             UserNotification_Status = 1\n\tUserNotification_ARCHIVED           UserNotification_Status = 2\n)\n\n// Enum value maps for UserNotification_Status.\nvar (\n\tUserNotification_Status_name = map[int32]string{\n\t\t0: \"STATUS_UNSPECIFIED\",\n\t\t1: \"UNREAD\",\n\t\t2: \"ARCHIVED\",\n\t}\n\tUserNotification_Status_value = map[string]int32{\n\t\t\"STATUS_UNSPECIFIED\": 0,\n\t\t\"UNREAD\":             1,\n\t\t\"ARCHIVED\":           2,\n\t}\n)\n\nfunc (x UserNotification_Status) Enum() *UserNotification_Status {\n\tp := new(UserNotification_Status)\n\t*p = x\n\treturn p\n}\n\nfunc (x UserNotification_Status) String() string {\n\treturn protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))\n}\n\nfunc (UserNotification_Status) Descriptor() protoreflect.EnumDescriptor {\n\treturn file_api_v1_user_service_proto_enumTypes[2].Descriptor()\n}\n\nfunc (UserNotification_Status) Type() protoreflect.EnumType {\n\treturn &file_api_v1_user_service_proto_enumTypes[2]\n}\n\nfunc (x UserNotification_Status) Number() protoreflect.EnumNumber {\n\treturn protoreflect.EnumNumber(x)\n}\n\n// Deprecated: Use UserNotification_Status.Descriptor instead.\nfunc (UserNotification_Status) EnumDescriptor() ([]byte, []int) {\n\treturn file_api_v1_user_service_proto_rawDescGZIP(), []int{28, 0}\n}\n\ntype UserNotification_Type int32\n\nconst (\n\tUserNotification_TYPE_UNSPECIFIED UserNotification_Type = 0\n\tUserNotification_MEMO_COMMENT     UserNotification_Type = 1\n)\n\n// Enum value maps for UserNotification_Type.\nvar (\n\tUserNotification_Type_name = map[int32]string{\n\t\t0: \"TYPE_UNSPECIFIED\",\n\t\t1: \"MEMO_COMMENT\",\n\t}\n\tUserNotification_Type_value = map[string]int32{\n\t\t\"TYPE_UNSPECIFIED\": 0,\n\t\t\"MEMO_COMMENT\":     1,\n\t}\n)\n\nfunc (x UserNotification_Type) Enum() *UserNotification_Type {\n\tp := new(UserNotification_Type)\n\t*p = x\n\treturn p\n}\n\nfunc (x UserNotification_Type) String() string {\n\treturn protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))\n}\n\nfunc (UserNotification_Type) Descriptor() protoreflect.EnumDescriptor {\n\treturn file_api_v1_user_service_proto_enumTypes[3].Descriptor()\n}\n\nfunc (UserNotification_Type) Type() protoreflect.EnumType {\n\treturn &file_api_v1_user_service_proto_enumTypes[3]\n}\n\nfunc (x UserNotification_Type) Number() protoreflect.EnumNumber {\n\treturn protoreflect.EnumNumber(x)\n}\n\n// Deprecated: Use UserNotification_Type.Descriptor instead.\nfunc (UserNotification_Type) EnumDescriptor() ([]byte, []int) {\n\treturn file_api_v1_user_service_proto_rawDescGZIP(), []int{28, 1}\n}\n\ntype User struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// The resource name of the user.\n\t// Format: users/{user}\n\tName string `protobuf:\"bytes,1,opt,name=name,proto3\" json:\"name,omitempty\"`\n\t// The role of the user.\n\tRole User_Role `protobuf:\"varint,2,opt,name=role,proto3,enum=memos.api.v1.User_Role\" json:\"role,omitempty\"`\n\t// Required. The unique username for login.\n\tUsername string `protobuf:\"bytes,3,opt,name=username,proto3\" json:\"username,omitempty\"`\n\t// Optional. The email address of the user.\n\tEmail string `protobuf:\"bytes,4,opt,name=email,proto3\" json:\"email,omitempty\"`\n\t// Optional. The display name of the user.\n\tDisplayName string `protobuf:\"bytes,5,opt,name=display_name,json=displayName,proto3\" json:\"display_name,omitempty\"`\n\t// Optional. The avatar URL of the user.\n\tAvatarUrl string `protobuf:\"bytes,6,opt,name=avatar_url,json=avatarUrl,proto3\" json:\"avatar_url,omitempty\"`\n\t// Optional. The description of the user.\n\tDescription string `protobuf:\"bytes,7,opt,name=description,proto3\" json:\"description,omitempty\"`\n\t// Input only. The password for the user.\n\tPassword string `protobuf:\"bytes,8,opt,name=password,proto3\" json:\"password,omitempty\"`\n\t// The state of the user.\n\tState State `protobuf:\"varint,9,opt,name=state,proto3,enum=memos.api.v1.State\" json:\"state,omitempty\"`\n\t// Output only. The creation timestamp.\n\tCreateTime *timestamppb.Timestamp `protobuf:\"bytes,10,opt,name=create_time,json=createTime,proto3\" json:\"create_time,omitempty\"`\n\t// Output only. The last update timestamp.\n\tUpdateTime    *timestamppb.Timestamp `protobuf:\"bytes,11,opt,name=update_time,json=updateTime,proto3\" json:\"update_time,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *User) Reset() {\n\t*x = User{}\n\tmi := &file_api_v1_user_service_proto_msgTypes[0]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *User) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*User) ProtoMessage() {}\n\nfunc (x *User) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_user_service_proto_msgTypes[0]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use User.ProtoReflect.Descriptor instead.\nfunc (*User) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_user_service_proto_rawDescGZIP(), []int{0}\n}\n\nfunc (x *User) GetName() string {\n\tif x != nil {\n\t\treturn x.Name\n\t}\n\treturn \"\"\n}\n\nfunc (x *User) GetRole() User_Role {\n\tif x != nil {\n\t\treturn x.Role\n\t}\n\treturn User_ROLE_UNSPECIFIED\n}\n\nfunc (x *User) GetUsername() string {\n\tif x != nil {\n\t\treturn x.Username\n\t}\n\treturn \"\"\n}\n\nfunc (x *User) GetEmail() string {\n\tif x != nil {\n\t\treturn x.Email\n\t}\n\treturn \"\"\n}\n\nfunc (x *User) GetDisplayName() string {\n\tif x != nil {\n\t\treturn x.DisplayName\n\t}\n\treturn \"\"\n}\n\nfunc (x *User) GetAvatarUrl() string {\n\tif x != nil {\n\t\treturn x.AvatarUrl\n\t}\n\treturn \"\"\n}\n\nfunc (x *User) GetDescription() string {\n\tif x != nil {\n\t\treturn x.Description\n\t}\n\treturn \"\"\n}\n\nfunc (x *User) GetPassword() string {\n\tif x != nil {\n\t\treturn x.Password\n\t}\n\treturn \"\"\n}\n\nfunc (x *User) GetState() State {\n\tif x != nil {\n\t\treturn x.State\n\t}\n\treturn State_STATE_UNSPECIFIED\n}\n\nfunc (x *User) GetCreateTime() *timestamppb.Timestamp {\n\tif x != nil {\n\t\treturn x.CreateTime\n\t}\n\treturn nil\n}\n\nfunc (x *User) GetUpdateTime() *timestamppb.Timestamp {\n\tif x != nil {\n\t\treturn x.UpdateTime\n\t}\n\treturn nil\n}\n\ntype ListUsersRequest struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// Optional. The maximum number of users to return.\n\t// The service may return fewer than this value.\n\t// If unspecified, at most 50 users will be returned.\n\t// The maximum value is 1000; values above 1000 will be coerced to 1000.\n\tPageSize int32 `protobuf:\"varint,1,opt,name=page_size,json=pageSize,proto3\" json:\"page_size,omitempty\"`\n\t// Optional. A page token, received from a previous `ListUsers` call.\n\t// Provide this to retrieve the subsequent page.\n\tPageToken string `protobuf:\"bytes,2,opt,name=page_token,json=pageToken,proto3\" json:\"page_token,omitempty\"`\n\t// Optional. Filter to apply to the list results.\n\t// Example: \"username == 'steven'\"\n\t// Supported operators: ==\n\t// Supported fields: username\n\tFilter string `protobuf:\"bytes,3,opt,name=filter,proto3\" json:\"filter,omitempty\"`\n\t// Optional. If true, show deleted users in the response.\n\tShowDeleted   bool `protobuf:\"varint,4,opt,name=show_deleted,json=showDeleted,proto3\" json:\"show_deleted,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *ListUsersRequest) Reset() {\n\t*x = ListUsersRequest{}\n\tmi := &file_api_v1_user_service_proto_msgTypes[1]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *ListUsersRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*ListUsersRequest) ProtoMessage() {}\n\nfunc (x *ListUsersRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_user_service_proto_msgTypes[1]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use ListUsersRequest.ProtoReflect.Descriptor instead.\nfunc (*ListUsersRequest) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_user_service_proto_rawDescGZIP(), []int{1}\n}\n\nfunc (x *ListUsersRequest) GetPageSize() int32 {\n\tif x != nil {\n\t\treturn x.PageSize\n\t}\n\treturn 0\n}\n\nfunc (x *ListUsersRequest) GetPageToken() string {\n\tif x != nil {\n\t\treturn x.PageToken\n\t}\n\treturn \"\"\n}\n\nfunc (x *ListUsersRequest) GetFilter() string {\n\tif x != nil {\n\t\treturn x.Filter\n\t}\n\treturn \"\"\n}\n\nfunc (x *ListUsersRequest) GetShowDeleted() bool {\n\tif x != nil {\n\t\treturn x.ShowDeleted\n\t}\n\treturn false\n}\n\ntype ListUsersResponse struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// The list of users.\n\tUsers []*User `protobuf:\"bytes,1,rep,name=users,proto3\" json:\"users,omitempty\"`\n\t// A token that can be sent as `page_token` to retrieve the next page.\n\t// If this field is omitted, there are no subsequent pages.\n\tNextPageToken string `protobuf:\"bytes,2,opt,name=next_page_token,json=nextPageToken,proto3\" json:\"next_page_token,omitempty\"`\n\t// The total count of users (may be approximate).\n\tTotalSize     int32 `protobuf:\"varint,3,opt,name=total_size,json=totalSize,proto3\" json:\"total_size,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *ListUsersResponse) Reset() {\n\t*x = ListUsersResponse{}\n\tmi := &file_api_v1_user_service_proto_msgTypes[2]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *ListUsersResponse) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*ListUsersResponse) ProtoMessage() {}\n\nfunc (x *ListUsersResponse) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_user_service_proto_msgTypes[2]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use ListUsersResponse.ProtoReflect.Descriptor instead.\nfunc (*ListUsersResponse) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_user_service_proto_rawDescGZIP(), []int{2}\n}\n\nfunc (x *ListUsersResponse) GetUsers() []*User {\n\tif x != nil {\n\t\treturn x.Users\n\t}\n\treturn nil\n}\n\nfunc (x *ListUsersResponse) GetNextPageToken() string {\n\tif x != nil {\n\t\treturn x.NextPageToken\n\t}\n\treturn \"\"\n}\n\nfunc (x *ListUsersResponse) GetTotalSize() int32 {\n\tif x != nil {\n\t\treturn x.TotalSize\n\t}\n\treturn 0\n}\n\ntype GetUserRequest struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// Required. The resource name of the user.\n\t// Supports both numeric IDs and username strings:\n\t//   - users/{id}       (e.g., users/101)\n\t//   - users/{username} (e.g., users/steven)\n\t//\n\t// Format: users/{id_or_username}\n\tName string `protobuf:\"bytes,1,opt,name=name,proto3\" json:\"name,omitempty\"`\n\t// Optional. The fields to return in the response.\n\t// If not specified, all fields are returned.\n\tReadMask      *fieldmaskpb.FieldMask `protobuf:\"bytes,2,opt,name=read_mask,json=readMask,proto3\" json:\"read_mask,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *GetUserRequest) Reset() {\n\t*x = GetUserRequest{}\n\tmi := &file_api_v1_user_service_proto_msgTypes[3]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *GetUserRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*GetUserRequest) ProtoMessage() {}\n\nfunc (x *GetUserRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_user_service_proto_msgTypes[3]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use GetUserRequest.ProtoReflect.Descriptor instead.\nfunc (*GetUserRequest) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_user_service_proto_rawDescGZIP(), []int{3}\n}\n\nfunc (x *GetUserRequest) GetName() string {\n\tif x != nil {\n\t\treturn x.Name\n\t}\n\treturn \"\"\n}\n\nfunc (x *GetUserRequest) GetReadMask() *fieldmaskpb.FieldMask {\n\tif x != nil {\n\t\treturn x.ReadMask\n\t}\n\treturn nil\n}\n\ntype CreateUserRequest struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// Required. The user to create.\n\tUser *User `protobuf:\"bytes,1,opt,name=user,proto3\" json:\"user,omitempty\"`\n\t// Optional. The user ID to use for this user.\n\t// If empty, a unique ID will be generated.\n\t// Must match the pattern [a-z0-9-]+\n\tUserId string `protobuf:\"bytes,2,opt,name=user_id,json=userId,proto3\" json:\"user_id,omitempty\"`\n\t// Optional. If set, validate the request but don't actually create the user.\n\tValidateOnly bool `protobuf:\"varint,3,opt,name=validate_only,json=validateOnly,proto3\" json:\"validate_only,omitempty\"`\n\t// Optional. An idempotency token that can be used to ensure that multiple\n\t// requests to create a user have the same result.\n\tRequestId     string `protobuf:\"bytes,4,opt,name=request_id,json=requestId,proto3\" json:\"request_id,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *CreateUserRequest) Reset() {\n\t*x = CreateUserRequest{}\n\tmi := &file_api_v1_user_service_proto_msgTypes[4]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *CreateUserRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*CreateUserRequest) ProtoMessage() {}\n\nfunc (x *CreateUserRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_user_service_proto_msgTypes[4]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use CreateUserRequest.ProtoReflect.Descriptor instead.\nfunc (*CreateUserRequest) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_user_service_proto_rawDescGZIP(), []int{4}\n}\n\nfunc (x *CreateUserRequest) GetUser() *User {\n\tif x != nil {\n\t\treturn x.User\n\t}\n\treturn nil\n}\n\nfunc (x *CreateUserRequest) GetUserId() string {\n\tif x != nil {\n\t\treturn x.UserId\n\t}\n\treturn \"\"\n}\n\nfunc (x *CreateUserRequest) GetValidateOnly() bool {\n\tif x != nil {\n\t\treturn x.ValidateOnly\n\t}\n\treturn false\n}\n\nfunc (x *CreateUserRequest) GetRequestId() string {\n\tif x != nil {\n\t\treturn x.RequestId\n\t}\n\treturn \"\"\n}\n\ntype UpdateUserRequest struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// Required. The user to update.\n\tUser *User `protobuf:\"bytes,1,opt,name=user,proto3\" json:\"user,omitempty\"`\n\t// Required. The list of fields to update.\n\tUpdateMask *fieldmaskpb.FieldMask `protobuf:\"bytes,2,opt,name=update_mask,json=updateMask,proto3\" json:\"update_mask,omitempty\"`\n\t// Optional. If set to true, allows updating sensitive fields.\n\tAllowMissing  bool `protobuf:\"varint,3,opt,name=allow_missing,json=allowMissing,proto3\" json:\"allow_missing,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *UpdateUserRequest) Reset() {\n\t*x = UpdateUserRequest{}\n\tmi := &file_api_v1_user_service_proto_msgTypes[5]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *UpdateUserRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*UpdateUserRequest) ProtoMessage() {}\n\nfunc (x *UpdateUserRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_user_service_proto_msgTypes[5]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use UpdateUserRequest.ProtoReflect.Descriptor instead.\nfunc (*UpdateUserRequest) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_user_service_proto_rawDescGZIP(), []int{5}\n}\n\nfunc (x *UpdateUserRequest) GetUser() *User {\n\tif x != nil {\n\t\treturn x.User\n\t}\n\treturn nil\n}\n\nfunc (x *UpdateUserRequest) GetUpdateMask() *fieldmaskpb.FieldMask {\n\tif x != nil {\n\t\treturn x.UpdateMask\n\t}\n\treturn nil\n}\n\nfunc (x *UpdateUserRequest) GetAllowMissing() bool {\n\tif x != nil {\n\t\treturn x.AllowMissing\n\t}\n\treturn false\n}\n\ntype DeleteUserRequest struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// Required. The resource name of the user to delete.\n\t// Format: users/{user}\n\tName string `protobuf:\"bytes,1,opt,name=name,proto3\" json:\"name,omitempty\"`\n\t// Optional. If set to true, the user will be deleted even if they have associated data.\n\tForce         bool `protobuf:\"varint,2,opt,name=force,proto3\" json:\"force,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *DeleteUserRequest) Reset() {\n\t*x = DeleteUserRequest{}\n\tmi := &file_api_v1_user_service_proto_msgTypes[6]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *DeleteUserRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*DeleteUserRequest) ProtoMessage() {}\n\nfunc (x *DeleteUserRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_user_service_proto_msgTypes[6]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use DeleteUserRequest.ProtoReflect.Descriptor instead.\nfunc (*DeleteUserRequest) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_user_service_proto_rawDescGZIP(), []int{6}\n}\n\nfunc (x *DeleteUserRequest) GetName() string {\n\tif x != nil {\n\t\treturn x.Name\n\t}\n\treturn \"\"\n}\n\nfunc (x *DeleteUserRequest) GetForce() bool {\n\tif x != nil {\n\t\treturn x.Force\n\t}\n\treturn false\n}\n\n// User statistics messages\ntype UserStats struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// The resource name of the user whose stats these are.\n\t// Format: users/{user}\n\tName string `protobuf:\"bytes,1,opt,name=name,proto3\" json:\"name,omitempty\"`\n\t// The timestamps when the memos were displayed.\n\tMemoDisplayTimestamps []*timestamppb.Timestamp `protobuf:\"bytes,2,rep,name=memo_display_timestamps,json=memoDisplayTimestamps,proto3\" json:\"memo_display_timestamps,omitempty\"`\n\t// The stats of memo types.\n\tMemoTypeStats *UserStats_MemoTypeStats `protobuf:\"bytes,3,opt,name=memo_type_stats,json=memoTypeStats,proto3\" json:\"memo_type_stats,omitempty\"`\n\t// The count of tags.\n\tTagCount map[string]int32 `protobuf:\"bytes,4,rep,name=tag_count,json=tagCount,proto3\" json:\"tag_count,omitempty\" protobuf_key:\"bytes,1,opt,name=key\" protobuf_val:\"varint,2,opt,name=value\"`\n\t// The pinned memos of the user.\n\tPinnedMemos []string `protobuf:\"bytes,5,rep,name=pinned_memos,json=pinnedMemos,proto3\" json:\"pinned_memos,omitempty\"`\n\t// Total memo count.\n\tTotalMemoCount int32 `protobuf:\"varint,6,opt,name=total_memo_count,json=totalMemoCount,proto3\" json:\"total_memo_count,omitempty\"`\n\tunknownFields  protoimpl.UnknownFields\n\tsizeCache      protoimpl.SizeCache\n}\n\nfunc (x *UserStats) Reset() {\n\t*x = UserStats{}\n\tmi := &file_api_v1_user_service_proto_msgTypes[7]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *UserStats) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*UserStats) ProtoMessage() {}\n\nfunc (x *UserStats) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_user_service_proto_msgTypes[7]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use UserStats.ProtoReflect.Descriptor instead.\nfunc (*UserStats) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_user_service_proto_rawDescGZIP(), []int{7}\n}\n\nfunc (x *UserStats) GetName() string {\n\tif x != nil {\n\t\treturn x.Name\n\t}\n\treturn \"\"\n}\n\nfunc (x *UserStats) GetMemoDisplayTimestamps() []*timestamppb.Timestamp {\n\tif x != nil {\n\t\treturn x.MemoDisplayTimestamps\n\t}\n\treturn nil\n}\n\nfunc (x *UserStats) GetMemoTypeStats() *UserStats_MemoTypeStats {\n\tif x != nil {\n\t\treturn x.MemoTypeStats\n\t}\n\treturn nil\n}\n\nfunc (x *UserStats) GetTagCount() map[string]int32 {\n\tif x != nil {\n\t\treturn x.TagCount\n\t}\n\treturn nil\n}\n\nfunc (x *UserStats) GetPinnedMemos() []string {\n\tif x != nil {\n\t\treturn x.PinnedMemos\n\t}\n\treturn nil\n}\n\nfunc (x *UserStats) GetTotalMemoCount() int32 {\n\tif x != nil {\n\t\treturn x.TotalMemoCount\n\t}\n\treturn 0\n}\n\ntype GetUserStatsRequest struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// Required. The resource name of the user.\n\t// Format: users/{user}\n\tName          string `protobuf:\"bytes,1,opt,name=name,proto3\" json:\"name,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *GetUserStatsRequest) Reset() {\n\t*x = GetUserStatsRequest{}\n\tmi := &file_api_v1_user_service_proto_msgTypes[8]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *GetUserStatsRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*GetUserStatsRequest) ProtoMessage() {}\n\nfunc (x *GetUserStatsRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_user_service_proto_msgTypes[8]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use GetUserStatsRequest.ProtoReflect.Descriptor instead.\nfunc (*GetUserStatsRequest) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_user_service_proto_rawDescGZIP(), []int{8}\n}\n\nfunc (x *GetUserStatsRequest) GetName() string {\n\tif x != nil {\n\t\treturn x.Name\n\t}\n\treturn \"\"\n}\n\ntype ListAllUserStatsRequest struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *ListAllUserStatsRequest) Reset() {\n\t*x = ListAllUserStatsRequest{}\n\tmi := &file_api_v1_user_service_proto_msgTypes[9]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *ListAllUserStatsRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*ListAllUserStatsRequest) ProtoMessage() {}\n\nfunc (x *ListAllUserStatsRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_user_service_proto_msgTypes[9]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use ListAllUserStatsRequest.ProtoReflect.Descriptor instead.\nfunc (*ListAllUserStatsRequest) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_user_service_proto_rawDescGZIP(), []int{9}\n}\n\ntype ListAllUserStatsResponse struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// The list of user statistics.\n\tStats         []*UserStats `protobuf:\"bytes,1,rep,name=stats,proto3\" json:\"stats,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *ListAllUserStatsResponse) Reset() {\n\t*x = ListAllUserStatsResponse{}\n\tmi := &file_api_v1_user_service_proto_msgTypes[10]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *ListAllUserStatsResponse) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*ListAllUserStatsResponse) ProtoMessage() {}\n\nfunc (x *ListAllUserStatsResponse) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_user_service_proto_msgTypes[10]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use ListAllUserStatsResponse.ProtoReflect.Descriptor instead.\nfunc (*ListAllUserStatsResponse) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_user_service_proto_rawDescGZIP(), []int{10}\n}\n\nfunc (x *ListAllUserStatsResponse) GetStats() []*UserStats {\n\tif x != nil {\n\t\treturn x.Stats\n\t}\n\treturn nil\n}\n\n// User settings message\ntype UserSetting struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// The name of the user setting.\n\t// Format: users/{user}/settings/{setting}, {setting} is the key for the setting.\n\t// For example, \"users/123/settings/GENERAL\" for general settings.\n\tName string `protobuf:\"bytes,1,opt,name=name,proto3\" json:\"name,omitempty\"`\n\t// Types that are valid to be assigned to Value:\n\t//\n\t//\t*UserSetting_GeneralSetting_\n\t//\t*UserSetting_WebhooksSetting_\n\tValue         isUserSetting_Value `protobuf_oneof:\"value\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *UserSetting) Reset() {\n\t*x = UserSetting{}\n\tmi := &file_api_v1_user_service_proto_msgTypes[11]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *UserSetting) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*UserSetting) ProtoMessage() {}\n\nfunc (x *UserSetting) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_user_service_proto_msgTypes[11]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use UserSetting.ProtoReflect.Descriptor instead.\nfunc (*UserSetting) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_user_service_proto_rawDescGZIP(), []int{11}\n}\n\nfunc (x *UserSetting) GetName() string {\n\tif x != nil {\n\t\treturn x.Name\n\t}\n\treturn \"\"\n}\n\nfunc (x *UserSetting) GetValue() isUserSetting_Value {\n\tif x != nil {\n\t\treturn x.Value\n\t}\n\treturn nil\n}\n\nfunc (x *UserSetting) GetGeneralSetting() *UserSetting_GeneralSetting {\n\tif x != nil {\n\t\tif x, ok := x.Value.(*UserSetting_GeneralSetting_); ok {\n\t\t\treturn x.GeneralSetting\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *UserSetting) GetWebhooksSetting() *UserSetting_WebhooksSetting {\n\tif x != nil {\n\t\tif x, ok := x.Value.(*UserSetting_WebhooksSetting_); ok {\n\t\t\treturn x.WebhooksSetting\n\t\t}\n\t}\n\treturn nil\n}\n\ntype isUserSetting_Value interface {\n\tisUserSetting_Value()\n}\n\ntype UserSetting_GeneralSetting_ struct {\n\tGeneralSetting *UserSetting_GeneralSetting `protobuf:\"bytes,2,opt,name=general_setting,json=generalSetting,proto3,oneof\"`\n}\n\ntype UserSetting_WebhooksSetting_ struct {\n\tWebhooksSetting *UserSetting_WebhooksSetting `protobuf:\"bytes,5,opt,name=webhooks_setting,json=webhooksSetting,proto3,oneof\"`\n}\n\nfunc (*UserSetting_GeneralSetting_) isUserSetting_Value() {}\n\nfunc (*UserSetting_WebhooksSetting_) isUserSetting_Value() {}\n\ntype GetUserSettingRequest struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// Required. The resource name of the user setting.\n\t// Format: users/{user}/settings/{setting}\n\tName          string `protobuf:\"bytes,1,opt,name=name,proto3\" json:\"name,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *GetUserSettingRequest) Reset() {\n\t*x = GetUserSettingRequest{}\n\tmi := &file_api_v1_user_service_proto_msgTypes[12]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *GetUserSettingRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*GetUserSettingRequest) ProtoMessage() {}\n\nfunc (x *GetUserSettingRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_user_service_proto_msgTypes[12]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use GetUserSettingRequest.ProtoReflect.Descriptor instead.\nfunc (*GetUserSettingRequest) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_user_service_proto_rawDescGZIP(), []int{12}\n}\n\nfunc (x *GetUserSettingRequest) GetName() string {\n\tif x != nil {\n\t\treturn x.Name\n\t}\n\treturn \"\"\n}\n\ntype UpdateUserSettingRequest struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// Required. The user setting to update.\n\tSetting *UserSetting `protobuf:\"bytes,1,opt,name=setting,proto3\" json:\"setting,omitempty\"`\n\t// Required. The list of fields to update.\n\tUpdateMask    *fieldmaskpb.FieldMask `protobuf:\"bytes,2,opt,name=update_mask,json=updateMask,proto3\" json:\"update_mask,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *UpdateUserSettingRequest) Reset() {\n\t*x = UpdateUserSettingRequest{}\n\tmi := &file_api_v1_user_service_proto_msgTypes[13]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *UpdateUserSettingRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*UpdateUserSettingRequest) ProtoMessage() {}\n\nfunc (x *UpdateUserSettingRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_user_service_proto_msgTypes[13]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use UpdateUserSettingRequest.ProtoReflect.Descriptor instead.\nfunc (*UpdateUserSettingRequest) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_user_service_proto_rawDescGZIP(), []int{13}\n}\n\nfunc (x *UpdateUserSettingRequest) GetSetting() *UserSetting {\n\tif x != nil {\n\t\treturn x.Setting\n\t}\n\treturn nil\n}\n\nfunc (x *UpdateUserSettingRequest) GetUpdateMask() *fieldmaskpb.FieldMask {\n\tif x != nil {\n\t\treturn x.UpdateMask\n\t}\n\treturn nil\n}\n\n// Request message for ListUserSettings method.\ntype ListUserSettingsRequest struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// Required. The parent resource whose settings will be listed.\n\t// Format: users/{user}\n\tParent string `protobuf:\"bytes,1,opt,name=parent,proto3\" json:\"parent,omitempty\"`\n\t// Optional. The maximum number of settings to return.\n\t// The service may return fewer than this value.\n\t// If unspecified, at most 50 settings will be returned.\n\t// The maximum value is 1000; values above 1000 will be coerced to 1000.\n\tPageSize int32 `protobuf:\"varint,2,opt,name=page_size,json=pageSize,proto3\" json:\"page_size,omitempty\"`\n\t// Optional. A page token, received from a previous `ListUserSettings` call.\n\t// Provide this to retrieve the subsequent page.\n\tPageToken     string `protobuf:\"bytes,3,opt,name=page_token,json=pageToken,proto3\" json:\"page_token,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *ListUserSettingsRequest) Reset() {\n\t*x = ListUserSettingsRequest{}\n\tmi := &file_api_v1_user_service_proto_msgTypes[14]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *ListUserSettingsRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*ListUserSettingsRequest) ProtoMessage() {}\n\nfunc (x *ListUserSettingsRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_user_service_proto_msgTypes[14]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use ListUserSettingsRequest.ProtoReflect.Descriptor instead.\nfunc (*ListUserSettingsRequest) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_user_service_proto_rawDescGZIP(), []int{14}\n}\n\nfunc (x *ListUserSettingsRequest) GetParent() string {\n\tif x != nil {\n\t\treturn x.Parent\n\t}\n\treturn \"\"\n}\n\nfunc (x *ListUserSettingsRequest) GetPageSize() int32 {\n\tif x != nil {\n\t\treturn x.PageSize\n\t}\n\treturn 0\n}\n\nfunc (x *ListUserSettingsRequest) GetPageToken() string {\n\tif x != nil {\n\t\treturn x.PageToken\n\t}\n\treturn \"\"\n}\n\n// Response message for ListUserSettings method.\ntype ListUserSettingsResponse struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// The list of user settings.\n\tSettings []*UserSetting `protobuf:\"bytes,1,rep,name=settings,proto3\" json:\"settings,omitempty\"`\n\t// A token that can be sent as `page_token` to retrieve the next page.\n\t// If this field is omitted, there are no subsequent pages.\n\tNextPageToken string `protobuf:\"bytes,2,opt,name=next_page_token,json=nextPageToken,proto3\" json:\"next_page_token,omitempty\"`\n\t// The total count of settings (may be approximate).\n\tTotalSize     int32 `protobuf:\"varint,3,opt,name=total_size,json=totalSize,proto3\" json:\"total_size,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *ListUserSettingsResponse) Reset() {\n\t*x = ListUserSettingsResponse{}\n\tmi := &file_api_v1_user_service_proto_msgTypes[15]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *ListUserSettingsResponse) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*ListUserSettingsResponse) ProtoMessage() {}\n\nfunc (x *ListUserSettingsResponse) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_user_service_proto_msgTypes[15]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use ListUserSettingsResponse.ProtoReflect.Descriptor instead.\nfunc (*ListUserSettingsResponse) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_user_service_proto_rawDescGZIP(), []int{15}\n}\n\nfunc (x *ListUserSettingsResponse) GetSettings() []*UserSetting {\n\tif x != nil {\n\t\treturn x.Settings\n\t}\n\treturn nil\n}\n\nfunc (x *ListUserSettingsResponse) GetNextPageToken() string {\n\tif x != nil {\n\t\treturn x.NextPageToken\n\t}\n\treturn \"\"\n}\n\nfunc (x *ListUserSettingsResponse) GetTotalSize() int32 {\n\tif x != nil {\n\t\treturn x.TotalSize\n\t}\n\treturn 0\n}\n\n// PersonalAccessToken represents a long-lived token for API/script access.\n// PATs are distinct from short-lived JWT access tokens used for session authentication.\ntype PersonalAccessToken struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// The resource name of the personal access token.\n\t// Format: users/{user}/personalAccessTokens/{personal_access_token}\n\tName string `protobuf:\"bytes,1,opt,name=name,proto3\" json:\"name,omitempty\"`\n\t// The description of the token.\n\tDescription string `protobuf:\"bytes,2,opt,name=description,proto3\" json:\"description,omitempty\"`\n\t// Output only. The creation timestamp.\n\tCreatedAt *timestamppb.Timestamp `protobuf:\"bytes,3,opt,name=created_at,json=createdAt,proto3\" json:\"created_at,omitempty\"`\n\t// Optional. The expiration timestamp.\n\tExpiresAt *timestamppb.Timestamp `protobuf:\"bytes,4,opt,name=expires_at,json=expiresAt,proto3\" json:\"expires_at,omitempty\"`\n\t// Output only. The last used timestamp.\n\tLastUsedAt    *timestamppb.Timestamp `protobuf:\"bytes,5,opt,name=last_used_at,json=lastUsedAt,proto3\" json:\"last_used_at,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *PersonalAccessToken) Reset() {\n\t*x = PersonalAccessToken{}\n\tmi := &file_api_v1_user_service_proto_msgTypes[16]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *PersonalAccessToken) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*PersonalAccessToken) ProtoMessage() {}\n\nfunc (x *PersonalAccessToken) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_user_service_proto_msgTypes[16]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use PersonalAccessToken.ProtoReflect.Descriptor instead.\nfunc (*PersonalAccessToken) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_user_service_proto_rawDescGZIP(), []int{16}\n}\n\nfunc (x *PersonalAccessToken) GetName() string {\n\tif x != nil {\n\t\treturn x.Name\n\t}\n\treturn \"\"\n}\n\nfunc (x *PersonalAccessToken) GetDescription() string {\n\tif x != nil {\n\t\treturn x.Description\n\t}\n\treturn \"\"\n}\n\nfunc (x *PersonalAccessToken) GetCreatedAt() *timestamppb.Timestamp {\n\tif x != nil {\n\t\treturn x.CreatedAt\n\t}\n\treturn nil\n}\n\nfunc (x *PersonalAccessToken) GetExpiresAt() *timestamppb.Timestamp {\n\tif x != nil {\n\t\treturn x.ExpiresAt\n\t}\n\treturn nil\n}\n\nfunc (x *PersonalAccessToken) GetLastUsedAt() *timestamppb.Timestamp {\n\tif x != nil {\n\t\treturn x.LastUsedAt\n\t}\n\treturn nil\n}\n\ntype ListPersonalAccessTokensRequest struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// Required. The parent resource whose personal access tokens will be listed.\n\t// Format: users/{user}\n\tParent string `protobuf:\"bytes,1,opt,name=parent,proto3\" json:\"parent,omitempty\"`\n\t// Optional. The maximum number of tokens to return.\n\tPageSize int32 `protobuf:\"varint,2,opt,name=page_size,json=pageSize,proto3\" json:\"page_size,omitempty\"`\n\t// Optional. A page token for pagination.\n\tPageToken     string `protobuf:\"bytes,3,opt,name=page_token,json=pageToken,proto3\" json:\"page_token,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *ListPersonalAccessTokensRequest) Reset() {\n\t*x = ListPersonalAccessTokensRequest{}\n\tmi := &file_api_v1_user_service_proto_msgTypes[17]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *ListPersonalAccessTokensRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*ListPersonalAccessTokensRequest) ProtoMessage() {}\n\nfunc (x *ListPersonalAccessTokensRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_user_service_proto_msgTypes[17]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use ListPersonalAccessTokensRequest.ProtoReflect.Descriptor instead.\nfunc (*ListPersonalAccessTokensRequest) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_user_service_proto_rawDescGZIP(), []int{17}\n}\n\nfunc (x *ListPersonalAccessTokensRequest) GetParent() string {\n\tif x != nil {\n\t\treturn x.Parent\n\t}\n\treturn \"\"\n}\n\nfunc (x *ListPersonalAccessTokensRequest) GetPageSize() int32 {\n\tif x != nil {\n\t\treturn x.PageSize\n\t}\n\treturn 0\n}\n\nfunc (x *ListPersonalAccessTokensRequest) GetPageToken() string {\n\tif x != nil {\n\t\treturn x.PageToken\n\t}\n\treturn \"\"\n}\n\ntype ListPersonalAccessTokensResponse struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// The list of personal access tokens.\n\tPersonalAccessTokens []*PersonalAccessToken `protobuf:\"bytes,1,rep,name=personal_access_tokens,json=personalAccessTokens,proto3\" json:\"personal_access_tokens,omitempty\"`\n\t// A token for the next page of results.\n\tNextPageToken string `protobuf:\"bytes,2,opt,name=next_page_token,json=nextPageToken,proto3\" json:\"next_page_token,omitempty\"`\n\t// The total count of personal access tokens.\n\tTotalSize     int32 `protobuf:\"varint,3,opt,name=total_size,json=totalSize,proto3\" json:\"total_size,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *ListPersonalAccessTokensResponse) Reset() {\n\t*x = ListPersonalAccessTokensResponse{}\n\tmi := &file_api_v1_user_service_proto_msgTypes[18]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *ListPersonalAccessTokensResponse) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*ListPersonalAccessTokensResponse) ProtoMessage() {}\n\nfunc (x *ListPersonalAccessTokensResponse) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_user_service_proto_msgTypes[18]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use ListPersonalAccessTokensResponse.ProtoReflect.Descriptor instead.\nfunc (*ListPersonalAccessTokensResponse) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_user_service_proto_rawDescGZIP(), []int{18}\n}\n\nfunc (x *ListPersonalAccessTokensResponse) GetPersonalAccessTokens() []*PersonalAccessToken {\n\tif x != nil {\n\t\treturn x.PersonalAccessTokens\n\t}\n\treturn nil\n}\n\nfunc (x *ListPersonalAccessTokensResponse) GetNextPageToken() string {\n\tif x != nil {\n\t\treturn x.NextPageToken\n\t}\n\treturn \"\"\n}\n\nfunc (x *ListPersonalAccessTokensResponse) GetTotalSize() int32 {\n\tif x != nil {\n\t\treturn x.TotalSize\n\t}\n\treturn 0\n}\n\ntype CreatePersonalAccessTokenRequest struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// Required. The parent resource where this token will be created.\n\t// Format: users/{user}\n\tParent string `protobuf:\"bytes,1,opt,name=parent,proto3\" json:\"parent,omitempty\"`\n\t// Optional. Description of the personal access token.\n\tDescription string `protobuf:\"bytes,2,opt,name=description,proto3\" json:\"description,omitempty\"`\n\t// Optional. Expiration duration in days (0 = never expires).\n\tExpiresInDays int32 `protobuf:\"varint,3,opt,name=expires_in_days,json=expiresInDays,proto3\" json:\"expires_in_days,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *CreatePersonalAccessTokenRequest) Reset() {\n\t*x = CreatePersonalAccessTokenRequest{}\n\tmi := &file_api_v1_user_service_proto_msgTypes[19]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *CreatePersonalAccessTokenRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*CreatePersonalAccessTokenRequest) ProtoMessage() {}\n\nfunc (x *CreatePersonalAccessTokenRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_user_service_proto_msgTypes[19]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use CreatePersonalAccessTokenRequest.ProtoReflect.Descriptor instead.\nfunc (*CreatePersonalAccessTokenRequest) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_user_service_proto_rawDescGZIP(), []int{19}\n}\n\nfunc (x *CreatePersonalAccessTokenRequest) GetParent() string {\n\tif x != nil {\n\t\treturn x.Parent\n\t}\n\treturn \"\"\n}\n\nfunc (x *CreatePersonalAccessTokenRequest) GetDescription() string {\n\tif x != nil {\n\t\treturn x.Description\n\t}\n\treturn \"\"\n}\n\nfunc (x *CreatePersonalAccessTokenRequest) GetExpiresInDays() int32 {\n\tif x != nil {\n\t\treturn x.ExpiresInDays\n\t}\n\treturn 0\n}\n\ntype CreatePersonalAccessTokenResponse struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// The personal access token metadata.\n\tPersonalAccessToken *PersonalAccessToken `protobuf:\"bytes,1,opt,name=personal_access_token,json=personalAccessToken,proto3\" json:\"personal_access_token,omitempty\"`\n\t// The actual token value - only returned on creation.\n\t// This is the only time the token value will be visible.\n\tToken         string `protobuf:\"bytes,2,opt,name=token,proto3\" json:\"token,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *CreatePersonalAccessTokenResponse) Reset() {\n\t*x = CreatePersonalAccessTokenResponse{}\n\tmi := &file_api_v1_user_service_proto_msgTypes[20]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *CreatePersonalAccessTokenResponse) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*CreatePersonalAccessTokenResponse) ProtoMessage() {}\n\nfunc (x *CreatePersonalAccessTokenResponse) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_user_service_proto_msgTypes[20]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use CreatePersonalAccessTokenResponse.ProtoReflect.Descriptor instead.\nfunc (*CreatePersonalAccessTokenResponse) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_user_service_proto_rawDescGZIP(), []int{20}\n}\n\nfunc (x *CreatePersonalAccessTokenResponse) GetPersonalAccessToken() *PersonalAccessToken {\n\tif x != nil {\n\t\treturn x.PersonalAccessToken\n\t}\n\treturn nil\n}\n\nfunc (x *CreatePersonalAccessTokenResponse) GetToken() string {\n\tif x != nil {\n\t\treturn x.Token\n\t}\n\treturn \"\"\n}\n\ntype DeletePersonalAccessTokenRequest struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// Required. The resource name of the personal access token to delete.\n\t// Format: users/{user}/personalAccessTokens/{personal_access_token}\n\tName          string `protobuf:\"bytes,1,opt,name=name,proto3\" json:\"name,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *DeletePersonalAccessTokenRequest) Reset() {\n\t*x = DeletePersonalAccessTokenRequest{}\n\tmi := &file_api_v1_user_service_proto_msgTypes[21]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *DeletePersonalAccessTokenRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*DeletePersonalAccessTokenRequest) ProtoMessage() {}\n\nfunc (x *DeletePersonalAccessTokenRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_user_service_proto_msgTypes[21]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use DeletePersonalAccessTokenRequest.ProtoReflect.Descriptor instead.\nfunc (*DeletePersonalAccessTokenRequest) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_user_service_proto_rawDescGZIP(), []int{21}\n}\n\nfunc (x *DeletePersonalAccessTokenRequest) GetName() string {\n\tif x != nil {\n\t\treturn x.Name\n\t}\n\treturn \"\"\n}\n\n// UserWebhook represents a webhook owned by a user.\ntype UserWebhook struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// The name of the webhook.\n\t// Format: users/{user}/webhooks/{webhook}\n\tName string `protobuf:\"bytes,1,opt,name=name,proto3\" json:\"name,omitempty\"`\n\t// The URL to send the webhook to.\n\tUrl string `protobuf:\"bytes,2,opt,name=url,proto3\" json:\"url,omitempty\"`\n\t// Optional. Human-readable name for the webhook.\n\tDisplayName string `protobuf:\"bytes,3,opt,name=display_name,json=displayName,proto3\" json:\"display_name,omitempty\"`\n\t// The creation time of the webhook.\n\tCreateTime *timestamppb.Timestamp `protobuf:\"bytes,4,opt,name=create_time,json=createTime,proto3\" json:\"create_time,omitempty\"`\n\t// The last update time of the webhook.\n\tUpdateTime    *timestamppb.Timestamp `protobuf:\"bytes,5,opt,name=update_time,json=updateTime,proto3\" json:\"update_time,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *UserWebhook) Reset() {\n\t*x = UserWebhook{}\n\tmi := &file_api_v1_user_service_proto_msgTypes[22]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *UserWebhook) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*UserWebhook) ProtoMessage() {}\n\nfunc (x *UserWebhook) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_user_service_proto_msgTypes[22]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use UserWebhook.ProtoReflect.Descriptor instead.\nfunc (*UserWebhook) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_user_service_proto_rawDescGZIP(), []int{22}\n}\n\nfunc (x *UserWebhook) GetName() string {\n\tif x != nil {\n\t\treturn x.Name\n\t}\n\treturn \"\"\n}\n\nfunc (x *UserWebhook) GetUrl() string {\n\tif x != nil {\n\t\treturn x.Url\n\t}\n\treturn \"\"\n}\n\nfunc (x *UserWebhook) GetDisplayName() string {\n\tif x != nil {\n\t\treturn x.DisplayName\n\t}\n\treturn \"\"\n}\n\nfunc (x *UserWebhook) GetCreateTime() *timestamppb.Timestamp {\n\tif x != nil {\n\t\treturn x.CreateTime\n\t}\n\treturn nil\n}\n\nfunc (x *UserWebhook) GetUpdateTime() *timestamppb.Timestamp {\n\tif x != nil {\n\t\treturn x.UpdateTime\n\t}\n\treturn nil\n}\n\ntype ListUserWebhooksRequest struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// The parent user resource.\n\t// Format: users/{user}\n\tParent        string `protobuf:\"bytes,1,opt,name=parent,proto3\" json:\"parent,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *ListUserWebhooksRequest) Reset() {\n\t*x = ListUserWebhooksRequest{}\n\tmi := &file_api_v1_user_service_proto_msgTypes[23]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *ListUserWebhooksRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*ListUserWebhooksRequest) ProtoMessage() {}\n\nfunc (x *ListUserWebhooksRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_user_service_proto_msgTypes[23]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use ListUserWebhooksRequest.ProtoReflect.Descriptor instead.\nfunc (*ListUserWebhooksRequest) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_user_service_proto_rawDescGZIP(), []int{23}\n}\n\nfunc (x *ListUserWebhooksRequest) GetParent() string {\n\tif x != nil {\n\t\treturn x.Parent\n\t}\n\treturn \"\"\n}\n\ntype ListUserWebhooksResponse struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// The list of webhooks.\n\tWebhooks      []*UserWebhook `protobuf:\"bytes,1,rep,name=webhooks,proto3\" json:\"webhooks,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *ListUserWebhooksResponse) Reset() {\n\t*x = ListUserWebhooksResponse{}\n\tmi := &file_api_v1_user_service_proto_msgTypes[24]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *ListUserWebhooksResponse) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*ListUserWebhooksResponse) ProtoMessage() {}\n\nfunc (x *ListUserWebhooksResponse) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_user_service_proto_msgTypes[24]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use ListUserWebhooksResponse.ProtoReflect.Descriptor instead.\nfunc (*ListUserWebhooksResponse) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_user_service_proto_rawDescGZIP(), []int{24}\n}\n\nfunc (x *ListUserWebhooksResponse) GetWebhooks() []*UserWebhook {\n\tif x != nil {\n\t\treturn x.Webhooks\n\t}\n\treturn nil\n}\n\ntype CreateUserWebhookRequest struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// The parent user resource.\n\t// Format: users/{user}\n\tParent string `protobuf:\"bytes,1,opt,name=parent,proto3\" json:\"parent,omitempty\"`\n\t// The webhook to create.\n\tWebhook       *UserWebhook `protobuf:\"bytes,2,opt,name=webhook,proto3\" json:\"webhook,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *CreateUserWebhookRequest) Reset() {\n\t*x = CreateUserWebhookRequest{}\n\tmi := &file_api_v1_user_service_proto_msgTypes[25]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *CreateUserWebhookRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*CreateUserWebhookRequest) ProtoMessage() {}\n\nfunc (x *CreateUserWebhookRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_user_service_proto_msgTypes[25]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use CreateUserWebhookRequest.ProtoReflect.Descriptor instead.\nfunc (*CreateUserWebhookRequest) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_user_service_proto_rawDescGZIP(), []int{25}\n}\n\nfunc (x *CreateUserWebhookRequest) GetParent() string {\n\tif x != nil {\n\t\treturn x.Parent\n\t}\n\treturn \"\"\n}\n\nfunc (x *CreateUserWebhookRequest) GetWebhook() *UserWebhook {\n\tif x != nil {\n\t\treturn x.Webhook\n\t}\n\treturn nil\n}\n\ntype UpdateUserWebhookRequest struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// The webhook to update.\n\tWebhook *UserWebhook `protobuf:\"bytes,1,opt,name=webhook,proto3\" json:\"webhook,omitempty\"`\n\t// The list of fields to update.\n\tUpdateMask    *fieldmaskpb.FieldMask `protobuf:\"bytes,2,opt,name=update_mask,json=updateMask,proto3\" json:\"update_mask,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *UpdateUserWebhookRequest) Reset() {\n\t*x = UpdateUserWebhookRequest{}\n\tmi := &file_api_v1_user_service_proto_msgTypes[26]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *UpdateUserWebhookRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*UpdateUserWebhookRequest) ProtoMessage() {}\n\nfunc (x *UpdateUserWebhookRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_user_service_proto_msgTypes[26]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use UpdateUserWebhookRequest.ProtoReflect.Descriptor instead.\nfunc (*UpdateUserWebhookRequest) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_user_service_proto_rawDescGZIP(), []int{26}\n}\n\nfunc (x *UpdateUserWebhookRequest) GetWebhook() *UserWebhook {\n\tif x != nil {\n\t\treturn x.Webhook\n\t}\n\treturn nil\n}\n\nfunc (x *UpdateUserWebhookRequest) GetUpdateMask() *fieldmaskpb.FieldMask {\n\tif x != nil {\n\t\treturn x.UpdateMask\n\t}\n\treturn nil\n}\n\ntype DeleteUserWebhookRequest struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// The name of the webhook to delete.\n\t// Format: users/{user}/webhooks/{webhook}\n\tName          string `protobuf:\"bytes,1,opt,name=name,proto3\" json:\"name,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *DeleteUserWebhookRequest) Reset() {\n\t*x = DeleteUserWebhookRequest{}\n\tmi := &file_api_v1_user_service_proto_msgTypes[27]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *DeleteUserWebhookRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*DeleteUserWebhookRequest) ProtoMessage() {}\n\nfunc (x *DeleteUserWebhookRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_user_service_proto_msgTypes[27]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use DeleteUserWebhookRequest.ProtoReflect.Descriptor instead.\nfunc (*DeleteUserWebhookRequest) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_user_service_proto_rawDescGZIP(), []int{27}\n}\n\nfunc (x *DeleteUserWebhookRequest) GetName() string {\n\tif x != nil {\n\t\treturn x.Name\n\t}\n\treturn \"\"\n}\n\ntype UserNotification struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// The resource name of the notification.\n\t// Format: users/{user}/notifications/{notification}\n\tName string `protobuf:\"bytes,1,opt,name=name,proto3\" json:\"name,omitempty\"`\n\t// The sender of the notification.\n\t// Format: users/{user}\n\tSender string `protobuf:\"bytes,2,opt,name=sender,proto3\" json:\"sender,omitempty\"`\n\t// The status of the notification.\n\tStatus UserNotification_Status `protobuf:\"varint,3,opt,name=status,proto3,enum=memos.api.v1.UserNotification_Status\" json:\"status,omitempty\"`\n\t// The creation timestamp.\n\tCreateTime *timestamppb.Timestamp `protobuf:\"bytes,4,opt,name=create_time,json=createTime,proto3\" json:\"create_time,omitempty\"`\n\t// The type of the notification.\n\tType UserNotification_Type `protobuf:\"varint,5,opt,name=type,proto3,enum=memos.api.v1.UserNotification_Type\" json:\"type,omitempty\"`\n\t// Types that are valid to be assigned to Payload:\n\t//\n\t//\t*UserNotification_MemoComment\n\tPayload       isUserNotification_Payload `protobuf_oneof:\"payload\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *UserNotification) Reset() {\n\t*x = UserNotification{}\n\tmi := &file_api_v1_user_service_proto_msgTypes[28]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *UserNotification) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*UserNotification) ProtoMessage() {}\n\nfunc (x *UserNotification) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_user_service_proto_msgTypes[28]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use UserNotification.ProtoReflect.Descriptor instead.\nfunc (*UserNotification) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_user_service_proto_rawDescGZIP(), []int{28}\n}\n\nfunc (x *UserNotification) GetName() string {\n\tif x != nil {\n\t\treturn x.Name\n\t}\n\treturn \"\"\n}\n\nfunc (x *UserNotification) GetSender() string {\n\tif x != nil {\n\t\treturn x.Sender\n\t}\n\treturn \"\"\n}\n\nfunc (x *UserNotification) GetStatus() UserNotification_Status {\n\tif x != nil {\n\t\treturn x.Status\n\t}\n\treturn UserNotification_STATUS_UNSPECIFIED\n}\n\nfunc (x *UserNotification) GetCreateTime() *timestamppb.Timestamp {\n\tif x != nil {\n\t\treturn x.CreateTime\n\t}\n\treturn nil\n}\n\nfunc (x *UserNotification) GetType() UserNotification_Type {\n\tif x != nil {\n\t\treturn x.Type\n\t}\n\treturn UserNotification_TYPE_UNSPECIFIED\n}\n\nfunc (x *UserNotification) GetPayload() isUserNotification_Payload {\n\tif x != nil {\n\t\treturn x.Payload\n\t}\n\treturn nil\n}\n\nfunc (x *UserNotification) GetMemoComment() *UserNotification_MemoCommentPayload {\n\tif x != nil {\n\t\tif x, ok := x.Payload.(*UserNotification_MemoComment); ok {\n\t\t\treturn x.MemoComment\n\t\t}\n\t}\n\treturn nil\n}\n\ntype isUserNotification_Payload interface {\n\tisUserNotification_Payload()\n}\n\ntype UserNotification_MemoComment struct {\n\tMemoComment *UserNotification_MemoCommentPayload `protobuf:\"bytes,6,opt,name=memo_comment,json=memoComment,proto3,oneof\"`\n}\n\nfunc (*UserNotification_MemoComment) isUserNotification_Payload() {}\n\ntype ListUserNotificationsRequest struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// The parent user resource.\n\t// Format: users/{user}\n\tParent        string `protobuf:\"bytes,1,opt,name=parent,proto3\" json:\"parent,omitempty\"`\n\tPageSize      int32  `protobuf:\"varint,2,opt,name=page_size,json=pageSize,proto3\" json:\"page_size,omitempty\"`\n\tPageToken     string `protobuf:\"bytes,3,opt,name=page_token,json=pageToken,proto3\" json:\"page_token,omitempty\"`\n\tFilter        string `protobuf:\"bytes,4,opt,name=filter,proto3\" json:\"filter,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *ListUserNotificationsRequest) Reset() {\n\t*x = ListUserNotificationsRequest{}\n\tmi := &file_api_v1_user_service_proto_msgTypes[29]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *ListUserNotificationsRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*ListUserNotificationsRequest) ProtoMessage() {}\n\nfunc (x *ListUserNotificationsRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_user_service_proto_msgTypes[29]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use ListUserNotificationsRequest.ProtoReflect.Descriptor instead.\nfunc (*ListUserNotificationsRequest) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_user_service_proto_rawDescGZIP(), []int{29}\n}\n\nfunc (x *ListUserNotificationsRequest) GetParent() string {\n\tif x != nil {\n\t\treturn x.Parent\n\t}\n\treturn \"\"\n}\n\nfunc (x *ListUserNotificationsRequest) GetPageSize() int32 {\n\tif x != nil {\n\t\treturn x.PageSize\n\t}\n\treturn 0\n}\n\nfunc (x *ListUserNotificationsRequest) GetPageToken() string {\n\tif x != nil {\n\t\treturn x.PageToken\n\t}\n\treturn \"\"\n}\n\nfunc (x *ListUserNotificationsRequest) GetFilter() string {\n\tif x != nil {\n\t\treturn x.Filter\n\t}\n\treturn \"\"\n}\n\ntype ListUserNotificationsResponse struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tNotifications []*UserNotification    `protobuf:\"bytes,1,rep,name=notifications,proto3\" json:\"notifications,omitempty\"`\n\tNextPageToken string                 `protobuf:\"bytes,2,opt,name=next_page_token,json=nextPageToken,proto3\" json:\"next_page_token,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *ListUserNotificationsResponse) Reset() {\n\t*x = ListUserNotificationsResponse{}\n\tmi := &file_api_v1_user_service_proto_msgTypes[30]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *ListUserNotificationsResponse) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*ListUserNotificationsResponse) ProtoMessage() {}\n\nfunc (x *ListUserNotificationsResponse) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_user_service_proto_msgTypes[30]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use ListUserNotificationsResponse.ProtoReflect.Descriptor instead.\nfunc (*ListUserNotificationsResponse) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_user_service_proto_rawDescGZIP(), []int{30}\n}\n\nfunc (x *ListUserNotificationsResponse) GetNotifications() []*UserNotification {\n\tif x != nil {\n\t\treturn x.Notifications\n\t}\n\treturn nil\n}\n\nfunc (x *ListUserNotificationsResponse) GetNextPageToken() string {\n\tif x != nil {\n\t\treturn x.NextPageToken\n\t}\n\treturn \"\"\n}\n\ntype UpdateUserNotificationRequest struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tNotification  *UserNotification      `protobuf:\"bytes,1,opt,name=notification,proto3\" json:\"notification,omitempty\"`\n\tUpdateMask    *fieldmaskpb.FieldMask `protobuf:\"bytes,2,opt,name=update_mask,json=updateMask,proto3\" json:\"update_mask,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *UpdateUserNotificationRequest) Reset() {\n\t*x = UpdateUserNotificationRequest{}\n\tmi := &file_api_v1_user_service_proto_msgTypes[31]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *UpdateUserNotificationRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*UpdateUserNotificationRequest) ProtoMessage() {}\n\nfunc (x *UpdateUserNotificationRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_user_service_proto_msgTypes[31]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use UpdateUserNotificationRequest.ProtoReflect.Descriptor instead.\nfunc (*UpdateUserNotificationRequest) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_user_service_proto_rawDescGZIP(), []int{31}\n}\n\nfunc (x *UpdateUserNotificationRequest) GetNotification() *UserNotification {\n\tif x != nil {\n\t\treturn x.Notification\n\t}\n\treturn nil\n}\n\nfunc (x *UpdateUserNotificationRequest) GetUpdateMask() *fieldmaskpb.FieldMask {\n\tif x != nil {\n\t\treturn x.UpdateMask\n\t}\n\treturn nil\n}\n\ntype DeleteUserNotificationRequest struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// Format: users/{user}/notifications/{notification}\n\tName          string `protobuf:\"bytes,1,opt,name=name,proto3\" json:\"name,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *DeleteUserNotificationRequest) Reset() {\n\t*x = DeleteUserNotificationRequest{}\n\tmi := &file_api_v1_user_service_proto_msgTypes[32]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *DeleteUserNotificationRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*DeleteUserNotificationRequest) ProtoMessage() {}\n\nfunc (x *DeleteUserNotificationRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_user_service_proto_msgTypes[32]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use DeleteUserNotificationRequest.ProtoReflect.Descriptor instead.\nfunc (*DeleteUserNotificationRequest) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_user_service_proto_rawDescGZIP(), []int{32}\n}\n\nfunc (x *DeleteUserNotificationRequest) GetName() string {\n\tif x != nil {\n\t\treturn x.Name\n\t}\n\treturn \"\"\n}\n\n// Memo type statistics.\ntype UserStats_MemoTypeStats struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tLinkCount     int32                  `protobuf:\"varint,1,opt,name=link_count,json=linkCount,proto3\" json:\"link_count,omitempty\"`\n\tCodeCount     int32                  `protobuf:\"varint,2,opt,name=code_count,json=codeCount,proto3\" json:\"code_count,omitempty\"`\n\tTodoCount     int32                  `protobuf:\"varint,3,opt,name=todo_count,json=todoCount,proto3\" json:\"todo_count,omitempty\"`\n\tUndoCount     int32                  `protobuf:\"varint,4,opt,name=undo_count,json=undoCount,proto3\" json:\"undo_count,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *UserStats_MemoTypeStats) Reset() {\n\t*x = UserStats_MemoTypeStats{}\n\tmi := &file_api_v1_user_service_proto_msgTypes[34]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *UserStats_MemoTypeStats) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*UserStats_MemoTypeStats) ProtoMessage() {}\n\nfunc (x *UserStats_MemoTypeStats) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_user_service_proto_msgTypes[34]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use UserStats_MemoTypeStats.ProtoReflect.Descriptor instead.\nfunc (*UserStats_MemoTypeStats) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_user_service_proto_rawDescGZIP(), []int{7, 1}\n}\n\nfunc (x *UserStats_MemoTypeStats) GetLinkCount() int32 {\n\tif x != nil {\n\t\treturn x.LinkCount\n\t}\n\treturn 0\n}\n\nfunc (x *UserStats_MemoTypeStats) GetCodeCount() int32 {\n\tif x != nil {\n\t\treturn x.CodeCount\n\t}\n\treturn 0\n}\n\nfunc (x *UserStats_MemoTypeStats) GetTodoCount() int32 {\n\tif x != nil {\n\t\treturn x.TodoCount\n\t}\n\treturn 0\n}\n\nfunc (x *UserStats_MemoTypeStats) GetUndoCount() int32 {\n\tif x != nil {\n\t\treturn x.UndoCount\n\t}\n\treturn 0\n}\n\n// General user settings configuration.\ntype UserSetting_GeneralSetting struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// The preferred locale of the user.\n\tLocale string `protobuf:\"bytes,1,opt,name=locale,proto3\" json:\"locale,omitempty\"`\n\t// The default visibility of the memo.\n\tMemoVisibility string `protobuf:\"bytes,3,opt,name=memo_visibility,json=memoVisibility,proto3\" json:\"memo_visibility,omitempty\"`\n\t// The preferred theme of the user.\n\t// This references a CSS file in the web/public/themes/ directory.\n\t// If not set, the default theme will be used.\n\tTheme         string `protobuf:\"bytes,4,opt,name=theme,proto3\" json:\"theme,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *UserSetting_GeneralSetting) Reset() {\n\t*x = UserSetting_GeneralSetting{}\n\tmi := &file_api_v1_user_service_proto_msgTypes[35]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *UserSetting_GeneralSetting) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*UserSetting_GeneralSetting) ProtoMessage() {}\n\nfunc (x *UserSetting_GeneralSetting) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_user_service_proto_msgTypes[35]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use UserSetting_GeneralSetting.ProtoReflect.Descriptor instead.\nfunc (*UserSetting_GeneralSetting) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_user_service_proto_rawDescGZIP(), []int{11, 0}\n}\n\nfunc (x *UserSetting_GeneralSetting) GetLocale() string {\n\tif x != nil {\n\t\treturn x.Locale\n\t}\n\treturn \"\"\n}\n\nfunc (x *UserSetting_GeneralSetting) GetMemoVisibility() string {\n\tif x != nil {\n\t\treturn x.MemoVisibility\n\t}\n\treturn \"\"\n}\n\nfunc (x *UserSetting_GeneralSetting) GetTheme() string {\n\tif x != nil {\n\t\treturn x.Theme\n\t}\n\treturn \"\"\n}\n\n// User webhooks configuration.\ntype UserSetting_WebhooksSetting struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// List of user webhooks.\n\tWebhooks      []*UserWebhook `protobuf:\"bytes,1,rep,name=webhooks,proto3\" json:\"webhooks,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *UserSetting_WebhooksSetting) Reset() {\n\t*x = UserSetting_WebhooksSetting{}\n\tmi := &file_api_v1_user_service_proto_msgTypes[36]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *UserSetting_WebhooksSetting) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*UserSetting_WebhooksSetting) ProtoMessage() {}\n\nfunc (x *UserSetting_WebhooksSetting) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_user_service_proto_msgTypes[36]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use UserSetting_WebhooksSetting.ProtoReflect.Descriptor instead.\nfunc (*UserSetting_WebhooksSetting) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_user_service_proto_rawDescGZIP(), []int{11, 1}\n}\n\nfunc (x *UserSetting_WebhooksSetting) GetWebhooks() []*UserWebhook {\n\tif x != nil {\n\t\treturn x.Webhooks\n\t}\n\treturn nil\n}\n\ntype UserNotification_MemoCommentPayload struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// The memo name of comment.\n\t// Format: memos/{memo}\n\tMemo string `protobuf:\"bytes,1,opt,name=memo,proto3\" json:\"memo,omitempty\"`\n\t// The name of related memo.\n\t// Format: memos/{memo}\n\tRelatedMemo   string `protobuf:\"bytes,2,opt,name=related_memo,json=relatedMemo,proto3\" json:\"related_memo,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *UserNotification_MemoCommentPayload) Reset() {\n\t*x = UserNotification_MemoCommentPayload{}\n\tmi := &file_api_v1_user_service_proto_msgTypes[37]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *UserNotification_MemoCommentPayload) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*UserNotification_MemoCommentPayload) ProtoMessage() {}\n\nfunc (x *UserNotification_MemoCommentPayload) ProtoReflect() protoreflect.Message {\n\tmi := &file_api_v1_user_service_proto_msgTypes[37]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use UserNotification_MemoCommentPayload.ProtoReflect.Descriptor instead.\nfunc (*UserNotification_MemoCommentPayload) Descriptor() ([]byte, []int) {\n\treturn file_api_v1_user_service_proto_rawDescGZIP(), []int{28, 0}\n}\n\nfunc (x *UserNotification_MemoCommentPayload) GetMemo() string {\n\tif x != nil {\n\t\treturn x.Memo\n\t}\n\treturn \"\"\n}\n\nfunc (x *UserNotification_MemoCommentPayload) GetRelatedMemo() string {\n\tif x != nil {\n\t\treturn x.RelatedMemo\n\t}\n\treturn \"\"\n}\n\nvar File_api_v1_user_service_proto protoreflect.FileDescriptor\n\nconst file_api_v1_user_service_proto_rawDesc = \"\" +\n\t\"\\n\" +\n\t\"\\x19api/v1/user_service.proto\\x12\\fmemos.api.v1\\x1a\\x13api/v1/common.proto\\x1a\\x1cgoogle/api/annotations.proto\\x1a\\x17google/api/client.proto\\x1a\\x1fgoogle/api/field_behavior.proto\\x1a\\x19google/api/resource.proto\\x1a\\x1bgoogle/protobuf/empty.proto\\x1a google/protobuf/field_mask.proto\\x1a\\x1fgoogle/protobuf/timestamp.proto\\\"\\xc1\\x04\\n\" +\n\t\"\\x04User\\x12\\x17\\n\" +\n\t\"\\x04name\\x18\\x01 \\x01(\\tB\\x03\\xe0A\\bR\\x04name\\x120\\n\" +\n\t\"\\x04role\\x18\\x02 \\x01(\\x0e2\\x17.memos.api.v1.User.RoleB\\x03\\xe0A\\x02R\\x04role\\x12\\x1f\\n\" +\n\t\"\\busername\\x18\\x03 \\x01(\\tB\\x03\\xe0A\\x02R\\busername\\x12\\x19\\n\" +\n\t\"\\x05email\\x18\\x04 \\x01(\\tB\\x03\\xe0A\\x01R\\x05email\\x12&\\n\" +\n\t\"\\fdisplay_name\\x18\\x05 \\x01(\\tB\\x03\\xe0A\\x01R\\vdisplayName\\x12\\\"\\n\" +\n\t\"\\n\" +\n\t\"avatar_url\\x18\\x06 \\x01(\\tB\\x03\\xe0A\\x01R\\tavatarUrl\\x12%\\n\" +\n\t\"\\vdescription\\x18\\a \\x01(\\tB\\x03\\xe0A\\x01R\\vdescription\\x12\\x1f\\n\" +\n\t\"\\bpassword\\x18\\b \\x01(\\tB\\x03\\xe0A\\x04R\\bpassword\\x12.\\n\" +\n\t\"\\x05state\\x18\\t \\x01(\\x0e2\\x13.memos.api.v1.StateB\\x03\\xe0A\\x02R\\x05state\\x12@\\n\" +\n\t\"\\vcreate_time\\x18\\n\" +\n\t\" \\x01(\\v2\\x1a.google.protobuf.TimestampB\\x03\\xe0A\\x03R\\n\" +\n\t\"createTime\\x12@\\n\" +\n\t\"\\vupdate_time\\x18\\v \\x01(\\v2\\x1a.google.protobuf.TimestampB\\x03\\xe0A\\x03R\\n\" +\n\t\"updateTime\\\"1\\n\" +\n\t\"\\x04Role\\x12\\x14\\n\" +\n\t\"\\x10ROLE_UNSPECIFIED\\x10\\x00\\x12\\t\\n\" +\n\t\"\\x05ADMIN\\x10\\x02\\x12\\b\\n\" +\n\t\"\\x04USER\\x10\\x03:7\\xeaA4\\n\" +\n\t\"\\x11memos.api.v1/User\\x12\\fusers/{user}\\x1a\\x04name*\\x05users2\\x04user\\\"\\x9d\\x01\\n\" +\n\t\"\\x10ListUsersRequest\\x12 \\n\" +\n\t\"\\tpage_size\\x18\\x01 \\x01(\\x05B\\x03\\xe0A\\x01R\\bpageSize\\x12\\\"\\n\" +\n\t\"\\n\" +\n\t\"page_token\\x18\\x02 \\x01(\\tB\\x03\\xe0A\\x01R\\tpageToken\\x12\\x1b\\n\" +\n\t\"\\x06filter\\x18\\x03 \\x01(\\tB\\x03\\xe0A\\x01R\\x06filter\\x12&\\n\" +\n\t\"\\fshow_deleted\\x18\\x04 \\x01(\\bB\\x03\\xe0A\\x01R\\vshowDeleted\\\"\\x84\\x01\\n\" +\n\t\"\\x11ListUsersResponse\\x12(\\n\" +\n\t\"\\x05users\\x18\\x01 \\x03(\\v2\\x12.memos.api.v1.UserR\\x05users\\x12&\\n\" +\n\t\"\\x0fnext_page_token\\x18\\x02 \\x01(\\tR\\rnextPageToken\\x12\\x1d\\n\" +\n\t\"\\n\" +\n\t\"total_size\\x18\\x03 \\x01(\\x05R\\ttotalSize\\\"}\\n\" +\n\t\"\\x0eGetUserRequest\\x12-\\n\" +\n\t\"\\x04name\\x18\\x01 \\x01(\\tB\\x19\\xe0A\\x02\\xfaA\\x13\\n\" +\n\t\"\\x11memos.api.v1/UserR\\x04name\\x12<\\n\" +\n\t\"\\tread_mask\\x18\\x02 \\x01(\\v2\\x1a.google.protobuf.FieldMaskB\\x03\\xe0A\\x01R\\breadMask\\\"\\xaf\\x01\\n\" +\n\t\"\\x11CreateUserRequest\\x12.\\n\" +\n\t\"\\x04user\\x18\\x01 \\x01(\\v2\\x12.memos.api.v1.UserB\\x06\\xe0A\\x02\\xe0A\\x04R\\x04user\\x12\\x1c\\n\" +\n\t\"\\auser_id\\x18\\x02 \\x01(\\tB\\x03\\xe0A\\x01R\\x06userId\\x12(\\n\" +\n\t\"\\rvalidate_only\\x18\\x03 \\x01(\\bB\\x03\\xe0A\\x01R\\fvalidateOnly\\x12\\\"\\n\" +\n\t\"\\n\" +\n\t\"request_id\\x18\\x04 \\x01(\\tB\\x03\\xe0A\\x01R\\trequestId\\\"\\xac\\x01\\n\" +\n\t\"\\x11UpdateUserRequest\\x12+\\n\" +\n\t\"\\x04user\\x18\\x01 \\x01(\\v2\\x12.memos.api.v1.UserB\\x03\\xe0A\\x02R\\x04user\\x12@\\n\" +\n\t\"\\vupdate_mask\\x18\\x02 \\x01(\\v2\\x1a.google.protobuf.FieldMaskB\\x03\\xe0A\\x02R\\n\" +\n\t\"updateMask\\x12(\\n\" +\n\t\"\\rallow_missing\\x18\\x03 \\x01(\\bB\\x03\\xe0A\\x01R\\fallowMissing\\\"]\\n\" +\n\t\"\\x11DeleteUserRequest\\x12-\\n\" +\n\t\"\\x04name\\x18\\x01 \\x01(\\tB\\x19\\xe0A\\x02\\xfaA\\x13\\n\" +\n\t\"\\x11memos.api.v1/UserR\\x04name\\x12\\x19\\n\" +\n\t\"\\x05force\\x18\\x02 \\x01(\\bB\\x03\\xe0A\\x01R\\x05force\\\"\\xe4\\x04\\n\" +\n\t\"\\tUserStats\\x12\\x17\\n\" +\n\t\"\\x04name\\x18\\x01 \\x01(\\tB\\x03\\xe0A\\bR\\x04name\\x12R\\n\" +\n\t\"\\x17memo_display_timestamps\\x18\\x02 \\x03(\\v2\\x1a.google.protobuf.TimestampR\\x15memoDisplayTimestamps\\x12M\\n\" +\n\t\"\\x0fmemo_type_stats\\x18\\x03 \\x01(\\v2%.memos.api.v1.UserStats.MemoTypeStatsR\\rmemoTypeStats\\x12B\\n\" +\n\t\"\\ttag_count\\x18\\x04 \\x03(\\v2%.memos.api.v1.UserStats.TagCountEntryR\\btagCount\\x12!\\n\" +\n\t\"\\fpinned_memos\\x18\\x05 \\x03(\\tR\\vpinnedMemos\\x12(\\n\" +\n\t\"\\x10total_memo_count\\x18\\x06 \\x01(\\x05R\\x0etotalMemoCount\\x1a;\\n\" +\n\t\"\\rTagCountEntry\\x12\\x10\\n\" +\n\t\"\\x03key\\x18\\x01 \\x01(\\tR\\x03key\\x12\\x14\\n\" +\n\t\"\\x05value\\x18\\x02 \\x01(\\x05R\\x05value:\\x028\\x01\\x1a\\x8b\\x01\\n\" +\n\t\"\\rMemoTypeStats\\x12\\x1d\\n\" +\n\t\"\\n\" +\n\t\"link_count\\x18\\x01 \\x01(\\x05R\\tlinkCount\\x12\\x1d\\n\" +\n\t\"\\n\" +\n\t\"code_count\\x18\\x02 \\x01(\\x05R\\tcodeCount\\x12\\x1d\\n\" +\n\t\"\\n\" +\n\t\"todo_count\\x18\\x03 \\x01(\\x05R\\ttodoCount\\x12\\x1d\\n\" +\n\t\"\\n\" +\n\t\"undo_count\\x18\\x04 \\x01(\\x05R\\tundoCount:?\\xeaA<\\n\" +\n\t\"\\x16memos.api.v1/UserStats\\x12\\fusers/{user}*\\tuserStats2\\tuserStats\\\"D\\n\" +\n\t\"\\x13GetUserStatsRequest\\x12-\\n\" +\n\t\"\\x04name\\x18\\x01 \\x01(\\tB\\x19\\xe0A\\x02\\xfaA\\x13\\n\" +\n\t\"\\x11memos.api.v1/UserR\\x04name\\\"\\x19\\n\" +\n\t\"\\x17ListAllUserStatsRequest\\\"I\\n\" +\n\t\"\\x18ListAllUserStatsResponse\\x12-\\n\" +\n\t\"\\x05stats\\x18\\x01 \\x03(\\v2\\x17.memos.api.v1.UserStatsR\\x05stats\\\"\\xb0\\x04\\n\" +\n\t\"\\vUserSetting\\x12\\x17\\n\" +\n\t\"\\x04name\\x18\\x01 \\x01(\\tB\\x03\\xe0A\\bR\\x04name\\x12S\\n\" +\n\t\"\\x0fgeneral_setting\\x18\\x02 \\x01(\\v2(.memos.api.v1.UserSetting.GeneralSettingH\\x00R\\x0egeneralSetting\\x12V\\n\" +\n\t\"\\x10webhooks_setting\\x18\\x05 \\x01(\\v2).memos.api.v1.UserSetting.WebhooksSettingH\\x00R\\x0fwebhooksSetting\\x1av\\n\" +\n\t\"\\x0eGeneralSetting\\x12\\x1b\\n\" +\n\t\"\\x06locale\\x18\\x01 \\x01(\\tB\\x03\\xe0A\\x01R\\x06locale\\x12,\\n\" +\n\t\"\\x0fmemo_visibility\\x18\\x03 \\x01(\\tB\\x03\\xe0A\\x01R\\x0ememoVisibility\\x12\\x19\\n\" +\n\t\"\\x05theme\\x18\\x04 \\x01(\\tB\\x03\\xe0A\\x01R\\x05theme\\x1aH\\n\" +\n\t\"\\x0fWebhooksSetting\\x125\\n\" +\n\t\"\\bwebhooks\\x18\\x01 \\x03(\\v2\\x19.memos.api.v1.UserWebhookR\\bwebhooks\\\"5\\n\" +\n\t\"\\x03Key\\x12\\x13\\n\" +\n\t\"\\x0fKEY_UNSPECIFIED\\x10\\x00\\x12\\v\\n\" +\n\t\"\\aGENERAL\\x10\\x01\\x12\\f\\n\" +\n\t\"\\bWEBHOOKS\\x10\\x04:Y\\xeaAV\\n\" +\n\t\"\\x18memos.api.v1/UserSetting\\x12\\x1fusers/{user}/settings/{setting}*\\fuserSettings2\\vuserSettingB\\a\\n\" +\n\t\"\\x05value\\\"M\\n\" +\n\t\"\\x15GetUserSettingRequest\\x124\\n\" +\n\t\"\\x04name\\x18\\x01 \\x01(\\tB \\xe0A\\x02\\xfaA\\x1a\\n\" +\n\t\"\\x18memos.api.v1/UserSettingR\\x04name\\\"\\x96\\x01\\n\" +\n\t\"\\x18UpdateUserSettingRequest\\x128\\n\" +\n\t\"\\asetting\\x18\\x01 \\x01(\\v2\\x19.memos.api.v1.UserSettingB\\x03\\xe0A\\x02R\\asetting\\x12@\\n\" +\n\t\"\\vupdate_mask\\x18\\x02 \\x01(\\v2\\x1a.google.protobuf.FieldMaskB\\x03\\xe0A\\x02R\\n\" +\n\t\"updateMask\\\"\\x92\\x01\\n\" +\n\t\"\\x17ListUserSettingsRequest\\x121\\n\" +\n\t\"\\x06parent\\x18\\x01 \\x01(\\tB\\x19\\xe0A\\x02\\xfaA\\x13\\n\" +\n\t\"\\x11memos.api.v1/UserR\\x06parent\\x12 \\n\" +\n\t\"\\tpage_size\\x18\\x02 \\x01(\\x05B\\x03\\xe0A\\x01R\\bpageSize\\x12\\\"\\n\" +\n\t\"\\n\" +\n\t\"page_token\\x18\\x03 \\x01(\\tB\\x03\\xe0A\\x01R\\tpageToken\\\"\\x98\\x01\\n\" +\n\t\"\\x18ListUserSettingsResponse\\x125\\n\" +\n\t\"\\bsettings\\x18\\x01 \\x03(\\v2\\x19.memos.api.v1.UserSettingR\\bsettings\\x12&\\n\" +\n\t\"\\x0fnext_page_token\\x18\\x02 \\x01(\\tR\\rnextPageToken\\x12\\x1d\\n\" +\n\t\"\\n\" +\n\t\"total_size\\x18\\x03 \\x01(\\x05R\\ttotalSize\\\"\\xa7\\x03\\n\" +\n\t\"\\x13PersonalAccessToken\\x12\\x17\\n\" +\n\t\"\\x04name\\x18\\x01 \\x01(\\tB\\x03\\xe0A\\bR\\x04name\\x12%\\n\" +\n\t\"\\vdescription\\x18\\x02 \\x01(\\tB\\x03\\xe0A\\x01R\\vdescription\\x12>\\n\" +\n\t\"\\n\" +\n\t\"created_at\\x18\\x03 \\x01(\\v2\\x1a.google.protobuf.TimestampB\\x03\\xe0A\\x03R\\tcreatedAt\\x12>\\n\" +\n\t\"\\n\" +\n\t\"expires_at\\x18\\x04 \\x01(\\v2\\x1a.google.protobuf.TimestampB\\x03\\xe0A\\x01R\\texpiresAt\\x12A\\n\" +\n\t\"\\flast_used_at\\x18\\x05 \\x01(\\v2\\x1a.google.protobuf.TimestampB\\x03\\xe0A\\x03R\\n\" +\n\t\"lastUsedAt:\\x8c\\x01\\xeaA\\x88\\x01\\n\" +\n\t\" memos.api.v1/PersonalAccessToken\\x129users/{user}/personalAccessTokens/{personal_access_token}*\\x14personalAccessTokens2\\x13personalAccessToken\\\"\\x9a\\x01\\n\" +\n\t\"\\x1fListPersonalAccessTokensRequest\\x121\\n\" +\n\t\"\\x06parent\\x18\\x01 \\x01(\\tB\\x19\\xe0A\\x02\\xfaA\\x13\\n\" +\n\t\"\\x11memos.api.v1/UserR\\x06parent\\x12 \\n\" +\n\t\"\\tpage_size\\x18\\x02 \\x01(\\x05B\\x03\\xe0A\\x01R\\bpageSize\\x12\\\"\\n\" +\n\t\"\\n\" +\n\t\"page_token\\x18\\x03 \\x01(\\tB\\x03\\xe0A\\x01R\\tpageToken\\\"\\xc2\\x01\\n\" +\n\t\" ListPersonalAccessTokensResponse\\x12W\\n\" +\n\t\"\\x16personal_access_tokens\\x18\\x01 \\x03(\\v2!.memos.api.v1.PersonalAccessTokenR\\x14personalAccessTokens\\x12&\\n\" +\n\t\"\\x0fnext_page_token\\x18\\x02 \\x01(\\tR\\rnextPageToken\\x12\\x1d\\n\" +\n\t\"\\n\" +\n\t\"total_size\\x18\\x03 \\x01(\\x05R\\ttotalSize\\\"\\xa9\\x01\\n\" +\n\t\" CreatePersonalAccessTokenRequest\\x121\\n\" +\n\t\"\\x06parent\\x18\\x01 \\x01(\\tB\\x19\\xe0A\\x02\\xfaA\\x13\\n\" +\n\t\"\\x11memos.api.v1/UserR\\x06parent\\x12%\\n\" +\n\t\"\\vdescription\\x18\\x02 \\x01(\\tB\\x03\\xe0A\\x01R\\vdescription\\x12+\\n\" +\n\t\"\\x0fexpires_in_days\\x18\\x03 \\x01(\\x05B\\x03\\xe0A\\x01R\\rexpiresInDays\\\"\\x90\\x01\\n\" +\n\t\"!CreatePersonalAccessTokenResponse\\x12U\\n\" +\n\t\"\\x15personal_access_token\\x18\\x01 \\x01(\\v2!.memos.api.v1.PersonalAccessTokenR\\x13personalAccessToken\\x12\\x14\\n\" +\n\t\"\\x05token\\x18\\x02 \\x01(\\tR\\x05token\\\"`\\n\" +\n\t\" DeletePersonalAccessTokenRequest\\x12<\\n\" +\n\t\"\\x04name\\x18\\x01 \\x01(\\tB(\\xe0A\\x02\\xfaA\\\"\\n\" +\n\t\" memos.api.v1/PersonalAccessTokenR\\x04name\\\"\\xda\\x01\\n\" +\n\t\"\\vUserWebhook\\x12\\x12\\n\" +\n\t\"\\x04name\\x18\\x01 \\x01(\\tR\\x04name\\x12\\x10\\n\" +\n\t\"\\x03url\\x18\\x02 \\x01(\\tR\\x03url\\x12!\\n\" +\n\t\"\\fdisplay_name\\x18\\x03 \\x01(\\tR\\vdisplayName\\x12@\\n\" +\n\t\"\\vcreate_time\\x18\\x04 \\x01(\\v2\\x1a.google.protobuf.TimestampB\\x03\\xe0A\\x03R\\n\" +\n\t\"createTime\\x12@\\n\" +\n\t\"\\vupdate_time\\x18\\x05 \\x01(\\v2\\x1a.google.protobuf.TimestampB\\x03\\xe0A\\x03R\\n\" +\n\t\"updateTime\\\"6\\n\" +\n\t\"\\x17ListUserWebhooksRequest\\x12\\x1b\\n\" +\n\t\"\\x06parent\\x18\\x01 \\x01(\\tB\\x03\\xe0A\\x02R\\x06parent\\\"Q\\n\" +\n\t\"\\x18ListUserWebhooksResponse\\x125\\n\" +\n\t\"\\bwebhooks\\x18\\x01 \\x03(\\v2\\x19.memos.api.v1.UserWebhookR\\bwebhooks\\\"q\\n\" +\n\t\"\\x18CreateUserWebhookRequest\\x12\\x1b\\n\" +\n\t\"\\x06parent\\x18\\x01 \\x01(\\tB\\x03\\xe0A\\x02R\\x06parent\\x128\\n\" +\n\t\"\\awebhook\\x18\\x02 \\x01(\\v2\\x19.memos.api.v1.UserWebhookB\\x03\\xe0A\\x02R\\awebhook\\\"\\x91\\x01\\n\" +\n\t\"\\x18UpdateUserWebhookRequest\\x128\\n\" +\n\t\"\\awebhook\\x18\\x01 \\x01(\\v2\\x19.memos.api.v1.UserWebhookB\\x03\\xe0A\\x02R\\awebhook\\x12;\\n\" +\n\t\"\\vupdate_mask\\x18\\x02 \\x01(\\v2\\x1a.google.protobuf.FieldMaskR\\n\" +\n\t\"updateMask\\\"3\\n\" +\n\t\"\\x18DeleteUserWebhookRequest\\x12\\x17\\n\" +\n\t\"\\x04name\\x18\\x01 \\x01(\\tB\\x03\\xe0A\\x02R\\x04name\\\"\\xb8\\x05\\n\" +\n\t\"\\x10UserNotification\\x12\\x1a\\n\" +\n\t\"\\x04name\\x18\\x01 \\x01(\\tB\\x06\\xe0A\\x03\\xe0A\\bR\\x04name\\x121\\n\" +\n\t\"\\x06sender\\x18\\x02 \\x01(\\tB\\x19\\xe0A\\x03\\xfaA\\x13\\n\" +\n\t\"\\x11memos.api.v1/UserR\\x06sender\\x12B\\n\" +\n\t\"\\x06status\\x18\\x03 \\x01(\\x0e2%.memos.api.v1.UserNotification.StatusB\\x03\\xe0A\\x01R\\x06status\\x12@\\n\" +\n\t\"\\vcreate_time\\x18\\x04 \\x01(\\v2\\x1a.google.protobuf.TimestampB\\x03\\xe0A\\x03R\\n\" +\n\t\"createTime\\x12<\\n\" +\n\t\"\\x04type\\x18\\x05 \\x01(\\x0e2#.memos.api.v1.UserNotification.TypeB\\x03\\xe0A\\x03R\\x04type\\x12[\\n\" +\n\t\"\\fmemo_comment\\x18\\x06 \\x01(\\v21.memos.api.v1.UserNotification.MemoCommentPayloadB\\x03\\xe0A\\x03H\\x00R\\vmemoComment\\x1aK\\n\" +\n\t\"\\x12MemoCommentPayload\\x12\\x12\\n\" +\n\t\"\\x04memo\\x18\\x01 \\x01(\\tR\\x04memo\\x12!\\n\" +\n\t\"\\frelated_memo\\x18\\x02 \\x01(\\tR\\vrelatedMemo\\\":\\n\" +\n\t\"\\x06Status\\x12\\x16\\n\" +\n\t\"\\x12STATUS_UNSPECIFIED\\x10\\x00\\x12\\n\" +\n\t\"\\n\" +\n\t\"\\x06UNREAD\\x10\\x01\\x12\\f\\n\" +\n\t\"\\bARCHIVED\\x10\\x02\\\".\\n\" +\n\t\"\\x04Type\\x12\\x14\\n\" +\n\t\"\\x10TYPE_UNSPECIFIED\\x10\\x00\\x12\\x10\\n\" +\n\t\"\\fMEMO_COMMENT\\x10\\x01:p\\xeaAm\\n\" +\n\t\"\\x1dmemos.api.v1/UserNotification\\x12)users/{user}/notifications/{notification}\\x1a\\x04name*\\rnotifications2\\fnotificationB\\t\\n\" +\n\t\"\\apayload\\\"\\xb4\\x01\\n\" +\n\t\"\\x1cListUserNotificationsRequest\\x121\\n\" +\n\t\"\\x06parent\\x18\\x01 \\x01(\\tB\\x19\\xe0A\\x02\\xfaA\\x13\\n\" +\n\t\"\\x11memos.api.v1/UserR\\x06parent\\x12 \\n\" +\n\t\"\\tpage_size\\x18\\x02 \\x01(\\x05B\\x03\\xe0A\\x01R\\bpageSize\\x12\\\"\\n\" +\n\t\"\\n\" +\n\t\"page_token\\x18\\x03 \\x01(\\tB\\x03\\xe0A\\x01R\\tpageToken\\x12\\x1b\\n\" +\n\t\"\\x06filter\\x18\\x04 \\x01(\\tB\\x03\\xe0A\\x01R\\x06filter\\\"\\x8d\\x01\\n\" +\n\t\"\\x1dListUserNotificationsResponse\\x12D\\n\" +\n\t\"\\rnotifications\\x18\\x01 \\x03(\\v2\\x1e.memos.api.v1.UserNotificationR\\rnotifications\\x12&\\n\" +\n\t\"\\x0fnext_page_token\\x18\\x02 \\x01(\\tR\\rnextPageToken\\\"\\xaa\\x01\\n\" +\n\t\"\\x1dUpdateUserNotificationRequest\\x12G\\n\" +\n\t\"\\fnotification\\x18\\x01 \\x01(\\v2\\x1e.memos.api.v1.UserNotificationB\\x03\\xe0A\\x02R\\fnotification\\x12@\\n\" +\n\t\"\\vupdate_mask\\x18\\x02 \\x01(\\v2\\x1a.google.protobuf.FieldMaskB\\x03\\xe0A\\x02R\\n\" +\n\t\"updateMask\\\"Z\\n\" +\n\t\"\\x1dDeleteUserNotificationRequest\\x129\\n\" +\n\t\"\\x04name\\x18\\x01 \\x01(\\tB%\\xe0A\\x02\\xfaA\\x1f\\n\" +\n\t\"\\x1dmemos.api.v1/UserNotificationR\\x04name2\\x83\\x17\\n\" +\n\t\"\\vUserService\\x12c\\n\" +\n\t\"\\tListUsers\\x12\\x1e.memos.api.v1.ListUsersRequest\\x1a\\x1f.memos.api.v1.ListUsersResponse\\\"\\x15\\x82\\xd3\\xe4\\x93\\x02\\x0f\\x12\\r/api/v1/users\\x12b\\n\" +\n\t\"\\aGetUser\\x12\\x1c.memos.api.v1.GetUserRequest\\x1a\\x12.memos.api.v1.User\\\"%\\xdaA\\x04name\\x82\\xd3\\xe4\\x93\\x02\\x18\\x12\\x16/api/v1/{name=users/*}\\x12e\\n\" +\n\t\"\\n\" +\n\t\"CreateUser\\x12\\x1f.memos.api.v1.CreateUserRequest\\x1a\\x12.memos.api.v1.User\\\"\\\"\\xdaA\\x04user\\x82\\xd3\\xe4\\x93\\x02\\x15:\\x04user\\\"\\r/api/v1/users\\x12\\x7f\\n\" +\n\t\"\\n\" +\n\t\"UpdateUser\\x12\\x1f.memos.api.v1.UpdateUserRequest\\x1a\\x12.memos.api.v1.User\\\"<\\xdaA\\x10user,update_mask\\x82\\xd3\\xe4\\x93\\x02#:\\x04user2\\x1b/api/v1/{user.name=users/*}\\x12l\\n\" +\n\t\"\\n\" +\n\t\"DeleteUser\\x12\\x1f.memos.api.v1.DeleteUserRequest\\x1a\\x16.google.protobuf.Empty\\\"%\\xdaA\\x04name\\x82\\xd3\\xe4\\x93\\x02\\x18*\\x16/api/v1/{name=users/*}\\x12~\\n\" +\n\t\"\\x10ListAllUserStats\\x12%.memos.api.v1.ListAllUserStatsRequest\\x1a&.memos.api.v1.ListAllUserStatsResponse\\\"\\x1b\\x82\\xd3\\xe4\\x93\\x02\\x15\\x12\\x13/api/v1/users:stats\\x12z\\n\" +\n\t\"\\fGetUserStats\\x12!.memos.api.v1.GetUserStatsRequest\\x1a\\x17.memos.api.v1.UserStats\\\".\\xdaA\\x04name\\x82\\xd3\\xe4\\x93\\x02!\\x12\\x1f/api/v1/{name=users/*}:getStats\\x12\\x82\\x01\\n\" +\n\t\"\\x0eGetUserSetting\\x12#.memos.api.v1.GetUserSettingRequest\\x1a\\x19.memos.api.v1.UserSetting\\\"0\\xdaA\\x04name\\x82\\xd3\\xe4\\x93\\x02#\\x12!/api/v1/{name=users/*/settings/*}\\x12\\xa8\\x01\\n\" +\n\t\"\\x11UpdateUserSetting\\x12&.memos.api.v1.UpdateUserSettingRequest\\x1a\\x19.memos.api.v1.UserSetting\\\"P\\xdaA\\x13setting,update_mask\\x82\\xd3\\xe4\\x93\\x024:\\asetting2)/api/v1/{setting.name=users/*/settings/*}\\x12\\x95\\x01\\n\" +\n\t\"\\x10ListUserSettings\\x12%.memos.api.v1.ListUserSettingsRequest\\x1a&.memos.api.v1.ListUserSettingsResponse\\\"2\\xdaA\\x06parent\\x82\\xd3\\xe4\\x93\\x02#\\x12!/api/v1/{parent=users/*}/settings\\x12\\xb9\\x01\\n\" +\n\t\"\\x18ListPersonalAccessTokens\\x12-.memos.api.v1.ListPersonalAccessTokensRequest\\x1a..memos.api.v1.ListPersonalAccessTokensResponse\\\">\\xdaA\\x06parent\\x82\\xd3\\xe4\\x93\\x02/\\x12-/api/v1/{parent=users/*}/personalAccessTokens\\x12\\xb6\\x01\\n\" +\n\t\"\\x19CreatePersonalAccessToken\\x12..memos.api.v1.CreatePersonalAccessTokenRequest\\x1a/.memos.api.v1.CreatePersonalAccessTokenResponse\\\"8\\x82\\xd3\\xe4\\x93\\x022:\\x01*\\\"-/api/v1/{parent=users/*}/personalAccessTokens\\x12\\xa1\\x01\\n\" +\n\t\"\\x19DeletePersonalAccessToken\\x12..memos.api.v1.DeletePersonalAccessTokenRequest\\x1a\\x16.google.protobuf.Empty\\\"<\\xdaA\\x04name\\x82\\xd3\\xe4\\x93\\x02/*-/api/v1/{name=users/*/personalAccessTokens/*}\\x12\\x95\\x01\\n\" +\n\t\"\\x10ListUserWebhooks\\x12%.memos.api.v1.ListUserWebhooksRequest\\x1a&.memos.api.v1.ListUserWebhooksResponse\\\"2\\xdaA\\x06parent\\x82\\xd3\\xe4\\x93\\x02#\\x12!/api/v1/{parent=users/*}/webhooks\\x12\\x9b\\x01\\n\" +\n\t\"\\x11CreateUserWebhook\\x12&.memos.api.v1.CreateUserWebhookRequest\\x1a\\x19.memos.api.v1.UserWebhook\\\"C\\xdaA\\x0eparent,webhook\\x82\\xd3\\xe4\\x93\\x02,:\\awebhook\\\"!/api/v1/{parent=users/*}/webhooks\\x12\\xa8\\x01\\n\" +\n\t\"\\x11UpdateUserWebhook\\x12&.memos.api.v1.UpdateUserWebhookRequest\\x1a\\x19.memos.api.v1.UserWebhook\\\"P\\xdaA\\x13webhook,update_mask\\x82\\xd3\\xe4\\x93\\x024:\\awebhook2)/api/v1/{webhook.name=users/*/webhooks/*}\\x12\\x85\\x01\\n\" +\n\t\"\\x11DeleteUserWebhook\\x12&.memos.api.v1.DeleteUserWebhookRequest\\x1a\\x16.google.protobuf.Empty\\\"0\\xdaA\\x04name\\x82\\xd3\\xe4\\x93\\x02#*!/api/v1/{name=users/*/webhooks/*}\\x12\\xa9\\x01\\n\" +\n\t\"\\x15ListUserNotifications\\x12*.memos.api.v1.ListUserNotificationsRequest\\x1a+.memos.api.v1.ListUserNotificationsResponse\\\"7\\xdaA\\x06parent\\x82\\xd3\\xe4\\x93\\x02(\\x12&/api/v1/{parent=users/*}/notifications\\x12\\xcb\\x01\\n\" +\n\t\"\\x16UpdateUserNotification\\x12+.memos.api.v1.UpdateUserNotificationRequest\\x1a\\x1e.memos.api.v1.UserNotification\\\"d\\xdaA\\x18notification,update_mask\\x82\\xd3\\xe4\\x93\\x02C:\\fnotification23/api/v1/{notification.name=users/*/notifications/*}\\x12\\x94\\x01\\n\" +\n\t\"\\x16DeleteUserNotification\\x12+.memos.api.v1.DeleteUserNotificationRequest\\x1a\\x16.google.protobuf.Empty\\\"5\\xdaA\\x04name\\x82\\xd3\\xe4\\x93\\x02(*&/api/v1/{name=users/*/notifications/*}B\\xa8\\x01\\n\" +\n\t\"\\x10com.memos.api.v1B\\x10UserServiceProtoP\\x01Z0github.com/usememos/memos/proto/gen/api/v1;apiv1\\xa2\\x02\\x03MAX\\xaa\\x02\\fMemos.Api.V1\\xca\\x02\\fMemos\\\\Api\\\\V1\\xe2\\x02\\x18Memos\\\\Api\\\\V1\\\\GPBMetadata\\xea\\x02\\x0eMemos::Api::V1b\\x06proto3\"\n\nvar (\n\tfile_api_v1_user_service_proto_rawDescOnce sync.Once\n\tfile_api_v1_user_service_proto_rawDescData []byte\n)\n\nfunc file_api_v1_user_service_proto_rawDescGZIP() []byte {\n\tfile_api_v1_user_service_proto_rawDescOnce.Do(func() {\n\t\tfile_api_v1_user_service_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_api_v1_user_service_proto_rawDesc), len(file_api_v1_user_service_proto_rawDesc)))\n\t})\n\treturn file_api_v1_user_service_proto_rawDescData\n}\n\nvar file_api_v1_user_service_proto_enumTypes = make([]protoimpl.EnumInfo, 4)\nvar file_api_v1_user_service_proto_msgTypes = make([]protoimpl.MessageInfo, 38)\nvar file_api_v1_user_service_proto_goTypes = []any{\n\t(User_Role)(0),                              // 0: memos.api.v1.User.Role\n\t(UserSetting_Key)(0),                        // 1: memos.api.v1.UserSetting.Key\n\t(UserNotification_Status)(0),                // 2: memos.api.v1.UserNotification.Status\n\t(UserNotification_Type)(0),                  // 3: memos.api.v1.UserNotification.Type\n\t(*User)(nil),                                // 4: memos.api.v1.User\n\t(*ListUsersRequest)(nil),                    // 5: memos.api.v1.ListUsersRequest\n\t(*ListUsersResponse)(nil),                   // 6: memos.api.v1.ListUsersResponse\n\t(*GetUserRequest)(nil),                      // 7: memos.api.v1.GetUserRequest\n\t(*CreateUserRequest)(nil),                   // 8: memos.api.v1.CreateUserRequest\n\t(*UpdateUserRequest)(nil),                   // 9: memos.api.v1.UpdateUserRequest\n\t(*DeleteUserRequest)(nil),                   // 10: memos.api.v1.DeleteUserRequest\n\t(*UserStats)(nil),                           // 11: memos.api.v1.UserStats\n\t(*GetUserStatsRequest)(nil),                 // 12: memos.api.v1.GetUserStatsRequest\n\t(*ListAllUserStatsRequest)(nil),             // 13: memos.api.v1.ListAllUserStatsRequest\n\t(*ListAllUserStatsResponse)(nil),            // 14: memos.api.v1.ListAllUserStatsResponse\n\t(*UserSetting)(nil),                         // 15: memos.api.v1.UserSetting\n\t(*GetUserSettingRequest)(nil),               // 16: memos.api.v1.GetUserSettingRequest\n\t(*UpdateUserSettingRequest)(nil),            // 17: memos.api.v1.UpdateUserSettingRequest\n\t(*ListUserSettingsRequest)(nil),             // 18: memos.api.v1.ListUserSettingsRequest\n\t(*ListUserSettingsResponse)(nil),            // 19: memos.api.v1.ListUserSettingsResponse\n\t(*PersonalAccessToken)(nil),                 // 20: memos.api.v1.PersonalAccessToken\n\t(*ListPersonalAccessTokensRequest)(nil),     // 21: memos.api.v1.ListPersonalAccessTokensRequest\n\t(*ListPersonalAccessTokensResponse)(nil),    // 22: memos.api.v1.ListPersonalAccessTokensResponse\n\t(*CreatePersonalAccessTokenRequest)(nil),    // 23: memos.api.v1.CreatePersonalAccessTokenRequest\n\t(*CreatePersonalAccessTokenResponse)(nil),   // 24: memos.api.v1.CreatePersonalAccessTokenResponse\n\t(*DeletePersonalAccessTokenRequest)(nil),    // 25: memos.api.v1.DeletePersonalAccessTokenRequest\n\t(*UserWebhook)(nil),                         // 26: memos.api.v1.UserWebhook\n\t(*ListUserWebhooksRequest)(nil),             // 27: memos.api.v1.ListUserWebhooksRequest\n\t(*ListUserWebhooksResponse)(nil),            // 28: memos.api.v1.ListUserWebhooksResponse\n\t(*CreateUserWebhookRequest)(nil),            // 29: memos.api.v1.CreateUserWebhookRequest\n\t(*UpdateUserWebhookRequest)(nil),            // 30: memos.api.v1.UpdateUserWebhookRequest\n\t(*DeleteUserWebhookRequest)(nil),            // 31: memos.api.v1.DeleteUserWebhookRequest\n\t(*UserNotification)(nil),                    // 32: memos.api.v1.UserNotification\n\t(*ListUserNotificationsRequest)(nil),        // 33: memos.api.v1.ListUserNotificationsRequest\n\t(*ListUserNotificationsResponse)(nil),       // 34: memos.api.v1.ListUserNotificationsResponse\n\t(*UpdateUserNotificationRequest)(nil),       // 35: memos.api.v1.UpdateUserNotificationRequest\n\t(*DeleteUserNotificationRequest)(nil),       // 36: memos.api.v1.DeleteUserNotificationRequest\n\tnil,                                         // 37: memos.api.v1.UserStats.TagCountEntry\n\t(*UserStats_MemoTypeStats)(nil),             // 38: memos.api.v1.UserStats.MemoTypeStats\n\t(*UserSetting_GeneralSetting)(nil),          // 39: memos.api.v1.UserSetting.GeneralSetting\n\t(*UserSetting_WebhooksSetting)(nil),         // 40: memos.api.v1.UserSetting.WebhooksSetting\n\t(*UserNotification_MemoCommentPayload)(nil), // 41: memos.api.v1.UserNotification.MemoCommentPayload\n\t(State)(0),                    // 42: memos.api.v1.State\n\t(*timestamppb.Timestamp)(nil), // 43: google.protobuf.Timestamp\n\t(*fieldmaskpb.FieldMask)(nil), // 44: google.protobuf.FieldMask\n\t(*emptypb.Empty)(nil),         // 45: google.protobuf.Empty\n}\nvar file_api_v1_user_service_proto_depIdxs = []int32{\n\t0,  // 0: memos.api.v1.User.role:type_name -> memos.api.v1.User.Role\n\t42, // 1: memos.api.v1.User.state:type_name -> memos.api.v1.State\n\t43, // 2: memos.api.v1.User.create_time:type_name -> google.protobuf.Timestamp\n\t43, // 3: memos.api.v1.User.update_time:type_name -> google.protobuf.Timestamp\n\t4,  // 4: memos.api.v1.ListUsersResponse.users:type_name -> memos.api.v1.User\n\t44, // 5: memos.api.v1.GetUserRequest.read_mask:type_name -> google.protobuf.FieldMask\n\t4,  // 6: memos.api.v1.CreateUserRequest.user:type_name -> memos.api.v1.User\n\t4,  // 7: memos.api.v1.UpdateUserRequest.user:type_name -> memos.api.v1.User\n\t44, // 8: memos.api.v1.UpdateUserRequest.update_mask:type_name -> google.protobuf.FieldMask\n\t43, // 9: memos.api.v1.UserStats.memo_display_timestamps:type_name -> google.protobuf.Timestamp\n\t38, // 10: memos.api.v1.UserStats.memo_type_stats:type_name -> memos.api.v1.UserStats.MemoTypeStats\n\t37, // 11: memos.api.v1.UserStats.tag_count:type_name -> memos.api.v1.UserStats.TagCountEntry\n\t11, // 12: memos.api.v1.ListAllUserStatsResponse.stats:type_name -> memos.api.v1.UserStats\n\t39, // 13: memos.api.v1.UserSetting.general_setting:type_name -> memos.api.v1.UserSetting.GeneralSetting\n\t40, // 14: memos.api.v1.UserSetting.webhooks_setting:type_name -> memos.api.v1.UserSetting.WebhooksSetting\n\t15, // 15: memos.api.v1.UpdateUserSettingRequest.setting:type_name -> memos.api.v1.UserSetting\n\t44, // 16: memos.api.v1.UpdateUserSettingRequest.update_mask:type_name -> google.protobuf.FieldMask\n\t15, // 17: memos.api.v1.ListUserSettingsResponse.settings:type_name -> memos.api.v1.UserSetting\n\t43, // 18: memos.api.v1.PersonalAccessToken.created_at:type_name -> google.protobuf.Timestamp\n\t43, // 19: memos.api.v1.PersonalAccessToken.expires_at:type_name -> google.protobuf.Timestamp\n\t43, // 20: memos.api.v1.PersonalAccessToken.last_used_at:type_name -> google.protobuf.Timestamp\n\t20, // 21: memos.api.v1.ListPersonalAccessTokensResponse.personal_access_tokens:type_name -> memos.api.v1.PersonalAccessToken\n\t20, // 22: memos.api.v1.CreatePersonalAccessTokenResponse.personal_access_token:type_name -> memos.api.v1.PersonalAccessToken\n\t43, // 23: memos.api.v1.UserWebhook.create_time:type_name -> google.protobuf.Timestamp\n\t43, // 24: memos.api.v1.UserWebhook.update_time:type_name -> google.protobuf.Timestamp\n\t26, // 25: memos.api.v1.ListUserWebhooksResponse.webhooks:type_name -> memos.api.v1.UserWebhook\n\t26, // 26: memos.api.v1.CreateUserWebhookRequest.webhook:type_name -> memos.api.v1.UserWebhook\n\t26, // 27: memos.api.v1.UpdateUserWebhookRequest.webhook:type_name -> memos.api.v1.UserWebhook\n\t44, // 28: memos.api.v1.UpdateUserWebhookRequest.update_mask:type_name -> google.protobuf.FieldMask\n\t2,  // 29: memos.api.v1.UserNotification.status:type_name -> memos.api.v1.UserNotification.Status\n\t43, // 30: memos.api.v1.UserNotification.create_time:type_name -> google.protobuf.Timestamp\n\t3,  // 31: memos.api.v1.UserNotification.type:type_name -> memos.api.v1.UserNotification.Type\n\t41, // 32: memos.api.v1.UserNotification.memo_comment:type_name -> memos.api.v1.UserNotification.MemoCommentPayload\n\t32, // 33: memos.api.v1.ListUserNotificationsResponse.notifications:type_name -> memos.api.v1.UserNotification\n\t32, // 34: memos.api.v1.UpdateUserNotificationRequest.notification:type_name -> memos.api.v1.UserNotification\n\t44, // 35: memos.api.v1.UpdateUserNotificationRequest.update_mask:type_name -> google.protobuf.FieldMask\n\t26, // 36: memos.api.v1.UserSetting.WebhooksSetting.webhooks:type_name -> memos.api.v1.UserWebhook\n\t5,  // 37: memos.api.v1.UserService.ListUsers:input_type -> memos.api.v1.ListUsersRequest\n\t7,  // 38: memos.api.v1.UserService.GetUser:input_type -> memos.api.v1.GetUserRequest\n\t8,  // 39: memos.api.v1.UserService.CreateUser:input_type -> memos.api.v1.CreateUserRequest\n\t9,  // 40: memos.api.v1.UserService.UpdateUser:input_type -> memos.api.v1.UpdateUserRequest\n\t10, // 41: memos.api.v1.UserService.DeleteUser:input_type -> memos.api.v1.DeleteUserRequest\n\t13, // 42: memos.api.v1.UserService.ListAllUserStats:input_type -> memos.api.v1.ListAllUserStatsRequest\n\t12, // 43: memos.api.v1.UserService.GetUserStats:input_type -> memos.api.v1.GetUserStatsRequest\n\t16, // 44: memos.api.v1.UserService.GetUserSetting:input_type -> memos.api.v1.GetUserSettingRequest\n\t17, // 45: memos.api.v1.UserService.UpdateUserSetting:input_type -> memos.api.v1.UpdateUserSettingRequest\n\t18, // 46: memos.api.v1.UserService.ListUserSettings:input_type -> memos.api.v1.ListUserSettingsRequest\n\t21, // 47: memos.api.v1.UserService.ListPersonalAccessTokens:input_type -> memos.api.v1.ListPersonalAccessTokensRequest\n\t23, // 48: memos.api.v1.UserService.CreatePersonalAccessToken:input_type -> memos.api.v1.CreatePersonalAccessTokenRequest\n\t25, // 49: memos.api.v1.UserService.DeletePersonalAccessToken:input_type -> memos.api.v1.DeletePersonalAccessTokenRequest\n\t27, // 50: memos.api.v1.UserService.ListUserWebhooks:input_type -> memos.api.v1.ListUserWebhooksRequest\n\t29, // 51: memos.api.v1.UserService.CreateUserWebhook:input_type -> memos.api.v1.CreateUserWebhookRequest\n\t30, // 52: memos.api.v1.UserService.UpdateUserWebhook:input_type -> memos.api.v1.UpdateUserWebhookRequest\n\t31, // 53: memos.api.v1.UserService.DeleteUserWebhook:input_type -> memos.api.v1.DeleteUserWebhookRequest\n\t33, // 54: memos.api.v1.UserService.ListUserNotifications:input_type -> memos.api.v1.ListUserNotificationsRequest\n\t35, // 55: memos.api.v1.UserService.UpdateUserNotification:input_type -> memos.api.v1.UpdateUserNotificationRequest\n\t36, // 56: memos.api.v1.UserService.DeleteUserNotification:input_type -> memos.api.v1.DeleteUserNotificationRequest\n\t6,  // 57: memos.api.v1.UserService.ListUsers:output_type -> memos.api.v1.ListUsersResponse\n\t4,  // 58: memos.api.v1.UserService.GetUser:output_type -> memos.api.v1.User\n\t4,  // 59: memos.api.v1.UserService.CreateUser:output_type -> memos.api.v1.User\n\t4,  // 60: memos.api.v1.UserService.UpdateUser:output_type -> memos.api.v1.User\n\t45, // 61: memos.api.v1.UserService.DeleteUser:output_type -> google.protobuf.Empty\n\t14, // 62: memos.api.v1.UserService.ListAllUserStats:output_type -> memos.api.v1.ListAllUserStatsResponse\n\t11, // 63: memos.api.v1.UserService.GetUserStats:output_type -> memos.api.v1.UserStats\n\t15, // 64: memos.api.v1.UserService.GetUserSetting:output_type -> memos.api.v1.UserSetting\n\t15, // 65: memos.api.v1.UserService.UpdateUserSetting:output_type -> memos.api.v1.UserSetting\n\t19, // 66: memos.api.v1.UserService.ListUserSettings:output_type -> memos.api.v1.ListUserSettingsResponse\n\t22, // 67: memos.api.v1.UserService.ListPersonalAccessTokens:output_type -> memos.api.v1.ListPersonalAccessTokensResponse\n\t24, // 68: memos.api.v1.UserService.CreatePersonalAccessToken:output_type -> memos.api.v1.CreatePersonalAccessTokenResponse\n\t45, // 69: memos.api.v1.UserService.DeletePersonalAccessToken:output_type -> google.protobuf.Empty\n\t28, // 70: memos.api.v1.UserService.ListUserWebhooks:output_type -> memos.api.v1.ListUserWebhooksResponse\n\t26, // 71: memos.api.v1.UserService.CreateUserWebhook:output_type -> memos.api.v1.UserWebhook\n\t26, // 72: memos.api.v1.UserService.UpdateUserWebhook:output_type -> memos.api.v1.UserWebhook\n\t45, // 73: memos.api.v1.UserService.DeleteUserWebhook:output_type -> google.protobuf.Empty\n\t34, // 74: memos.api.v1.UserService.ListUserNotifications:output_type -> memos.api.v1.ListUserNotificationsResponse\n\t32, // 75: memos.api.v1.UserService.UpdateUserNotification:output_type -> memos.api.v1.UserNotification\n\t45, // 76: memos.api.v1.UserService.DeleteUserNotification:output_type -> google.protobuf.Empty\n\t57, // [57:77] is the sub-list for method output_type\n\t37, // [37:57] is the sub-list for method input_type\n\t37, // [37:37] is the sub-list for extension type_name\n\t37, // [37:37] is the sub-list for extension extendee\n\t0,  // [0:37] is the sub-list for field type_name\n}\n\nfunc init() { file_api_v1_user_service_proto_init() }\nfunc file_api_v1_user_service_proto_init() {\n\tif File_api_v1_user_service_proto != nil {\n\t\treturn\n\t}\n\tfile_api_v1_common_proto_init()\n\tfile_api_v1_user_service_proto_msgTypes[11].OneofWrappers = []any{\n\t\t(*UserSetting_GeneralSetting_)(nil),\n\t\t(*UserSetting_WebhooksSetting_)(nil),\n\t}\n\tfile_api_v1_user_service_proto_msgTypes[28].OneofWrappers = []any{\n\t\t(*UserNotification_MemoComment)(nil),\n\t}\n\ttype x struct{}\n\tout := protoimpl.TypeBuilder{\n\t\tFile: protoimpl.DescBuilder{\n\t\t\tGoPackagePath: reflect.TypeOf(x{}).PkgPath(),\n\t\t\tRawDescriptor: unsafe.Slice(unsafe.StringData(file_api_v1_user_service_proto_rawDesc), len(file_api_v1_user_service_proto_rawDesc)),\n\t\t\tNumEnums:      4,\n\t\t\tNumMessages:   38,\n\t\t\tNumExtensions: 0,\n\t\t\tNumServices:   1,\n\t\t},\n\t\tGoTypes:           file_api_v1_user_service_proto_goTypes,\n\t\tDependencyIndexes: file_api_v1_user_service_proto_depIdxs,\n\t\tEnumInfos:         file_api_v1_user_service_proto_enumTypes,\n\t\tMessageInfos:      file_api_v1_user_service_proto_msgTypes,\n\t}.Build()\n\tFile_api_v1_user_service_proto = out.File\n\tfile_api_v1_user_service_proto_goTypes = nil\n\tfile_api_v1_user_service_proto_depIdxs = nil\n}\n"
  },
  {
    "path": "proto/gen/api/v1/user_service.pb.gw.go",
    "content": "// Code generated by protoc-gen-grpc-gateway. DO NOT EDIT.\n// source: api/v1/user_service.proto\n\n/*\nPackage apiv1 is a reverse proxy.\n\nIt translates gRPC into RESTful JSON APIs.\n*/\npackage apiv1\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"io\"\n\t\"net/http\"\n\n\t\"github.com/grpc-ecosystem/grpc-gateway/v2/runtime\"\n\t\"github.com/grpc-ecosystem/grpc-gateway/v2/utilities\"\n\t\"google.golang.org/grpc\"\n\t\"google.golang.org/grpc/codes\"\n\t\"google.golang.org/grpc/grpclog\"\n\t\"google.golang.org/grpc/metadata\"\n\t\"google.golang.org/grpc/status\"\n\t\"google.golang.org/protobuf/proto\"\n)\n\n// Suppress \"imported and not used\" errors\nvar (\n\t_ codes.Code\n\t_ io.Reader\n\t_ status.Status\n\t_ = errors.New\n\t_ = runtime.String\n\t_ = utilities.NewDoubleArray\n\t_ = metadata.Join\n)\n\nvar filter_UserService_ListUsers_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)}\n\nfunc request_UserService_ListUsers_0(ctx context.Context, marshaler runtime.Marshaler, client UserServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq ListUsersRequest\n\t\tmetadata runtime.ServerMetadata\n\t)\n\tif req.Body != nil {\n\t\t_, _ = io.Copy(io.Discard, req.Body)\n\t}\n\tif err := req.ParseForm(); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tif err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_UserService_ListUsers_0); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tmsg, err := client.ListUsers(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))\n\treturn msg, metadata, err\n}\n\nfunc local_request_UserService_ListUsers_0(ctx context.Context, marshaler runtime.Marshaler, server UserServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq ListUsersRequest\n\t\tmetadata runtime.ServerMetadata\n\t)\n\tif err := req.ParseForm(); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tif err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_UserService_ListUsers_0); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tmsg, err := server.ListUsers(ctx, &protoReq)\n\treturn msg, metadata, err\n}\n\nvar filter_UserService_GetUser_0 = &utilities.DoubleArray{Encoding: map[string]int{\"name\": 0}, Base: []int{1, 1, 0}, Check: []int{0, 1, 2}}\n\nfunc request_UserService_GetUser_0(ctx context.Context, marshaler runtime.Marshaler, client UserServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq GetUserRequest\n\t\tmetadata runtime.ServerMetadata\n\t\terr      error\n\t)\n\tif req.Body != nil {\n\t\t_, _ = io.Copy(io.Discard, req.Body)\n\t}\n\tval, ok := pathParams[\"name\"]\n\tif !ok {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"missing parameter %s\", \"name\")\n\t}\n\tprotoReq.Name, err = runtime.String(val)\n\tif err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"type mismatch, parameter: %s, error: %v\", \"name\", err)\n\t}\n\tif err := req.ParseForm(); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tif err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_UserService_GetUser_0); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tmsg, err := client.GetUser(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))\n\treturn msg, metadata, err\n}\n\nfunc local_request_UserService_GetUser_0(ctx context.Context, marshaler runtime.Marshaler, server UserServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq GetUserRequest\n\t\tmetadata runtime.ServerMetadata\n\t\terr      error\n\t)\n\tval, ok := pathParams[\"name\"]\n\tif !ok {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"missing parameter %s\", \"name\")\n\t}\n\tprotoReq.Name, err = runtime.String(val)\n\tif err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"type mismatch, parameter: %s, error: %v\", \"name\", err)\n\t}\n\tif err := req.ParseForm(); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tif err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_UserService_GetUser_0); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tmsg, err := server.GetUser(ctx, &protoReq)\n\treturn msg, metadata, err\n}\n\nvar filter_UserService_CreateUser_0 = &utilities.DoubleArray{Encoding: map[string]int{\"user\": 0}, Base: []int{1, 1, 0}, Check: []int{0, 1, 2}}\n\nfunc request_UserService_CreateUser_0(ctx context.Context, marshaler runtime.Marshaler, client UserServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq CreateUserRequest\n\t\tmetadata runtime.ServerMetadata\n\t)\n\tif err := marshaler.NewDecoder(req.Body).Decode(&protoReq.User); err != nil && !errors.Is(err, io.EOF) {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tif req.Body != nil {\n\t\t_, _ = io.Copy(io.Discard, req.Body)\n\t}\n\tif err := req.ParseForm(); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tif err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_UserService_CreateUser_0); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tmsg, err := client.CreateUser(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))\n\treturn msg, metadata, err\n}\n\nfunc local_request_UserService_CreateUser_0(ctx context.Context, marshaler runtime.Marshaler, server UserServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq CreateUserRequest\n\t\tmetadata runtime.ServerMetadata\n\t)\n\tif err := marshaler.NewDecoder(req.Body).Decode(&protoReq.User); err != nil && !errors.Is(err, io.EOF) {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tif err := req.ParseForm(); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tif err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_UserService_CreateUser_0); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tmsg, err := server.CreateUser(ctx, &protoReq)\n\treturn msg, metadata, err\n}\n\nvar filter_UserService_UpdateUser_0 = &utilities.DoubleArray{Encoding: map[string]int{\"user\": 0, \"name\": 1}, Base: []int{1, 2, 1, 0, 0}, Check: []int{0, 1, 2, 3, 2}}\n\nfunc request_UserService_UpdateUser_0(ctx context.Context, marshaler runtime.Marshaler, client UserServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq UpdateUserRequest\n\t\tmetadata runtime.ServerMetadata\n\t\terr      error\n\t)\n\tnewReader, berr := utilities.IOReaderFactory(req.Body)\n\tif berr != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", berr)\n\t}\n\tif err := marshaler.NewDecoder(newReader()).Decode(&protoReq.User); err != nil && !errors.Is(err, io.EOF) {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tif req.Body != nil {\n\t\t_, _ = io.Copy(io.Discard, req.Body)\n\t}\n\tif protoReq.UpdateMask == nil || len(protoReq.UpdateMask.GetPaths()) == 0 {\n\t\tif fieldMask, err := runtime.FieldMaskFromRequestBody(newReader(), protoReq.User); err != nil {\n\t\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t\t} else {\n\t\t\tprotoReq.UpdateMask = fieldMask\n\t\t}\n\t}\n\tval, ok := pathParams[\"user.name\"]\n\tif !ok {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"missing parameter %s\", \"user.name\")\n\t}\n\terr = runtime.PopulateFieldFromPath(&protoReq, \"user.name\", val)\n\tif err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"type mismatch, parameter: %s, error: %v\", \"user.name\", err)\n\t}\n\tif err := req.ParseForm(); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tif err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_UserService_UpdateUser_0); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tmsg, err := client.UpdateUser(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))\n\treturn msg, metadata, err\n}\n\nfunc local_request_UserService_UpdateUser_0(ctx context.Context, marshaler runtime.Marshaler, server UserServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq UpdateUserRequest\n\t\tmetadata runtime.ServerMetadata\n\t\terr      error\n\t)\n\tnewReader, berr := utilities.IOReaderFactory(req.Body)\n\tif berr != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", berr)\n\t}\n\tif err := marshaler.NewDecoder(newReader()).Decode(&protoReq.User); err != nil && !errors.Is(err, io.EOF) {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tif protoReq.UpdateMask == nil || len(protoReq.UpdateMask.GetPaths()) == 0 {\n\t\tif fieldMask, err := runtime.FieldMaskFromRequestBody(newReader(), protoReq.User); err != nil {\n\t\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t\t} else {\n\t\t\tprotoReq.UpdateMask = fieldMask\n\t\t}\n\t}\n\tval, ok := pathParams[\"user.name\"]\n\tif !ok {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"missing parameter %s\", \"user.name\")\n\t}\n\terr = runtime.PopulateFieldFromPath(&protoReq, \"user.name\", val)\n\tif err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"type mismatch, parameter: %s, error: %v\", \"user.name\", err)\n\t}\n\tif err := req.ParseForm(); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tif err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_UserService_UpdateUser_0); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tmsg, err := server.UpdateUser(ctx, &protoReq)\n\treturn msg, metadata, err\n}\n\nvar filter_UserService_DeleteUser_0 = &utilities.DoubleArray{Encoding: map[string]int{\"name\": 0}, Base: []int{1, 1, 0}, Check: []int{0, 1, 2}}\n\nfunc request_UserService_DeleteUser_0(ctx context.Context, marshaler runtime.Marshaler, client UserServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq DeleteUserRequest\n\t\tmetadata runtime.ServerMetadata\n\t\terr      error\n\t)\n\tif req.Body != nil {\n\t\t_, _ = io.Copy(io.Discard, req.Body)\n\t}\n\tval, ok := pathParams[\"name\"]\n\tif !ok {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"missing parameter %s\", \"name\")\n\t}\n\tprotoReq.Name, err = runtime.String(val)\n\tif err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"type mismatch, parameter: %s, error: %v\", \"name\", err)\n\t}\n\tif err := req.ParseForm(); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tif err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_UserService_DeleteUser_0); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tmsg, err := client.DeleteUser(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))\n\treturn msg, metadata, err\n}\n\nfunc local_request_UserService_DeleteUser_0(ctx context.Context, marshaler runtime.Marshaler, server UserServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq DeleteUserRequest\n\t\tmetadata runtime.ServerMetadata\n\t\terr      error\n\t)\n\tval, ok := pathParams[\"name\"]\n\tif !ok {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"missing parameter %s\", \"name\")\n\t}\n\tprotoReq.Name, err = runtime.String(val)\n\tif err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"type mismatch, parameter: %s, error: %v\", \"name\", err)\n\t}\n\tif err := req.ParseForm(); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tif err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_UserService_DeleteUser_0); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tmsg, err := server.DeleteUser(ctx, &protoReq)\n\treturn msg, metadata, err\n}\n\nfunc request_UserService_ListAllUserStats_0(ctx context.Context, marshaler runtime.Marshaler, client UserServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq ListAllUserStatsRequest\n\t\tmetadata runtime.ServerMetadata\n\t)\n\tif req.Body != nil {\n\t\t_, _ = io.Copy(io.Discard, req.Body)\n\t}\n\tmsg, err := client.ListAllUserStats(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))\n\treturn msg, metadata, err\n}\n\nfunc local_request_UserService_ListAllUserStats_0(ctx context.Context, marshaler runtime.Marshaler, server UserServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq ListAllUserStatsRequest\n\t\tmetadata runtime.ServerMetadata\n\t)\n\tmsg, err := server.ListAllUserStats(ctx, &protoReq)\n\treturn msg, metadata, err\n}\n\nfunc request_UserService_GetUserStats_0(ctx context.Context, marshaler runtime.Marshaler, client UserServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq GetUserStatsRequest\n\t\tmetadata runtime.ServerMetadata\n\t\terr      error\n\t)\n\tif req.Body != nil {\n\t\t_, _ = io.Copy(io.Discard, req.Body)\n\t}\n\tval, ok := pathParams[\"name\"]\n\tif !ok {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"missing parameter %s\", \"name\")\n\t}\n\tprotoReq.Name, err = runtime.String(val)\n\tif err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"type mismatch, parameter: %s, error: %v\", \"name\", err)\n\t}\n\tmsg, err := client.GetUserStats(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))\n\treturn msg, metadata, err\n}\n\nfunc local_request_UserService_GetUserStats_0(ctx context.Context, marshaler runtime.Marshaler, server UserServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq GetUserStatsRequest\n\t\tmetadata runtime.ServerMetadata\n\t\terr      error\n\t)\n\tval, ok := pathParams[\"name\"]\n\tif !ok {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"missing parameter %s\", \"name\")\n\t}\n\tprotoReq.Name, err = runtime.String(val)\n\tif err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"type mismatch, parameter: %s, error: %v\", \"name\", err)\n\t}\n\tmsg, err := server.GetUserStats(ctx, &protoReq)\n\treturn msg, metadata, err\n}\n\nfunc request_UserService_GetUserSetting_0(ctx context.Context, marshaler runtime.Marshaler, client UserServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq GetUserSettingRequest\n\t\tmetadata runtime.ServerMetadata\n\t\terr      error\n\t)\n\tif req.Body != nil {\n\t\t_, _ = io.Copy(io.Discard, req.Body)\n\t}\n\tval, ok := pathParams[\"name\"]\n\tif !ok {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"missing parameter %s\", \"name\")\n\t}\n\tprotoReq.Name, err = runtime.String(val)\n\tif err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"type mismatch, parameter: %s, error: %v\", \"name\", err)\n\t}\n\tmsg, err := client.GetUserSetting(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))\n\treturn msg, metadata, err\n}\n\nfunc local_request_UserService_GetUserSetting_0(ctx context.Context, marshaler runtime.Marshaler, server UserServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq GetUserSettingRequest\n\t\tmetadata runtime.ServerMetadata\n\t\terr      error\n\t)\n\tval, ok := pathParams[\"name\"]\n\tif !ok {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"missing parameter %s\", \"name\")\n\t}\n\tprotoReq.Name, err = runtime.String(val)\n\tif err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"type mismatch, parameter: %s, error: %v\", \"name\", err)\n\t}\n\tmsg, err := server.GetUserSetting(ctx, &protoReq)\n\treturn msg, metadata, err\n}\n\nvar filter_UserService_UpdateUserSetting_0 = &utilities.DoubleArray{Encoding: map[string]int{\"setting\": 0, \"name\": 1}, Base: []int{1, 2, 1, 0, 0}, Check: []int{0, 1, 2, 3, 2}}\n\nfunc request_UserService_UpdateUserSetting_0(ctx context.Context, marshaler runtime.Marshaler, client UserServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq UpdateUserSettingRequest\n\t\tmetadata runtime.ServerMetadata\n\t\terr      error\n\t)\n\tnewReader, berr := utilities.IOReaderFactory(req.Body)\n\tif berr != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", berr)\n\t}\n\tif err := marshaler.NewDecoder(newReader()).Decode(&protoReq.Setting); err != nil && !errors.Is(err, io.EOF) {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tif req.Body != nil {\n\t\t_, _ = io.Copy(io.Discard, req.Body)\n\t}\n\tif protoReq.UpdateMask == nil || len(protoReq.UpdateMask.GetPaths()) == 0 {\n\t\tif fieldMask, err := runtime.FieldMaskFromRequestBody(newReader(), protoReq.Setting); err != nil {\n\t\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t\t} else {\n\t\t\tprotoReq.UpdateMask = fieldMask\n\t\t}\n\t}\n\tval, ok := pathParams[\"setting.name\"]\n\tif !ok {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"missing parameter %s\", \"setting.name\")\n\t}\n\terr = runtime.PopulateFieldFromPath(&protoReq, \"setting.name\", val)\n\tif err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"type mismatch, parameter: %s, error: %v\", \"setting.name\", err)\n\t}\n\tif err := req.ParseForm(); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tif err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_UserService_UpdateUserSetting_0); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tmsg, err := client.UpdateUserSetting(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))\n\treturn msg, metadata, err\n}\n\nfunc local_request_UserService_UpdateUserSetting_0(ctx context.Context, marshaler runtime.Marshaler, server UserServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq UpdateUserSettingRequest\n\t\tmetadata runtime.ServerMetadata\n\t\terr      error\n\t)\n\tnewReader, berr := utilities.IOReaderFactory(req.Body)\n\tif berr != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", berr)\n\t}\n\tif err := marshaler.NewDecoder(newReader()).Decode(&protoReq.Setting); err != nil && !errors.Is(err, io.EOF) {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tif protoReq.UpdateMask == nil || len(protoReq.UpdateMask.GetPaths()) == 0 {\n\t\tif fieldMask, err := runtime.FieldMaskFromRequestBody(newReader(), protoReq.Setting); err != nil {\n\t\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t\t} else {\n\t\t\tprotoReq.UpdateMask = fieldMask\n\t\t}\n\t}\n\tval, ok := pathParams[\"setting.name\"]\n\tif !ok {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"missing parameter %s\", \"setting.name\")\n\t}\n\terr = runtime.PopulateFieldFromPath(&protoReq, \"setting.name\", val)\n\tif err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"type mismatch, parameter: %s, error: %v\", \"setting.name\", err)\n\t}\n\tif err := req.ParseForm(); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tif err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_UserService_UpdateUserSetting_0); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tmsg, err := server.UpdateUserSetting(ctx, &protoReq)\n\treturn msg, metadata, err\n}\n\nvar filter_UserService_ListUserSettings_0 = &utilities.DoubleArray{Encoding: map[string]int{\"parent\": 0}, Base: []int{1, 1, 0}, Check: []int{0, 1, 2}}\n\nfunc request_UserService_ListUserSettings_0(ctx context.Context, marshaler runtime.Marshaler, client UserServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq ListUserSettingsRequest\n\t\tmetadata runtime.ServerMetadata\n\t\terr      error\n\t)\n\tif req.Body != nil {\n\t\t_, _ = io.Copy(io.Discard, req.Body)\n\t}\n\tval, ok := pathParams[\"parent\"]\n\tif !ok {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"missing parameter %s\", \"parent\")\n\t}\n\tprotoReq.Parent, err = runtime.String(val)\n\tif err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"type mismatch, parameter: %s, error: %v\", \"parent\", err)\n\t}\n\tif err := req.ParseForm(); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tif err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_UserService_ListUserSettings_0); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tmsg, err := client.ListUserSettings(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))\n\treturn msg, metadata, err\n}\n\nfunc local_request_UserService_ListUserSettings_0(ctx context.Context, marshaler runtime.Marshaler, server UserServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq ListUserSettingsRequest\n\t\tmetadata runtime.ServerMetadata\n\t\terr      error\n\t)\n\tval, ok := pathParams[\"parent\"]\n\tif !ok {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"missing parameter %s\", \"parent\")\n\t}\n\tprotoReq.Parent, err = runtime.String(val)\n\tif err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"type mismatch, parameter: %s, error: %v\", \"parent\", err)\n\t}\n\tif err := req.ParseForm(); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tif err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_UserService_ListUserSettings_0); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tmsg, err := server.ListUserSettings(ctx, &protoReq)\n\treturn msg, metadata, err\n}\n\nvar filter_UserService_ListPersonalAccessTokens_0 = &utilities.DoubleArray{Encoding: map[string]int{\"parent\": 0}, Base: []int{1, 1, 0}, Check: []int{0, 1, 2}}\n\nfunc request_UserService_ListPersonalAccessTokens_0(ctx context.Context, marshaler runtime.Marshaler, client UserServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq ListPersonalAccessTokensRequest\n\t\tmetadata runtime.ServerMetadata\n\t\terr      error\n\t)\n\tif req.Body != nil {\n\t\t_, _ = io.Copy(io.Discard, req.Body)\n\t}\n\tval, ok := pathParams[\"parent\"]\n\tif !ok {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"missing parameter %s\", \"parent\")\n\t}\n\tprotoReq.Parent, err = runtime.String(val)\n\tif err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"type mismatch, parameter: %s, error: %v\", \"parent\", err)\n\t}\n\tif err := req.ParseForm(); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tif err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_UserService_ListPersonalAccessTokens_0); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tmsg, err := client.ListPersonalAccessTokens(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))\n\treturn msg, metadata, err\n}\n\nfunc local_request_UserService_ListPersonalAccessTokens_0(ctx context.Context, marshaler runtime.Marshaler, server UserServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq ListPersonalAccessTokensRequest\n\t\tmetadata runtime.ServerMetadata\n\t\terr      error\n\t)\n\tval, ok := pathParams[\"parent\"]\n\tif !ok {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"missing parameter %s\", \"parent\")\n\t}\n\tprotoReq.Parent, err = runtime.String(val)\n\tif err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"type mismatch, parameter: %s, error: %v\", \"parent\", err)\n\t}\n\tif err := req.ParseForm(); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tif err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_UserService_ListPersonalAccessTokens_0); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tmsg, err := server.ListPersonalAccessTokens(ctx, &protoReq)\n\treturn msg, metadata, err\n}\n\nfunc request_UserService_CreatePersonalAccessToken_0(ctx context.Context, marshaler runtime.Marshaler, client UserServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq CreatePersonalAccessTokenRequest\n\t\tmetadata runtime.ServerMetadata\n\t\terr      error\n\t)\n\tif err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tif req.Body != nil {\n\t\t_, _ = io.Copy(io.Discard, req.Body)\n\t}\n\tval, ok := pathParams[\"parent\"]\n\tif !ok {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"missing parameter %s\", \"parent\")\n\t}\n\tprotoReq.Parent, err = runtime.String(val)\n\tif err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"type mismatch, parameter: %s, error: %v\", \"parent\", err)\n\t}\n\tmsg, err := client.CreatePersonalAccessToken(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))\n\treturn msg, metadata, err\n}\n\nfunc local_request_UserService_CreatePersonalAccessToken_0(ctx context.Context, marshaler runtime.Marshaler, server UserServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq CreatePersonalAccessTokenRequest\n\t\tmetadata runtime.ServerMetadata\n\t\terr      error\n\t)\n\tif err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tval, ok := pathParams[\"parent\"]\n\tif !ok {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"missing parameter %s\", \"parent\")\n\t}\n\tprotoReq.Parent, err = runtime.String(val)\n\tif err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"type mismatch, parameter: %s, error: %v\", \"parent\", err)\n\t}\n\tmsg, err := server.CreatePersonalAccessToken(ctx, &protoReq)\n\treturn msg, metadata, err\n}\n\nfunc request_UserService_DeletePersonalAccessToken_0(ctx context.Context, marshaler runtime.Marshaler, client UserServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq DeletePersonalAccessTokenRequest\n\t\tmetadata runtime.ServerMetadata\n\t\terr      error\n\t)\n\tif req.Body != nil {\n\t\t_, _ = io.Copy(io.Discard, req.Body)\n\t}\n\tval, ok := pathParams[\"name\"]\n\tif !ok {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"missing parameter %s\", \"name\")\n\t}\n\tprotoReq.Name, err = runtime.String(val)\n\tif err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"type mismatch, parameter: %s, error: %v\", \"name\", err)\n\t}\n\tmsg, err := client.DeletePersonalAccessToken(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))\n\treturn msg, metadata, err\n}\n\nfunc local_request_UserService_DeletePersonalAccessToken_0(ctx context.Context, marshaler runtime.Marshaler, server UserServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq DeletePersonalAccessTokenRequest\n\t\tmetadata runtime.ServerMetadata\n\t\terr      error\n\t)\n\tval, ok := pathParams[\"name\"]\n\tif !ok {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"missing parameter %s\", \"name\")\n\t}\n\tprotoReq.Name, err = runtime.String(val)\n\tif err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"type mismatch, parameter: %s, error: %v\", \"name\", err)\n\t}\n\tmsg, err := server.DeletePersonalAccessToken(ctx, &protoReq)\n\treturn msg, metadata, err\n}\n\nfunc request_UserService_ListUserWebhooks_0(ctx context.Context, marshaler runtime.Marshaler, client UserServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq ListUserWebhooksRequest\n\t\tmetadata runtime.ServerMetadata\n\t\terr      error\n\t)\n\tif req.Body != nil {\n\t\t_, _ = io.Copy(io.Discard, req.Body)\n\t}\n\tval, ok := pathParams[\"parent\"]\n\tif !ok {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"missing parameter %s\", \"parent\")\n\t}\n\tprotoReq.Parent, err = runtime.String(val)\n\tif err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"type mismatch, parameter: %s, error: %v\", \"parent\", err)\n\t}\n\tmsg, err := client.ListUserWebhooks(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))\n\treturn msg, metadata, err\n}\n\nfunc local_request_UserService_ListUserWebhooks_0(ctx context.Context, marshaler runtime.Marshaler, server UserServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq ListUserWebhooksRequest\n\t\tmetadata runtime.ServerMetadata\n\t\terr      error\n\t)\n\tval, ok := pathParams[\"parent\"]\n\tif !ok {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"missing parameter %s\", \"parent\")\n\t}\n\tprotoReq.Parent, err = runtime.String(val)\n\tif err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"type mismatch, parameter: %s, error: %v\", \"parent\", err)\n\t}\n\tmsg, err := server.ListUserWebhooks(ctx, &protoReq)\n\treturn msg, metadata, err\n}\n\nfunc request_UserService_CreateUserWebhook_0(ctx context.Context, marshaler runtime.Marshaler, client UserServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq CreateUserWebhookRequest\n\t\tmetadata runtime.ServerMetadata\n\t\terr      error\n\t)\n\tif err := marshaler.NewDecoder(req.Body).Decode(&protoReq.Webhook); err != nil && !errors.Is(err, io.EOF) {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tif req.Body != nil {\n\t\t_, _ = io.Copy(io.Discard, req.Body)\n\t}\n\tval, ok := pathParams[\"parent\"]\n\tif !ok {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"missing parameter %s\", \"parent\")\n\t}\n\tprotoReq.Parent, err = runtime.String(val)\n\tif err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"type mismatch, parameter: %s, error: %v\", \"parent\", err)\n\t}\n\tmsg, err := client.CreateUserWebhook(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))\n\treturn msg, metadata, err\n}\n\nfunc local_request_UserService_CreateUserWebhook_0(ctx context.Context, marshaler runtime.Marshaler, server UserServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq CreateUserWebhookRequest\n\t\tmetadata runtime.ServerMetadata\n\t\terr      error\n\t)\n\tif err := marshaler.NewDecoder(req.Body).Decode(&protoReq.Webhook); err != nil && !errors.Is(err, io.EOF) {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tval, ok := pathParams[\"parent\"]\n\tif !ok {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"missing parameter %s\", \"parent\")\n\t}\n\tprotoReq.Parent, err = runtime.String(val)\n\tif err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"type mismatch, parameter: %s, error: %v\", \"parent\", err)\n\t}\n\tmsg, err := server.CreateUserWebhook(ctx, &protoReq)\n\treturn msg, metadata, err\n}\n\nvar filter_UserService_UpdateUserWebhook_0 = &utilities.DoubleArray{Encoding: map[string]int{\"webhook\": 0, \"name\": 1}, Base: []int{1, 2, 1, 0, 0}, Check: []int{0, 1, 2, 3, 2}}\n\nfunc request_UserService_UpdateUserWebhook_0(ctx context.Context, marshaler runtime.Marshaler, client UserServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq UpdateUserWebhookRequest\n\t\tmetadata runtime.ServerMetadata\n\t\terr      error\n\t)\n\tnewReader, berr := utilities.IOReaderFactory(req.Body)\n\tif berr != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", berr)\n\t}\n\tif err := marshaler.NewDecoder(newReader()).Decode(&protoReq.Webhook); err != nil && !errors.Is(err, io.EOF) {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tif req.Body != nil {\n\t\t_, _ = io.Copy(io.Discard, req.Body)\n\t}\n\tif protoReq.UpdateMask == nil || len(protoReq.UpdateMask.GetPaths()) == 0 {\n\t\tif fieldMask, err := runtime.FieldMaskFromRequestBody(newReader(), protoReq.Webhook); err != nil {\n\t\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t\t} else {\n\t\t\tprotoReq.UpdateMask = fieldMask\n\t\t}\n\t}\n\tval, ok := pathParams[\"webhook.name\"]\n\tif !ok {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"missing parameter %s\", \"webhook.name\")\n\t}\n\terr = runtime.PopulateFieldFromPath(&protoReq, \"webhook.name\", val)\n\tif err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"type mismatch, parameter: %s, error: %v\", \"webhook.name\", err)\n\t}\n\tif err := req.ParseForm(); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tif err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_UserService_UpdateUserWebhook_0); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tmsg, err := client.UpdateUserWebhook(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))\n\treturn msg, metadata, err\n}\n\nfunc local_request_UserService_UpdateUserWebhook_0(ctx context.Context, marshaler runtime.Marshaler, server UserServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq UpdateUserWebhookRequest\n\t\tmetadata runtime.ServerMetadata\n\t\terr      error\n\t)\n\tnewReader, berr := utilities.IOReaderFactory(req.Body)\n\tif berr != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", berr)\n\t}\n\tif err := marshaler.NewDecoder(newReader()).Decode(&protoReq.Webhook); err != nil && !errors.Is(err, io.EOF) {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tif protoReq.UpdateMask == nil || len(protoReq.UpdateMask.GetPaths()) == 0 {\n\t\tif fieldMask, err := runtime.FieldMaskFromRequestBody(newReader(), protoReq.Webhook); err != nil {\n\t\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t\t} else {\n\t\t\tprotoReq.UpdateMask = fieldMask\n\t\t}\n\t}\n\tval, ok := pathParams[\"webhook.name\"]\n\tif !ok {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"missing parameter %s\", \"webhook.name\")\n\t}\n\terr = runtime.PopulateFieldFromPath(&protoReq, \"webhook.name\", val)\n\tif err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"type mismatch, parameter: %s, error: %v\", \"webhook.name\", err)\n\t}\n\tif err := req.ParseForm(); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tif err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_UserService_UpdateUserWebhook_0); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tmsg, err := server.UpdateUserWebhook(ctx, &protoReq)\n\treturn msg, metadata, err\n}\n\nfunc request_UserService_DeleteUserWebhook_0(ctx context.Context, marshaler runtime.Marshaler, client UserServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq DeleteUserWebhookRequest\n\t\tmetadata runtime.ServerMetadata\n\t\terr      error\n\t)\n\tif req.Body != nil {\n\t\t_, _ = io.Copy(io.Discard, req.Body)\n\t}\n\tval, ok := pathParams[\"name\"]\n\tif !ok {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"missing parameter %s\", \"name\")\n\t}\n\tprotoReq.Name, err = runtime.String(val)\n\tif err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"type mismatch, parameter: %s, error: %v\", \"name\", err)\n\t}\n\tmsg, err := client.DeleteUserWebhook(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))\n\treturn msg, metadata, err\n}\n\nfunc local_request_UserService_DeleteUserWebhook_0(ctx context.Context, marshaler runtime.Marshaler, server UserServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq DeleteUserWebhookRequest\n\t\tmetadata runtime.ServerMetadata\n\t\terr      error\n\t)\n\tval, ok := pathParams[\"name\"]\n\tif !ok {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"missing parameter %s\", \"name\")\n\t}\n\tprotoReq.Name, err = runtime.String(val)\n\tif err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"type mismatch, parameter: %s, error: %v\", \"name\", err)\n\t}\n\tmsg, err := server.DeleteUserWebhook(ctx, &protoReq)\n\treturn msg, metadata, err\n}\n\nvar filter_UserService_ListUserNotifications_0 = &utilities.DoubleArray{Encoding: map[string]int{\"parent\": 0}, Base: []int{1, 1, 0}, Check: []int{0, 1, 2}}\n\nfunc request_UserService_ListUserNotifications_0(ctx context.Context, marshaler runtime.Marshaler, client UserServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq ListUserNotificationsRequest\n\t\tmetadata runtime.ServerMetadata\n\t\terr      error\n\t)\n\tif req.Body != nil {\n\t\t_, _ = io.Copy(io.Discard, req.Body)\n\t}\n\tval, ok := pathParams[\"parent\"]\n\tif !ok {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"missing parameter %s\", \"parent\")\n\t}\n\tprotoReq.Parent, err = runtime.String(val)\n\tif err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"type mismatch, parameter: %s, error: %v\", \"parent\", err)\n\t}\n\tif err := req.ParseForm(); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tif err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_UserService_ListUserNotifications_0); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tmsg, err := client.ListUserNotifications(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))\n\treturn msg, metadata, err\n}\n\nfunc local_request_UserService_ListUserNotifications_0(ctx context.Context, marshaler runtime.Marshaler, server UserServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq ListUserNotificationsRequest\n\t\tmetadata runtime.ServerMetadata\n\t\terr      error\n\t)\n\tval, ok := pathParams[\"parent\"]\n\tif !ok {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"missing parameter %s\", \"parent\")\n\t}\n\tprotoReq.Parent, err = runtime.String(val)\n\tif err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"type mismatch, parameter: %s, error: %v\", \"parent\", err)\n\t}\n\tif err := req.ParseForm(); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tif err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_UserService_ListUserNotifications_0); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tmsg, err := server.ListUserNotifications(ctx, &protoReq)\n\treturn msg, metadata, err\n}\n\nvar filter_UserService_UpdateUserNotification_0 = &utilities.DoubleArray{Encoding: map[string]int{\"notification\": 0, \"name\": 1}, Base: []int{1, 2, 1, 0, 0}, Check: []int{0, 1, 2, 3, 2}}\n\nfunc request_UserService_UpdateUserNotification_0(ctx context.Context, marshaler runtime.Marshaler, client UserServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq UpdateUserNotificationRequest\n\t\tmetadata runtime.ServerMetadata\n\t\terr      error\n\t)\n\tnewReader, berr := utilities.IOReaderFactory(req.Body)\n\tif berr != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", berr)\n\t}\n\tif err := marshaler.NewDecoder(newReader()).Decode(&protoReq.Notification); err != nil && !errors.Is(err, io.EOF) {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tif req.Body != nil {\n\t\t_, _ = io.Copy(io.Discard, req.Body)\n\t}\n\tif protoReq.UpdateMask == nil || len(protoReq.UpdateMask.GetPaths()) == 0 {\n\t\tif fieldMask, err := runtime.FieldMaskFromRequestBody(newReader(), protoReq.Notification); err != nil {\n\t\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t\t} else {\n\t\t\tprotoReq.UpdateMask = fieldMask\n\t\t}\n\t}\n\tval, ok := pathParams[\"notification.name\"]\n\tif !ok {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"missing parameter %s\", \"notification.name\")\n\t}\n\terr = runtime.PopulateFieldFromPath(&protoReq, \"notification.name\", val)\n\tif err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"type mismatch, parameter: %s, error: %v\", \"notification.name\", err)\n\t}\n\tif err := req.ParseForm(); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tif err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_UserService_UpdateUserNotification_0); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tmsg, err := client.UpdateUserNotification(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))\n\treturn msg, metadata, err\n}\n\nfunc local_request_UserService_UpdateUserNotification_0(ctx context.Context, marshaler runtime.Marshaler, server UserServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq UpdateUserNotificationRequest\n\t\tmetadata runtime.ServerMetadata\n\t\terr      error\n\t)\n\tnewReader, berr := utilities.IOReaderFactory(req.Body)\n\tif berr != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", berr)\n\t}\n\tif err := marshaler.NewDecoder(newReader()).Decode(&protoReq.Notification); err != nil && !errors.Is(err, io.EOF) {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tif protoReq.UpdateMask == nil || len(protoReq.UpdateMask.GetPaths()) == 0 {\n\t\tif fieldMask, err := runtime.FieldMaskFromRequestBody(newReader(), protoReq.Notification); err != nil {\n\t\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t\t} else {\n\t\t\tprotoReq.UpdateMask = fieldMask\n\t\t}\n\t}\n\tval, ok := pathParams[\"notification.name\"]\n\tif !ok {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"missing parameter %s\", \"notification.name\")\n\t}\n\terr = runtime.PopulateFieldFromPath(&protoReq, \"notification.name\", val)\n\tif err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"type mismatch, parameter: %s, error: %v\", \"notification.name\", err)\n\t}\n\tif err := req.ParseForm(); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tif err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_UserService_UpdateUserNotification_0); err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"%v\", err)\n\t}\n\tmsg, err := server.UpdateUserNotification(ctx, &protoReq)\n\treturn msg, metadata, err\n}\n\nfunc request_UserService_DeleteUserNotification_0(ctx context.Context, marshaler runtime.Marshaler, client UserServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq DeleteUserNotificationRequest\n\t\tmetadata runtime.ServerMetadata\n\t\terr      error\n\t)\n\tif req.Body != nil {\n\t\t_, _ = io.Copy(io.Discard, req.Body)\n\t}\n\tval, ok := pathParams[\"name\"]\n\tif !ok {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"missing parameter %s\", \"name\")\n\t}\n\tprotoReq.Name, err = runtime.String(val)\n\tif err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"type mismatch, parameter: %s, error: %v\", \"name\", err)\n\t}\n\tmsg, err := client.DeleteUserNotification(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))\n\treturn msg, metadata, err\n}\n\nfunc local_request_UserService_DeleteUserNotification_0(ctx context.Context, marshaler runtime.Marshaler, server UserServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {\n\tvar (\n\t\tprotoReq DeleteUserNotificationRequest\n\t\tmetadata runtime.ServerMetadata\n\t\terr      error\n\t)\n\tval, ok := pathParams[\"name\"]\n\tif !ok {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"missing parameter %s\", \"name\")\n\t}\n\tprotoReq.Name, err = runtime.String(val)\n\tif err != nil {\n\t\treturn nil, metadata, status.Errorf(codes.InvalidArgument, \"type mismatch, parameter: %s, error: %v\", \"name\", err)\n\t}\n\tmsg, err := server.DeleteUserNotification(ctx, &protoReq)\n\treturn msg, metadata, err\n}\n\n// RegisterUserServiceHandlerServer registers the http handlers for service UserService to \"mux\".\n// UnaryRPC     :call UserServiceServer directly.\n// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906.\n// Note that using this registration option will cause many gRPC library features to stop working. Consider using RegisterUserServiceHandlerFromEndpoint instead.\n// GRPC interceptors will not work for this type of registration. To use interceptors, you must use the \"runtime.WithMiddlewares\" option in the \"runtime.NewServeMux\" call.\nfunc RegisterUserServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux, server UserServiceServer) error {\n\tmux.Handle(http.MethodGet, pattern_UserService_ListUsers_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tvar stream runtime.ServerTransportStream\n\t\tctx = grpc.NewContextWithServerTransportStream(ctx, &stream)\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, \"/memos.api.v1.UserService/ListUsers\", runtime.WithHTTPPathPattern(\"/api/v1/users\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := local_request_UserService_ListUsers_0(annotatedContext, inboundMarshaler, server, req, pathParams)\n\t\tmd.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_UserService_ListUsers_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodGet, pattern_UserService_GetUser_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tvar stream runtime.ServerTransportStream\n\t\tctx = grpc.NewContextWithServerTransportStream(ctx, &stream)\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, \"/memos.api.v1.UserService/GetUser\", runtime.WithHTTPPathPattern(\"/api/v1/{name=users/*}\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := local_request_UserService_GetUser_0(annotatedContext, inboundMarshaler, server, req, pathParams)\n\t\tmd.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_UserService_GetUser_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodPost, pattern_UserService_CreateUser_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tvar stream runtime.ServerTransportStream\n\t\tctx = grpc.NewContextWithServerTransportStream(ctx, &stream)\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, \"/memos.api.v1.UserService/CreateUser\", runtime.WithHTTPPathPattern(\"/api/v1/users\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := local_request_UserService_CreateUser_0(annotatedContext, inboundMarshaler, server, req, pathParams)\n\t\tmd.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_UserService_CreateUser_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodPatch, pattern_UserService_UpdateUser_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tvar stream runtime.ServerTransportStream\n\t\tctx = grpc.NewContextWithServerTransportStream(ctx, &stream)\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, \"/memos.api.v1.UserService/UpdateUser\", runtime.WithHTTPPathPattern(\"/api/v1/{user.name=users/*}\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := local_request_UserService_UpdateUser_0(annotatedContext, inboundMarshaler, server, req, pathParams)\n\t\tmd.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_UserService_UpdateUser_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodDelete, pattern_UserService_DeleteUser_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tvar stream runtime.ServerTransportStream\n\t\tctx = grpc.NewContextWithServerTransportStream(ctx, &stream)\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, \"/memos.api.v1.UserService/DeleteUser\", runtime.WithHTTPPathPattern(\"/api/v1/{name=users/*}\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := local_request_UserService_DeleteUser_0(annotatedContext, inboundMarshaler, server, req, pathParams)\n\t\tmd.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_UserService_DeleteUser_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodGet, pattern_UserService_ListAllUserStats_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tvar stream runtime.ServerTransportStream\n\t\tctx = grpc.NewContextWithServerTransportStream(ctx, &stream)\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, \"/memos.api.v1.UserService/ListAllUserStats\", runtime.WithHTTPPathPattern(\"/api/v1/users:stats\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := local_request_UserService_ListAllUserStats_0(annotatedContext, inboundMarshaler, server, req, pathParams)\n\t\tmd.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_UserService_ListAllUserStats_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodGet, pattern_UserService_GetUserStats_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tvar stream runtime.ServerTransportStream\n\t\tctx = grpc.NewContextWithServerTransportStream(ctx, &stream)\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, \"/memos.api.v1.UserService/GetUserStats\", runtime.WithHTTPPathPattern(\"/api/v1/{name=users/*}:getStats\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := local_request_UserService_GetUserStats_0(annotatedContext, inboundMarshaler, server, req, pathParams)\n\t\tmd.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_UserService_GetUserStats_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodGet, pattern_UserService_GetUserSetting_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tvar stream runtime.ServerTransportStream\n\t\tctx = grpc.NewContextWithServerTransportStream(ctx, &stream)\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, \"/memos.api.v1.UserService/GetUserSetting\", runtime.WithHTTPPathPattern(\"/api/v1/{name=users/*/settings/*}\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := local_request_UserService_GetUserSetting_0(annotatedContext, inboundMarshaler, server, req, pathParams)\n\t\tmd.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_UserService_GetUserSetting_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodPatch, pattern_UserService_UpdateUserSetting_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tvar stream runtime.ServerTransportStream\n\t\tctx = grpc.NewContextWithServerTransportStream(ctx, &stream)\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, \"/memos.api.v1.UserService/UpdateUserSetting\", runtime.WithHTTPPathPattern(\"/api/v1/{setting.name=users/*/settings/*}\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := local_request_UserService_UpdateUserSetting_0(annotatedContext, inboundMarshaler, server, req, pathParams)\n\t\tmd.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_UserService_UpdateUserSetting_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodGet, pattern_UserService_ListUserSettings_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tvar stream runtime.ServerTransportStream\n\t\tctx = grpc.NewContextWithServerTransportStream(ctx, &stream)\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, \"/memos.api.v1.UserService/ListUserSettings\", runtime.WithHTTPPathPattern(\"/api/v1/{parent=users/*}/settings\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := local_request_UserService_ListUserSettings_0(annotatedContext, inboundMarshaler, server, req, pathParams)\n\t\tmd.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_UserService_ListUserSettings_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodGet, pattern_UserService_ListPersonalAccessTokens_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tvar stream runtime.ServerTransportStream\n\t\tctx = grpc.NewContextWithServerTransportStream(ctx, &stream)\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, \"/memos.api.v1.UserService/ListPersonalAccessTokens\", runtime.WithHTTPPathPattern(\"/api/v1/{parent=users/*}/personalAccessTokens\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := local_request_UserService_ListPersonalAccessTokens_0(annotatedContext, inboundMarshaler, server, req, pathParams)\n\t\tmd.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_UserService_ListPersonalAccessTokens_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodPost, pattern_UserService_CreatePersonalAccessToken_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tvar stream runtime.ServerTransportStream\n\t\tctx = grpc.NewContextWithServerTransportStream(ctx, &stream)\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, \"/memos.api.v1.UserService/CreatePersonalAccessToken\", runtime.WithHTTPPathPattern(\"/api/v1/{parent=users/*}/personalAccessTokens\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := local_request_UserService_CreatePersonalAccessToken_0(annotatedContext, inboundMarshaler, server, req, pathParams)\n\t\tmd.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_UserService_CreatePersonalAccessToken_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodDelete, pattern_UserService_DeletePersonalAccessToken_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tvar stream runtime.ServerTransportStream\n\t\tctx = grpc.NewContextWithServerTransportStream(ctx, &stream)\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, \"/memos.api.v1.UserService/DeletePersonalAccessToken\", runtime.WithHTTPPathPattern(\"/api/v1/{name=users/*/personalAccessTokens/*}\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := local_request_UserService_DeletePersonalAccessToken_0(annotatedContext, inboundMarshaler, server, req, pathParams)\n\t\tmd.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_UserService_DeletePersonalAccessToken_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodGet, pattern_UserService_ListUserWebhooks_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tvar stream runtime.ServerTransportStream\n\t\tctx = grpc.NewContextWithServerTransportStream(ctx, &stream)\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, \"/memos.api.v1.UserService/ListUserWebhooks\", runtime.WithHTTPPathPattern(\"/api/v1/{parent=users/*}/webhooks\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := local_request_UserService_ListUserWebhooks_0(annotatedContext, inboundMarshaler, server, req, pathParams)\n\t\tmd.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_UserService_ListUserWebhooks_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodPost, pattern_UserService_CreateUserWebhook_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tvar stream runtime.ServerTransportStream\n\t\tctx = grpc.NewContextWithServerTransportStream(ctx, &stream)\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, \"/memos.api.v1.UserService/CreateUserWebhook\", runtime.WithHTTPPathPattern(\"/api/v1/{parent=users/*}/webhooks\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := local_request_UserService_CreateUserWebhook_0(annotatedContext, inboundMarshaler, server, req, pathParams)\n\t\tmd.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_UserService_CreateUserWebhook_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodPatch, pattern_UserService_UpdateUserWebhook_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tvar stream runtime.ServerTransportStream\n\t\tctx = grpc.NewContextWithServerTransportStream(ctx, &stream)\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, \"/memos.api.v1.UserService/UpdateUserWebhook\", runtime.WithHTTPPathPattern(\"/api/v1/{webhook.name=users/*/webhooks/*}\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := local_request_UserService_UpdateUserWebhook_0(annotatedContext, inboundMarshaler, server, req, pathParams)\n\t\tmd.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_UserService_UpdateUserWebhook_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodDelete, pattern_UserService_DeleteUserWebhook_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tvar stream runtime.ServerTransportStream\n\t\tctx = grpc.NewContextWithServerTransportStream(ctx, &stream)\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, \"/memos.api.v1.UserService/DeleteUserWebhook\", runtime.WithHTTPPathPattern(\"/api/v1/{name=users/*/webhooks/*}\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := local_request_UserService_DeleteUserWebhook_0(annotatedContext, inboundMarshaler, server, req, pathParams)\n\t\tmd.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_UserService_DeleteUserWebhook_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodGet, pattern_UserService_ListUserNotifications_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tvar stream runtime.ServerTransportStream\n\t\tctx = grpc.NewContextWithServerTransportStream(ctx, &stream)\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, \"/memos.api.v1.UserService/ListUserNotifications\", runtime.WithHTTPPathPattern(\"/api/v1/{parent=users/*}/notifications\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := local_request_UserService_ListUserNotifications_0(annotatedContext, inboundMarshaler, server, req, pathParams)\n\t\tmd.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_UserService_ListUserNotifications_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodPatch, pattern_UserService_UpdateUserNotification_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tvar stream runtime.ServerTransportStream\n\t\tctx = grpc.NewContextWithServerTransportStream(ctx, &stream)\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, \"/memos.api.v1.UserService/UpdateUserNotification\", runtime.WithHTTPPathPattern(\"/api/v1/{notification.name=users/*/notifications/*}\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := local_request_UserService_UpdateUserNotification_0(annotatedContext, inboundMarshaler, server, req, pathParams)\n\t\tmd.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_UserService_UpdateUserNotification_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodDelete, pattern_UserService_DeleteUserNotification_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tvar stream runtime.ServerTransportStream\n\t\tctx = grpc.NewContextWithServerTransportStream(ctx, &stream)\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, \"/memos.api.v1.UserService/DeleteUserNotification\", runtime.WithHTTPPathPattern(\"/api/v1/{name=users/*/notifications/*}\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := local_request_UserService_DeleteUserNotification_0(annotatedContext, inboundMarshaler, server, req, pathParams)\n\t\tmd.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_UserService_DeleteUserNotification_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\n\treturn nil\n}\n\n// RegisterUserServiceHandlerFromEndpoint is same as RegisterUserServiceHandler but\n// automatically dials to \"endpoint\" and closes the connection when \"ctx\" gets done.\nfunc RegisterUserServiceHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) {\n\tconn, err := grpc.NewClient(endpoint, opts...)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer func() {\n\t\tif err != nil {\n\t\t\tif cerr := conn.Close(); cerr != nil {\n\t\t\t\tgrpclog.Errorf(\"Failed to close conn to %s: %v\", endpoint, cerr)\n\t\t\t}\n\t\t\treturn\n\t\t}\n\t\tgo func() {\n\t\t\t<-ctx.Done()\n\t\t\tif cerr := conn.Close(); cerr != nil {\n\t\t\t\tgrpclog.Errorf(\"Failed to close conn to %s: %v\", endpoint, cerr)\n\t\t\t}\n\t\t}()\n\t}()\n\treturn RegisterUserServiceHandler(ctx, mux, conn)\n}\n\n// RegisterUserServiceHandler registers the http handlers for service UserService to \"mux\".\n// The handlers forward requests to the grpc endpoint over \"conn\".\nfunc RegisterUserServiceHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error {\n\treturn RegisterUserServiceHandlerClient(ctx, mux, NewUserServiceClient(conn))\n}\n\n// RegisterUserServiceHandlerClient registers the http handlers for service UserService\n// to \"mux\". The handlers forward requests to the grpc endpoint over the given implementation of \"UserServiceClient\".\n// Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in \"UserServiceClient\"\n// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in\n// \"UserServiceClient\" to call the correct interceptors. This client ignores the HTTP middlewares.\nfunc RegisterUserServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux, client UserServiceClient) error {\n\tmux.Handle(http.MethodGet, pattern_UserService_ListUsers_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateContext(ctx, mux, req, \"/memos.api.v1.UserService/ListUsers\", runtime.WithHTTPPathPattern(\"/api/v1/users\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := request_UserService_ListUsers_0(annotatedContext, inboundMarshaler, client, req, pathParams)\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_UserService_ListUsers_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodGet, pattern_UserService_GetUser_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateContext(ctx, mux, req, \"/memos.api.v1.UserService/GetUser\", runtime.WithHTTPPathPattern(\"/api/v1/{name=users/*}\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := request_UserService_GetUser_0(annotatedContext, inboundMarshaler, client, req, pathParams)\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_UserService_GetUser_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodPost, pattern_UserService_CreateUser_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateContext(ctx, mux, req, \"/memos.api.v1.UserService/CreateUser\", runtime.WithHTTPPathPattern(\"/api/v1/users\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := request_UserService_CreateUser_0(annotatedContext, inboundMarshaler, client, req, pathParams)\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_UserService_CreateUser_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodPatch, pattern_UserService_UpdateUser_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateContext(ctx, mux, req, \"/memos.api.v1.UserService/UpdateUser\", runtime.WithHTTPPathPattern(\"/api/v1/{user.name=users/*}\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := request_UserService_UpdateUser_0(annotatedContext, inboundMarshaler, client, req, pathParams)\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_UserService_UpdateUser_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodDelete, pattern_UserService_DeleteUser_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateContext(ctx, mux, req, \"/memos.api.v1.UserService/DeleteUser\", runtime.WithHTTPPathPattern(\"/api/v1/{name=users/*}\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := request_UserService_DeleteUser_0(annotatedContext, inboundMarshaler, client, req, pathParams)\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_UserService_DeleteUser_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodGet, pattern_UserService_ListAllUserStats_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateContext(ctx, mux, req, \"/memos.api.v1.UserService/ListAllUserStats\", runtime.WithHTTPPathPattern(\"/api/v1/users:stats\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := request_UserService_ListAllUserStats_0(annotatedContext, inboundMarshaler, client, req, pathParams)\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_UserService_ListAllUserStats_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodGet, pattern_UserService_GetUserStats_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateContext(ctx, mux, req, \"/memos.api.v1.UserService/GetUserStats\", runtime.WithHTTPPathPattern(\"/api/v1/{name=users/*}:getStats\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := request_UserService_GetUserStats_0(annotatedContext, inboundMarshaler, client, req, pathParams)\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_UserService_GetUserStats_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodGet, pattern_UserService_GetUserSetting_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateContext(ctx, mux, req, \"/memos.api.v1.UserService/GetUserSetting\", runtime.WithHTTPPathPattern(\"/api/v1/{name=users/*/settings/*}\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := request_UserService_GetUserSetting_0(annotatedContext, inboundMarshaler, client, req, pathParams)\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_UserService_GetUserSetting_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodPatch, pattern_UserService_UpdateUserSetting_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateContext(ctx, mux, req, \"/memos.api.v1.UserService/UpdateUserSetting\", runtime.WithHTTPPathPattern(\"/api/v1/{setting.name=users/*/settings/*}\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := request_UserService_UpdateUserSetting_0(annotatedContext, inboundMarshaler, client, req, pathParams)\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_UserService_UpdateUserSetting_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodGet, pattern_UserService_ListUserSettings_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateContext(ctx, mux, req, \"/memos.api.v1.UserService/ListUserSettings\", runtime.WithHTTPPathPattern(\"/api/v1/{parent=users/*}/settings\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := request_UserService_ListUserSettings_0(annotatedContext, inboundMarshaler, client, req, pathParams)\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_UserService_ListUserSettings_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodGet, pattern_UserService_ListPersonalAccessTokens_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateContext(ctx, mux, req, \"/memos.api.v1.UserService/ListPersonalAccessTokens\", runtime.WithHTTPPathPattern(\"/api/v1/{parent=users/*}/personalAccessTokens\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := request_UserService_ListPersonalAccessTokens_0(annotatedContext, inboundMarshaler, client, req, pathParams)\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_UserService_ListPersonalAccessTokens_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodPost, pattern_UserService_CreatePersonalAccessToken_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateContext(ctx, mux, req, \"/memos.api.v1.UserService/CreatePersonalAccessToken\", runtime.WithHTTPPathPattern(\"/api/v1/{parent=users/*}/personalAccessTokens\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := request_UserService_CreatePersonalAccessToken_0(annotatedContext, inboundMarshaler, client, req, pathParams)\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_UserService_CreatePersonalAccessToken_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodDelete, pattern_UserService_DeletePersonalAccessToken_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateContext(ctx, mux, req, \"/memos.api.v1.UserService/DeletePersonalAccessToken\", runtime.WithHTTPPathPattern(\"/api/v1/{name=users/*/personalAccessTokens/*}\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := request_UserService_DeletePersonalAccessToken_0(annotatedContext, inboundMarshaler, client, req, pathParams)\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_UserService_DeletePersonalAccessToken_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodGet, pattern_UserService_ListUserWebhooks_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateContext(ctx, mux, req, \"/memos.api.v1.UserService/ListUserWebhooks\", runtime.WithHTTPPathPattern(\"/api/v1/{parent=users/*}/webhooks\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := request_UserService_ListUserWebhooks_0(annotatedContext, inboundMarshaler, client, req, pathParams)\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_UserService_ListUserWebhooks_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodPost, pattern_UserService_CreateUserWebhook_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateContext(ctx, mux, req, \"/memos.api.v1.UserService/CreateUserWebhook\", runtime.WithHTTPPathPattern(\"/api/v1/{parent=users/*}/webhooks\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := request_UserService_CreateUserWebhook_0(annotatedContext, inboundMarshaler, client, req, pathParams)\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_UserService_CreateUserWebhook_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodPatch, pattern_UserService_UpdateUserWebhook_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateContext(ctx, mux, req, \"/memos.api.v1.UserService/UpdateUserWebhook\", runtime.WithHTTPPathPattern(\"/api/v1/{webhook.name=users/*/webhooks/*}\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := request_UserService_UpdateUserWebhook_0(annotatedContext, inboundMarshaler, client, req, pathParams)\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_UserService_UpdateUserWebhook_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodDelete, pattern_UserService_DeleteUserWebhook_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateContext(ctx, mux, req, \"/memos.api.v1.UserService/DeleteUserWebhook\", runtime.WithHTTPPathPattern(\"/api/v1/{name=users/*/webhooks/*}\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := request_UserService_DeleteUserWebhook_0(annotatedContext, inboundMarshaler, client, req, pathParams)\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_UserService_DeleteUserWebhook_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodGet, pattern_UserService_ListUserNotifications_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateContext(ctx, mux, req, \"/memos.api.v1.UserService/ListUserNotifications\", runtime.WithHTTPPathPattern(\"/api/v1/{parent=users/*}/notifications\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := request_UserService_ListUserNotifications_0(annotatedContext, inboundMarshaler, client, req, pathParams)\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_UserService_ListUserNotifications_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodPatch, pattern_UserService_UpdateUserNotification_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateContext(ctx, mux, req, \"/memos.api.v1.UserService/UpdateUserNotification\", runtime.WithHTTPPathPattern(\"/api/v1/{notification.name=users/*/notifications/*}\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := request_UserService_UpdateUserNotification_0(annotatedContext, inboundMarshaler, client, req, pathParams)\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_UserService_UpdateUserNotification_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\tmux.Handle(http.MethodDelete, pattern_UserService_DeleteUserNotification_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {\n\t\tctx, cancel := context.WithCancel(req.Context())\n\t\tdefer cancel()\n\t\tinboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)\n\t\tannotatedContext, err := runtime.AnnotateContext(ctx, mux, req, \"/memos.api.v1.UserService/DeleteUserNotification\", runtime.WithHTTPPathPattern(\"/api/v1/{name=users/*/notifications/*}\"))\n\t\tif err != nil {\n\t\t\truntime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tresp, md, err := request_UserService_DeleteUserNotification_0(annotatedContext, inboundMarshaler, client, req, pathParams)\n\t\tannotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)\n\t\tif err != nil {\n\t\t\truntime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)\n\t\t\treturn\n\t\t}\n\t\tforward_UserService_DeleteUserNotification_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)\n\t})\n\treturn nil\n}\n\nvar (\n\tpattern_UserService_ListUsers_0                 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{\"api\", \"v1\", \"users\"}, \"\"))\n\tpattern_UserService_GetUser_0                   = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3}, []string{\"api\", \"v1\", \"users\", \"name\"}, \"\"))\n\tpattern_UserService_CreateUser_0                = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{\"api\", \"v1\", \"users\"}, \"\"))\n\tpattern_UserService_UpdateUser_0                = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3}, []string{\"api\", \"v1\", \"users\", \"user.name\"}, \"\"))\n\tpattern_UserService_DeleteUser_0                = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3}, []string{\"api\", \"v1\", \"users\", \"name\"}, \"\"))\n\tpattern_UserService_ListAllUserStats_0          = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{\"api\", \"v1\", \"users\"}, \"stats\"))\n\tpattern_UserService_GetUserStats_0              = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3}, []string{\"api\", \"v1\", \"users\", \"name\"}, \"getStats\"))\n\tpattern_UserService_GetUserSetting_0            = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 2, 3, 1, 0, 4, 4, 5, 4}, []string{\"api\", \"v1\", \"users\", \"settings\", \"name\"}, \"\"))\n\tpattern_UserService_UpdateUserSetting_0         = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 2, 3, 1, 0, 4, 4, 5, 4}, []string{\"api\", \"v1\", \"users\", \"settings\", \"setting.name\"}, \"\"))\n\tpattern_UserService_ListUserSettings_0          = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3, 2, 4}, []string{\"api\", \"v1\", \"users\", \"parent\", \"settings\"}, \"\"))\n\tpattern_UserService_ListPersonalAccessTokens_0  = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3, 2, 4}, []string{\"api\", \"v1\", \"users\", \"parent\", \"personalAccessTokens\"}, \"\"))\n\tpattern_UserService_CreatePersonalAccessToken_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3, 2, 4}, []string{\"api\", \"v1\", \"users\", \"parent\", \"personalAccessTokens\"}, \"\"))\n\tpattern_UserService_DeletePersonalAccessToken_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 2, 3, 1, 0, 4, 4, 5, 4}, []string{\"api\", \"v1\", \"users\", \"personalAccessTokens\", \"name\"}, \"\"))\n\tpattern_UserService_ListUserWebhooks_0          = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3, 2, 4}, []string{\"api\", \"v1\", \"users\", \"parent\", \"webhooks\"}, \"\"))\n\tpattern_UserService_CreateUserWebhook_0         = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3, 2, 4}, []string{\"api\", \"v1\", \"users\", \"parent\", \"webhooks\"}, \"\"))\n\tpattern_UserService_UpdateUserWebhook_0         = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 2, 3, 1, 0, 4, 4, 5, 4}, []string{\"api\", \"v1\", \"users\", \"webhooks\", \"webhook.name\"}, \"\"))\n\tpattern_UserService_DeleteUserWebhook_0         = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 2, 3, 1, 0, 4, 4, 5, 4}, []string{\"api\", \"v1\", \"users\", \"webhooks\", \"name\"}, \"\"))\n\tpattern_UserService_ListUserNotifications_0     = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3, 2, 4}, []string{\"api\", \"v1\", \"users\", \"parent\", \"notifications\"}, \"\"))\n\tpattern_UserService_UpdateUserNotification_0    = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 2, 3, 1, 0, 4, 4, 5, 4}, []string{\"api\", \"v1\", \"users\", \"notifications\", \"notification.name\"}, \"\"))\n\tpattern_UserService_DeleteUserNotification_0    = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 2, 3, 1, 0, 4, 4, 5, 4}, []string{\"api\", \"v1\", \"users\", \"notifications\", \"name\"}, \"\"))\n)\n\nvar (\n\tforward_UserService_ListUsers_0                 = runtime.ForwardResponseMessage\n\tforward_UserService_GetUser_0                   = runtime.ForwardResponseMessage\n\tforward_UserService_CreateUser_0                = runtime.ForwardResponseMessage\n\tforward_UserService_UpdateUser_0                = runtime.ForwardResponseMessage\n\tforward_UserService_DeleteUser_0                = runtime.ForwardResponseMessage\n\tforward_UserService_ListAllUserStats_0          = runtime.ForwardResponseMessage\n\tforward_UserService_GetUserStats_0              = runtime.ForwardResponseMessage\n\tforward_UserService_GetUserSetting_0            = runtime.ForwardResponseMessage\n\tforward_UserService_UpdateUserSetting_0         = runtime.ForwardResponseMessage\n\tforward_UserService_ListUserSettings_0          = runtime.ForwardResponseMessage\n\tforward_UserService_ListPersonalAccessTokens_0  = runtime.ForwardResponseMessage\n\tforward_UserService_CreatePersonalAccessToken_0 = runtime.ForwardResponseMessage\n\tforward_UserService_DeletePersonalAccessToken_0 = runtime.ForwardResponseMessage\n\tforward_UserService_ListUserWebhooks_0          = runtime.ForwardResponseMessage\n\tforward_UserService_CreateUserWebhook_0         = runtime.ForwardResponseMessage\n\tforward_UserService_UpdateUserWebhook_0         = runtime.ForwardResponseMessage\n\tforward_UserService_DeleteUserWebhook_0         = runtime.ForwardResponseMessage\n\tforward_UserService_ListUserNotifications_0     = runtime.ForwardResponseMessage\n\tforward_UserService_UpdateUserNotification_0    = runtime.ForwardResponseMessage\n\tforward_UserService_DeleteUserNotification_0    = runtime.ForwardResponseMessage\n)\n"
  },
  {
    "path": "proto/gen/api/v1/user_service_grpc.pb.go",
    "content": "// Code generated by protoc-gen-go-grpc. DO NOT EDIT.\n// versions:\n// - protoc-gen-go-grpc v1.6.1\n// - protoc             (unknown)\n// source: api/v1/user_service.proto\n\npackage apiv1\n\nimport (\n\tcontext \"context\"\n\tgrpc \"google.golang.org/grpc\"\n\tcodes \"google.golang.org/grpc/codes\"\n\tstatus \"google.golang.org/grpc/status\"\n\temptypb \"google.golang.org/protobuf/types/known/emptypb\"\n)\n\n// This is a compile-time assertion to ensure that this generated file\n// is compatible with the grpc package it is being compiled against.\n// Requires gRPC-Go v1.64.0 or later.\nconst _ = grpc.SupportPackageIsVersion9\n\nconst (\n\tUserService_ListUsers_FullMethodName                 = \"/memos.api.v1.UserService/ListUsers\"\n\tUserService_GetUser_FullMethodName                   = \"/memos.api.v1.UserService/GetUser\"\n\tUserService_CreateUser_FullMethodName                = \"/memos.api.v1.UserService/CreateUser\"\n\tUserService_UpdateUser_FullMethodName                = \"/memos.api.v1.UserService/UpdateUser\"\n\tUserService_DeleteUser_FullMethodName                = \"/memos.api.v1.UserService/DeleteUser\"\n\tUserService_ListAllUserStats_FullMethodName          = \"/memos.api.v1.UserService/ListAllUserStats\"\n\tUserService_GetUserStats_FullMethodName              = \"/memos.api.v1.UserService/GetUserStats\"\n\tUserService_GetUserSetting_FullMethodName            = \"/memos.api.v1.UserService/GetUserSetting\"\n\tUserService_UpdateUserSetting_FullMethodName         = \"/memos.api.v1.UserService/UpdateUserSetting\"\n\tUserService_ListUserSettings_FullMethodName          = \"/memos.api.v1.UserService/ListUserSettings\"\n\tUserService_ListPersonalAccessTokens_FullMethodName  = \"/memos.api.v1.UserService/ListPersonalAccessTokens\"\n\tUserService_CreatePersonalAccessToken_FullMethodName = \"/memos.api.v1.UserService/CreatePersonalAccessToken\"\n\tUserService_DeletePersonalAccessToken_FullMethodName = \"/memos.api.v1.UserService/DeletePersonalAccessToken\"\n\tUserService_ListUserWebhooks_FullMethodName          = \"/memos.api.v1.UserService/ListUserWebhooks\"\n\tUserService_CreateUserWebhook_FullMethodName         = \"/memos.api.v1.UserService/CreateUserWebhook\"\n\tUserService_UpdateUserWebhook_FullMethodName         = \"/memos.api.v1.UserService/UpdateUserWebhook\"\n\tUserService_DeleteUserWebhook_FullMethodName         = \"/memos.api.v1.UserService/DeleteUserWebhook\"\n\tUserService_ListUserNotifications_FullMethodName     = \"/memos.api.v1.UserService/ListUserNotifications\"\n\tUserService_UpdateUserNotification_FullMethodName    = \"/memos.api.v1.UserService/UpdateUserNotification\"\n\tUserService_DeleteUserNotification_FullMethodName    = \"/memos.api.v1.UserService/DeleteUserNotification\"\n)\n\n// UserServiceClient is the client API for UserService service.\n//\n// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.\ntype UserServiceClient interface {\n\t// ListUsers returns a list of users.\n\tListUsers(ctx context.Context, in *ListUsersRequest, opts ...grpc.CallOption) (*ListUsersResponse, error)\n\t// GetUser gets a user by ID or username.\n\t// Supports both numeric IDs and username strings:\n\t//   - users/{id}       (e.g., users/101)\n\t//   - users/{username} (e.g., users/steven)\n\tGetUser(ctx context.Context, in *GetUserRequest, opts ...grpc.CallOption) (*User, error)\n\t// CreateUser creates a new user.\n\tCreateUser(ctx context.Context, in *CreateUserRequest, opts ...grpc.CallOption) (*User, error)\n\t// UpdateUser updates a user.\n\tUpdateUser(ctx context.Context, in *UpdateUserRequest, opts ...grpc.CallOption) (*User, error)\n\t// DeleteUser deletes a user.\n\tDeleteUser(ctx context.Context, in *DeleteUserRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)\n\t// ListAllUserStats returns statistics for all users.\n\tListAllUserStats(ctx context.Context, in *ListAllUserStatsRequest, opts ...grpc.CallOption) (*ListAllUserStatsResponse, error)\n\t// GetUserStats returns statistics for a specific user.\n\tGetUserStats(ctx context.Context, in *GetUserStatsRequest, opts ...grpc.CallOption) (*UserStats, error)\n\t// GetUserSetting returns the user setting.\n\tGetUserSetting(ctx context.Context, in *GetUserSettingRequest, opts ...grpc.CallOption) (*UserSetting, error)\n\t// UpdateUserSetting updates the user setting.\n\tUpdateUserSetting(ctx context.Context, in *UpdateUserSettingRequest, opts ...grpc.CallOption) (*UserSetting, error)\n\t// ListUserSettings returns a list of user settings.\n\tListUserSettings(ctx context.Context, in *ListUserSettingsRequest, opts ...grpc.CallOption) (*ListUserSettingsResponse, error)\n\t// ListPersonalAccessTokens returns a list of Personal Access Tokens (PATs) for a user.\n\t// PATs are long-lived tokens for API/script access, distinct from short-lived JWT access tokens.\n\tListPersonalAccessTokens(ctx context.Context, in *ListPersonalAccessTokensRequest, opts ...grpc.CallOption) (*ListPersonalAccessTokensResponse, error)\n\t// CreatePersonalAccessToken creates a new Personal Access Token for a user.\n\t// The token value is only returned once upon creation.\n\tCreatePersonalAccessToken(ctx context.Context, in *CreatePersonalAccessTokenRequest, opts ...grpc.CallOption) (*CreatePersonalAccessTokenResponse, error)\n\t// DeletePersonalAccessToken deletes a Personal Access Token.\n\tDeletePersonalAccessToken(ctx context.Context, in *DeletePersonalAccessTokenRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)\n\t// ListUserWebhooks returns a list of webhooks for a user.\n\tListUserWebhooks(ctx context.Context, in *ListUserWebhooksRequest, opts ...grpc.CallOption) (*ListUserWebhooksResponse, error)\n\t// CreateUserWebhook creates a new webhook for a user.\n\tCreateUserWebhook(ctx context.Context, in *CreateUserWebhookRequest, opts ...grpc.CallOption) (*UserWebhook, error)\n\t// UpdateUserWebhook updates an existing webhook for a user.\n\tUpdateUserWebhook(ctx context.Context, in *UpdateUserWebhookRequest, opts ...grpc.CallOption) (*UserWebhook, error)\n\t// DeleteUserWebhook deletes a webhook for a user.\n\tDeleteUserWebhook(ctx context.Context, in *DeleteUserWebhookRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)\n\t// ListUserNotifications lists notifications for a user.\n\tListUserNotifications(ctx context.Context, in *ListUserNotificationsRequest, opts ...grpc.CallOption) (*ListUserNotificationsResponse, error)\n\t// UpdateUserNotification updates a notification.\n\tUpdateUserNotification(ctx context.Context, in *UpdateUserNotificationRequest, opts ...grpc.CallOption) (*UserNotification, error)\n\t// DeleteUserNotification deletes a notification.\n\tDeleteUserNotification(ctx context.Context, in *DeleteUserNotificationRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)\n}\n\ntype userServiceClient struct {\n\tcc grpc.ClientConnInterface\n}\n\nfunc NewUserServiceClient(cc grpc.ClientConnInterface) UserServiceClient {\n\treturn &userServiceClient{cc}\n}\n\nfunc (c *userServiceClient) ListUsers(ctx context.Context, in *ListUsersRequest, opts ...grpc.CallOption) (*ListUsersResponse, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(ListUsersResponse)\n\terr := c.cc.Invoke(ctx, UserService_ListUsers_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *userServiceClient) GetUser(ctx context.Context, in *GetUserRequest, opts ...grpc.CallOption) (*User, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(User)\n\terr := c.cc.Invoke(ctx, UserService_GetUser_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *userServiceClient) CreateUser(ctx context.Context, in *CreateUserRequest, opts ...grpc.CallOption) (*User, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(User)\n\terr := c.cc.Invoke(ctx, UserService_CreateUser_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *userServiceClient) UpdateUser(ctx context.Context, in *UpdateUserRequest, opts ...grpc.CallOption) (*User, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(User)\n\terr := c.cc.Invoke(ctx, UserService_UpdateUser_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *userServiceClient) DeleteUser(ctx context.Context, in *DeleteUserRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(emptypb.Empty)\n\terr := c.cc.Invoke(ctx, UserService_DeleteUser_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *userServiceClient) ListAllUserStats(ctx context.Context, in *ListAllUserStatsRequest, opts ...grpc.CallOption) (*ListAllUserStatsResponse, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(ListAllUserStatsResponse)\n\terr := c.cc.Invoke(ctx, UserService_ListAllUserStats_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *userServiceClient) GetUserStats(ctx context.Context, in *GetUserStatsRequest, opts ...grpc.CallOption) (*UserStats, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(UserStats)\n\terr := c.cc.Invoke(ctx, UserService_GetUserStats_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *userServiceClient) GetUserSetting(ctx context.Context, in *GetUserSettingRequest, opts ...grpc.CallOption) (*UserSetting, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(UserSetting)\n\terr := c.cc.Invoke(ctx, UserService_GetUserSetting_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *userServiceClient) UpdateUserSetting(ctx context.Context, in *UpdateUserSettingRequest, opts ...grpc.CallOption) (*UserSetting, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(UserSetting)\n\terr := c.cc.Invoke(ctx, UserService_UpdateUserSetting_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *userServiceClient) ListUserSettings(ctx context.Context, in *ListUserSettingsRequest, opts ...grpc.CallOption) (*ListUserSettingsResponse, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(ListUserSettingsResponse)\n\terr := c.cc.Invoke(ctx, UserService_ListUserSettings_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *userServiceClient) ListPersonalAccessTokens(ctx context.Context, in *ListPersonalAccessTokensRequest, opts ...grpc.CallOption) (*ListPersonalAccessTokensResponse, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(ListPersonalAccessTokensResponse)\n\terr := c.cc.Invoke(ctx, UserService_ListPersonalAccessTokens_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *userServiceClient) CreatePersonalAccessToken(ctx context.Context, in *CreatePersonalAccessTokenRequest, opts ...grpc.CallOption) (*CreatePersonalAccessTokenResponse, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(CreatePersonalAccessTokenResponse)\n\terr := c.cc.Invoke(ctx, UserService_CreatePersonalAccessToken_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *userServiceClient) DeletePersonalAccessToken(ctx context.Context, in *DeletePersonalAccessTokenRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(emptypb.Empty)\n\terr := c.cc.Invoke(ctx, UserService_DeletePersonalAccessToken_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *userServiceClient) ListUserWebhooks(ctx context.Context, in *ListUserWebhooksRequest, opts ...grpc.CallOption) (*ListUserWebhooksResponse, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(ListUserWebhooksResponse)\n\terr := c.cc.Invoke(ctx, UserService_ListUserWebhooks_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *userServiceClient) CreateUserWebhook(ctx context.Context, in *CreateUserWebhookRequest, opts ...grpc.CallOption) (*UserWebhook, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(UserWebhook)\n\terr := c.cc.Invoke(ctx, UserService_CreateUserWebhook_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *userServiceClient) UpdateUserWebhook(ctx context.Context, in *UpdateUserWebhookRequest, opts ...grpc.CallOption) (*UserWebhook, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(UserWebhook)\n\terr := c.cc.Invoke(ctx, UserService_UpdateUserWebhook_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *userServiceClient) DeleteUserWebhook(ctx context.Context, in *DeleteUserWebhookRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(emptypb.Empty)\n\terr := c.cc.Invoke(ctx, UserService_DeleteUserWebhook_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *userServiceClient) ListUserNotifications(ctx context.Context, in *ListUserNotificationsRequest, opts ...grpc.CallOption) (*ListUserNotificationsResponse, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(ListUserNotificationsResponse)\n\terr := c.cc.Invoke(ctx, UserService_ListUserNotifications_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *userServiceClient) UpdateUserNotification(ctx context.Context, in *UpdateUserNotificationRequest, opts ...grpc.CallOption) (*UserNotification, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(UserNotification)\n\terr := c.cc.Invoke(ctx, UserService_UpdateUserNotification_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *userServiceClient) DeleteUserNotification(ctx context.Context, in *DeleteUserNotificationRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(emptypb.Empty)\n\terr := c.cc.Invoke(ctx, UserService_DeleteUserNotification_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\n// UserServiceServer is the server API for UserService service.\n// All implementations must embed UnimplementedUserServiceServer\n// for forward compatibility.\ntype UserServiceServer interface {\n\t// ListUsers returns a list of users.\n\tListUsers(context.Context, *ListUsersRequest) (*ListUsersResponse, error)\n\t// GetUser gets a user by ID or username.\n\t// Supports both numeric IDs and username strings:\n\t//   - users/{id}       (e.g., users/101)\n\t//   - users/{username} (e.g., users/steven)\n\tGetUser(context.Context, *GetUserRequest) (*User, error)\n\t// CreateUser creates a new user.\n\tCreateUser(context.Context, *CreateUserRequest) (*User, error)\n\t// UpdateUser updates a user.\n\tUpdateUser(context.Context, *UpdateUserRequest) (*User, error)\n\t// DeleteUser deletes a user.\n\tDeleteUser(context.Context, *DeleteUserRequest) (*emptypb.Empty, error)\n\t// ListAllUserStats returns statistics for all users.\n\tListAllUserStats(context.Context, *ListAllUserStatsRequest) (*ListAllUserStatsResponse, error)\n\t// GetUserStats returns statistics for a specific user.\n\tGetUserStats(context.Context, *GetUserStatsRequest) (*UserStats, error)\n\t// GetUserSetting returns the user setting.\n\tGetUserSetting(context.Context, *GetUserSettingRequest) (*UserSetting, error)\n\t// UpdateUserSetting updates the user setting.\n\tUpdateUserSetting(context.Context, *UpdateUserSettingRequest) (*UserSetting, error)\n\t// ListUserSettings returns a list of user settings.\n\tListUserSettings(context.Context, *ListUserSettingsRequest) (*ListUserSettingsResponse, error)\n\t// ListPersonalAccessTokens returns a list of Personal Access Tokens (PATs) for a user.\n\t// PATs are long-lived tokens for API/script access, distinct from short-lived JWT access tokens.\n\tListPersonalAccessTokens(context.Context, *ListPersonalAccessTokensRequest) (*ListPersonalAccessTokensResponse, error)\n\t// CreatePersonalAccessToken creates a new Personal Access Token for a user.\n\t// The token value is only returned once upon creation.\n\tCreatePersonalAccessToken(context.Context, *CreatePersonalAccessTokenRequest) (*CreatePersonalAccessTokenResponse, error)\n\t// DeletePersonalAccessToken deletes a Personal Access Token.\n\tDeletePersonalAccessToken(context.Context, *DeletePersonalAccessTokenRequest) (*emptypb.Empty, error)\n\t// ListUserWebhooks returns a list of webhooks for a user.\n\tListUserWebhooks(context.Context, *ListUserWebhooksRequest) (*ListUserWebhooksResponse, error)\n\t// CreateUserWebhook creates a new webhook for a user.\n\tCreateUserWebhook(context.Context, *CreateUserWebhookRequest) (*UserWebhook, error)\n\t// UpdateUserWebhook updates an existing webhook for a user.\n\tUpdateUserWebhook(context.Context, *UpdateUserWebhookRequest) (*UserWebhook, error)\n\t// DeleteUserWebhook deletes a webhook for a user.\n\tDeleteUserWebhook(context.Context, *DeleteUserWebhookRequest) (*emptypb.Empty, error)\n\t// ListUserNotifications lists notifications for a user.\n\tListUserNotifications(context.Context, *ListUserNotificationsRequest) (*ListUserNotificationsResponse, error)\n\t// UpdateUserNotification updates a notification.\n\tUpdateUserNotification(context.Context, *UpdateUserNotificationRequest) (*UserNotification, error)\n\t// DeleteUserNotification deletes a notification.\n\tDeleteUserNotification(context.Context, *DeleteUserNotificationRequest) (*emptypb.Empty, error)\n\tmustEmbedUnimplementedUserServiceServer()\n}\n\n// UnimplementedUserServiceServer must be embedded to have\n// forward compatible implementations.\n//\n// NOTE: this should be embedded by value instead of pointer to avoid a nil\n// pointer dereference when methods are called.\ntype UnimplementedUserServiceServer struct{}\n\nfunc (UnimplementedUserServiceServer) ListUsers(context.Context, *ListUsersRequest) (*ListUsersResponse, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method ListUsers not implemented\")\n}\nfunc (UnimplementedUserServiceServer) GetUser(context.Context, *GetUserRequest) (*User, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method GetUser not implemented\")\n}\nfunc (UnimplementedUserServiceServer) CreateUser(context.Context, *CreateUserRequest) (*User, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method CreateUser not implemented\")\n}\nfunc (UnimplementedUserServiceServer) UpdateUser(context.Context, *UpdateUserRequest) (*User, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method UpdateUser not implemented\")\n}\nfunc (UnimplementedUserServiceServer) DeleteUser(context.Context, *DeleteUserRequest) (*emptypb.Empty, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method DeleteUser not implemented\")\n}\nfunc (UnimplementedUserServiceServer) ListAllUserStats(context.Context, *ListAllUserStatsRequest) (*ListAllUserStatsResponse, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method ListAllUserStats not implemented\")\n}\nfunc (UnimplementedUserServiceServer) GetUserStats(context.Context, *GetUserStatsRequest) (*UserStats, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method GetUserStats not implemented\")\n}\nfunc (UnimplementedUserServiceServer) GetUserSetting(context.Context, *GetUserSettingRequest) (*UserSetting, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method GetUserSetting not implemented\")\n}\nfunc (UnimplementedUserServiceServer) UpdateUserSetting(context.Context, *UpdateUserSettingRequest) (*UserSetting, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method UpdateUserSetting not implemented\")\n}\nfunc (UnimplementedUserServiceServer) ListUserSettings(context.Context, *ListUserSettingsRequest) (*ListUserSettingsResponse, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method ListUserSettings not implemented\")\n}\nfunc (UnimplementedUserServiceServer) ListPersonalAccessTokens(context.Context, *ListPersonalAccessTokensRequest) (*ListPersonalAccessTokensResponse, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method ListPersonalAccessTokens not implemented\")\n}\nfunc (UnimplementedUserServiceServer) CreatePersonalAccessToken(context.Context, *CreatePersonalAccessTokenRequest) (*CreatePersonalAccessTokenResponse, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method CreatePersonalAccessToken not implemented\")\n}\nfunc (UnimplementedUserServiceServer) DeletePersonalAccessToken(context.Context, *DeletePersonalAccessTokenRequest) (*emptypb.Empty, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method DeletePersonalAccessToken not implemented\")\n}\nfunc (UnimplementedUserServiceServer) ListUserWebhooks(context.Context, *ListUserWebhooksRequest) (*ListUserWebhooksResponse, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method ListUserWebhooks not implemented\")\n}\nfunc (UnimplementedUserServiceServer) CreateUserWebhook(context.Context, *CreateUserWebhookRequest) (*UserWebhook, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method CreateUserWebhook not implemented\")\n}\nfunc (UnimplementedUserServiceServer) UpdateUserWebhook(context.Context, *UpdateUserWebhookRequest) (*UserWebhook, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method UpdateUserWebhook not implemented\")\n}\nfunc (UnimplementedUserServiceServer) DeleteUserWebhook(context.Context, *DeleteUserWebhookRequest) (*emptypb.Empty, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method DeleteUserWebhook not implemented\")\n}\nfunc (UnimplementedUserServiceServer) ListUserNotifications(context.Context, *ListUserNotificationsRequest) (*ListUserNotificationsResponse, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method ListUserNotifications not implemented\")\n}\nfunc (UnimplementedUserServiceServer) UpdateUserNotification(context.Context, *UpdateUserNotificationRequest) (*UserNotification, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method UpdateUserNotification not implemented\")\n}\nfunc (UnimplementedUserServiceServer) DeleteUserNotification(context.Context, *DeleteUserNotificationRequest) (*emptypb.Empty, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method DeleteUserNotification not implemented\")\n}\nfunc (UnimplementedUserServiceServer) mustEmbedUnimplementedUserServiceServer() {}\nfunc (UnimplementedUserServiceServer) testEmbeddedByValue()                     {}\n\n// UnsafeUserServiceServer may be embedded to opt out of forward compatibility for this service.\n// Use of this interface is not recommended, as added methods to UserServiceServer will\n// result in compilation errors.\ntype UnsafeUserServiceServer interface {\n\tmustEmbedUnimplementedUserServiceServer()\n}\n\nfunc RegisterUserServiceServer(s grpc.ServiceRegistrar, srv UserServiceServer) {\n\t// If the following call panics, it indicates UnimplementedUserServiceServer was\n\t// embedded by pointer and is nil.  This will cause panics if an\n\t// unimplemented method is ever invoked, so we test this at initialization\n\t// time to prevent it from happening at runtime later due to I/O.\n\tif t, ok := srv.(interface{ testEmbeddedByValue() }); ok {\n\t\tt.testEmbeddedByValue()\n\t}\n\ts.RegisterService(&UserService_ServiceDesc, srv)\n}\n\nfunc _UserService_ListUsers_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(ListUsersRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(UserServiceServer).ListUsers(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: UserService_ListUsers_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(UserServiceServer).ListUsers(ctx, req.(*ListUsersRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _UserService_GetUser_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(GetUserRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(UserServiceServer).GetUser(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: UserService_GetUser_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(UserServiceServer).GetUser(ctx, req.(*GetUserRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _UserService_CreateUser_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(CreateUserRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(UserServiceServer).CreateUser(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: UserService_CreateUser_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(UserServiceServer).CreateUser(ctx, req.(*CreateUserRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _UserService_UpdateUser_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(UpdateUserRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(UserServiceServer).UpdateUser(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: UserService_UpdateUser_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(UserServiceServer).UpdateUser(ctx, req.(*UpdateUserRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _UserService_DeleteUser_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(DeleteUserRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(UserServiceServer).DeleteUser(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: UserService_DeleteUser_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(UserServiceServer).DeleteUser(ctx, req.(*DeleteUserRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _UserService_ListAllUserStats_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(ListAllUserStatsRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(UserServiceServer).ListAllUserStats(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: UserService_ListAllUserStats_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(UserServiceServer).ListAllUserStats(ctx, req.(*ListAllUserStatsRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _UserService_GetUserStats_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(GetUserStatsRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(UserServiceServer).GetUserStats(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: UserService_GetUserStats_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(UserServiceServer).GetUserStats(ctx, req.(*GetUserStatsRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _UserService_GetUserSetting_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(GetUserSettingRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(UserServiceServer).GetUserSetting(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: UserService_GetUserSetting_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(UserServiceServer).GetUserSetting(ctx, req.(*GetUserSettingRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _UserService_UpdateUserSetting_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(UpdateUserSettingRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(UserServiceServer).UpdateUserSetting(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: UserService_UpdateUserSetting_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(UserServiceServer).UpdateUserSetting(ctx, req.(*UpdateUserSettingRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _UserService_ListUserSettings_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(ListUserSettingsRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(UserServiceServer).ListUserSettings(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: UserService_ListUserSettings_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(UserServiceServer).ListUserSettings(ctx, req.(*ListUserSettingsRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _UserService_ListPersonalAccessTokens_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(ListPersonalAccessTokensRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(UserServiceServer).ListPersonalAccessTokens(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: UserService_ListPersonalAccessTokens_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(UserServiceServer).ListPersonalAccessTokens(ctx, req.(*ListPersonalAccessTokensRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _UserService_CreatePersonalAccessToken_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(CreatePersonalAccessTokenRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(UserServiceServer).CreatePersonalAccessToken(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: UserService_CreatePersonalAccessToken_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(UserServiceServer).CreatePersonalAccessToken(ctx, req.(*CreatePersonalAccessTokenRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _UserService_DeletePersonalAccessToken_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(DeletePersonalAccessTokenRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(UserServiceServer).DeletePersonalAccessToken(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: UserService_DeletePersonalAccessToken_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(UserServiceServer).DeletePersonalAccessToken(ctx, req.(*DeletePersonalAccessTokenRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _UserService_ListUserWebhooks_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(ListUserWebhooksRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(UserServiceServer).ListUserWebhooks(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: UserService_ListUserWebhooks_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(UserServiceServer).ListUserWebhooks(ctx, req.(*ListUserWebhooksRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _UserService_CreateUserWebhook_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(CreateUserWebhookRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(UserServiceServer).CreateUserWebhook(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: UserService_CreateUserWebhook_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(UserServiceServer).CreateUserWebhook(ctx, req.(*CreateUserWebhookRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _UserService_UpdateUserWebhook_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(UpdateUserWebhookRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(UserServiceServer).UpdateUserWebhook(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: UserService_UpdateUserWebhook_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(UserServiceServer).UpdateUserWebhook(ctx, req.(*UpdateUserWebhookRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _UserService_DeleteUserWebhook_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(DeleteUserWebhookRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(UserServiceServer).DeleteUserWebhook(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: UserService_DeleteUserWebhook_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(UserServiceServer).DeleteUserWebhook(ctx, req.(*DeleteUserWebhookRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _UserService_ListUserNotifications_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(ListUserNotificationsRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(UserServiceServer).ListUserNotifications(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: UserService_ListUserNotifications_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(UserServiceServer).ListUserNotifications(ctx, req.(*ListUserNotificationsRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _UserService_UpdateUserNotification_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(UpdateUserNotificationRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(UserServiceServer).UpdateUserNotification(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: UserService_UpdateUserNotification_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(UserServiceServer).UpdateUserNotification(ctx, req.(*UpdateUserNotificationRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _UserService_DeleteUserNotification_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(DeleteUserNotificationRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(UserServiceServer).DeleteUserNotification(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: UserService_DeleteUserNotification_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(UserServiceServer).DeleteUserNotification(ctx, req.(*DeleteUserNotificationRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\n// UserService_ServiceDesc is the grpc.ServiceDesc for UserService service.\n// It's only intended for direct use with grpc.RegisterService,\n// and not to be introspected or modified (even as a copy)\nvar UserService_ServiceDesc = grpc.ServiceDesc{\n\tServiceName: \"memos.api.v1.UserService\",\n\tHandlerType: (*UserServiceServer)(nil),\n\tMethods: []grpc.MethodDesc{\n\t\t{\n\t\t\tMethodName: \"ListUsers\",\n\t\t\tHandler:    _UserService_ListUsers_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"GetUser\",\n\t\t\tHandler:    _UserService_GetUser_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"CreateUser\",\n\t\t\tHandler:    _UserService_CreateUser_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"UpdateUser\",\n\t\t\tHandler:    _UserService_UpdateUser_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"DeleteUser\",\n\t\t\tHandler:    _UserService_DeleteUser_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"ListAllUserStats\",\n\t\t\tHandler:    _UserService_ListAllUserStats_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"GetUserStats\",\n\t\t\tHandler:    _UserService_GetUserStats_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"GetUserSetting\",\n\t\t\tHandler:    _UserService_GetUserSetting_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"UpdateUserSetting\",\n\t\t\tHandler:    _UserService_UpdateUserSetting_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"ListUserSettings\",\n\t\t\tHandler:    _UserService_ListUserSettings_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"ListPersonalAccessTokens\",\n\t\t\tHandler:    _UserService_ListPersonalAccessTokens_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"CreatePersonalAccessToken\",\n\t\t\tHandler:    _UserService_CreatePersonalAccessToken_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"DeletePersonalAccessToken\",\n\t\t\tHandler:    _UserService_DeletePersonalAccessToken_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"ListUserWebhooks\",\n\t\t\tHandler:    _UserService_ListUserWebhooks_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"CreateUserWebhook\",\n\t\t\tHandler:    _UserService_CreateUserWebhook_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"UpdateUserWebhook\",\n\t\t\tHandler:    _UserService_UpdateUserWebhook_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"DeleteUserWebhook\",\n\t\t\tHandler:    _UserService_DeleteUserWebhook_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"ListUserNotifications\",\n\t\t\tHandler:    _UserService_ListUserNotifications_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"UpdateUserNotification\",\n\t\t\tHandler:    _UserService_UpdateUserNotification_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"DeleteUserNotification\",\n\t\t\tHandler:    _UserService_DeleteUserNotification_Handler,\n\t\t},\n\t},\n\tStreams:  []grpc.StreamDesc{},\n\tMetadata: \"api/v1/user_service.proto\",\n}\n"
  },
  {
    "path": "proto/gen/openapi.yaml",
    "content": "# Generated with protoc-gen-openapi\n# https://github.com/google/gnostic/tree/master/cmd/protoc-gen-openapi\n\nopenapi: 3.0.3\ninfo:\n    title: \"\"\n    version: 0.0.1\npaths:\n    /api/v1/attachments:\n        get:\n            tags:\n                - AttachmentService\n            description: ListAttachments lists all attachments.\n            operationId: AttachmentService_ListAttachments\n            parameters:\n                - name: pageSize\n                  in: query\n                  description: |-\n                    Optional. The maximum number of attachments to return.\n                     The service may return fewer than this value.\n                     If unspecified, at most 50 attachments will be returned.\n                     The maximum value is 1000; values above 1000 will be coerced to 1000.\n                  schema:\n                    type: integer\n                    format: int32\n                - name: pageToken\n                  in: query\n                  description: |-\n                    Optional. A page token, received from a previous `ListAttachments` call.\n                     Provide this to retrieve the subsequent page.\n                  schema:\n                    type: string\n                - name: filter\n                  in: query\n                  description: |-\n                    Optional. Filter to apply to the list results.\n                     Example: \"mime_type==\\\"image/png\\\"\" or \"filename.contains(\\\"test\\\")\"\n                     Supported operators: =, !=, <, <=, >, >=, : (contains), in\n                     Supported fields: filename, mime_type, create_time, memo\n                  schema:\n                    type: string\n                - name: orderBy\n                  in: query\n                  description: |-\n                    Optional. The order to sort results by.\n                     Example: \"create_time desc\" or \"filename asc\"\n                  schema:\n                    type: string\n            responses:\n                \"200\":\n                    description: OK\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/ListAttachmentsResponse'\n                default:\n                    description: Default error response\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/Status'\n        post:\n            tags:\n                - AttachmentService\n            description: CreateAttachment creates a new attachment.\n            operationId: AttachmentService_CreateAttachment\n            parameters:\n                - name: attachmentId\n                  in: query\n                  description: |-\n                    Optional. The attachment ID to use for this attachment.\n                     If empty, a unique ID will be generated.\n                  schema:\n                    type: string\n            requestBody:\n                content:\n                    application/json:\n                        schema:\n                            $ref: '#/components/schemas/Attachment'\n                required: true\n            responses:\n                \"200\":\n                    description: OK\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/Attachment'\n                default:\n                    description: Default error response\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/Status'\n    /api/v1/attachments/{attachment}:\n        get:\n            tags:\n                - AttachmentService\n            description: GetAttachment returns an attachment by name.\n            operationId: AttachmentService_GetAttachment\n            parameters:\n                - name: attachment\n                  in: path\n                  description: The attachment id.\n                  required: true\n                  schema:\n                    type: string\n            responses:\n                \"200\":\n                    description: OK\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/Attachment'\n                default:\n                    description: Default error response\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/Status'\n        delete:\n            tags:\n                - AttachmentService\n            description: DeleteAttachment deletes an attachment by name.\n            operationId: AttachmentService_DeleteAttachment\n            parameters:\n                - name: attachment\n                  in: path\n                  description: The attachment id.\n                  required: true\n                  schema:\n                    type: string\n            responses:\n                \"200\":\n                    description: OK\n                    content: {}\n                default:\n                    description: Default error response\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/Status'\n        patch:\n            tags:\n                - AttachmentService\n            description: UpdateAttachment updates an attachment.\n            operationId: AttachmentService_UpdateAttachment\n            parameters:\n                - name: attachment\n                  in: path\n                  description: The attachment id.\n                  required: true\n                  schema:\n                    type: string\n                - name: updateMask\n                  in: query\n                  description: Required. The list of fields to update.\n                  schema:\n                    type: string\n                    format: field-mask\n            requestBody:\n                content:\n                    application/json:\n                        schema:\n                            $ref: '#/components/schemas/Attachment'\n                required: true\n            responses:\n                \"200\":\n                    description: OK\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/Attachment'\n                default:\n                    description: Default error response\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/Status'\n    /api/v1/auth/me:\n        get:\n            tags:\n                - AuthService\n            description: |-\n                GetCurrentUser returns the authenticated user's information.\n                 Validates the access token and returns user details.\n                 Similar to OIDC's /userinfo endpoint.\n            operationId: AuthService_GetCurrentUser\n            responses:\n                \"200\":\n                    description: OK\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/GetCurrentUserResponse'\n                default:\n                    description: Default error response\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/Status'\n    /api/v1/auth/refresh:\n        post:\n            tags:\n                - AuthService\n            description: |-\n                RefreshToken exchanges a valid refresh token for a new access token.\n                 The refresh token is read from the HttpOnly cookie.\n                 Returns a new short-lived access token.\n            operationId: AuthService_RefreshToken\n            requestBody:\n                content:\n                    application/json:\n                        schema:\n                            $ref: '#/components/schemas/RefreshTokenRequest'\n                required: true\n            responses:\n                \"200\":\n                    description: OK\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/RefreshTokenResponse'\n                default:\n                    description: Default error response\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/Status'\n    /api/v1/auth/signin:\n        post:\n            tags:\n                - AuthService\n            description: |-\n                SignIn authenticates a user with credentials and returns tokens.\n                 On success, returns an access token and sets a refresh token cookie.\n                 Supports password-based and SSO authentication methods.\n            operationId: AuthService_SignIn\n            requestBody:\n                content:\n                    application/json:\n                        schema:\n                            $ref: '#/components/schemas/SignInRequest'\n                required: true\n            responses:\n                \"200\":\n                    description: OK\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/SignInResponse'\n                default:\n                    description: Default error response\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/Status'\n    /api/v1/auth/signout:\n        post:\n            tags:\n                - AuthService\n            description: |-\n                SignOut terminates the user's authentication.\n                 Revokes the refresh token and clears the authentication cookie.\n            operationId: AuthService_SignOut\n            responses:\n                \"200\":\n                    description: OK\n                    content: {}\n                default:\n                    description: Default error response\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/Status'\n    /api/v1/identity-providers:\n        get:\n            tags:\n                - IdentityProviderService\n            description: ListIdentityProviders lists identity providers.\n            operationId: IdentityProviderService_ListIdentityProviders\n            responses:\n                \"200\":\n                    description: OK\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/ListIdentityProvidersResponse'\n                default:\n                    description: Default error response\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/Status'\n        post:\n            tags:\n                - IdentityProviderService\n            description: CreateIdentityProvider creates an identity provider.\n            operationId: IdentityProviderService_CreateIdentityProvider\n            parameters:\n                - name: identityProviderId\n                  in: query\n                  description: |-\n                    Optional. The ID to use for the identity provider, which will become the final component of the resource name.\n                     If not provided, the system will generate one.\n                  schema:\n                    type: string\n            requestBody:\n                content:\n                    application/json:\n                        schema:\n                            $ref: '#/components/schemas/IdentityProvider'\n                required: true\n            responses:\n                \"200\":\n                    description: OK\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/IdentityProvider'\n                default:\n                    description: Default error response\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/Status'\n    /api/v1/identity-providers/{identity-provider}:\n        get:\n            tags:\n                - IdentityProviderService\n            description: GetIdentityProvider gets an identity provider.\n            operationId: IdentityProviderService_GetIdentityProvider\n            parameters:\n                - name: identity-provider\n                  in: path\n                  description: The identity-provider id.\n                  required: true\n                  schema:\n                    type: string\n            responses:\n                \"200\":\n                    description: OK\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/IdentityProvider'\n                default:\n                    description: Default error response\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/Status'\n        delete:\n            tags:\n                - IdentityProviderService\n            description: DeleteIdentityProvider deletes an identity provider.\n            operationId: IdentityProviderService_DeleteIdentityProvider\n            parameters:\n                - name: identity-provider\n                  in: path\n                  description: The identity-provider id.\n                  required: true\n                  schema:\n                    type: string\n            responses:\n                \"200\":\n                    description: OK\n                    content: {}\n                default:\n                    description: Default error response\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/Status'\n        patch:\n            tags:\n                - IdentityProviderService\n            description: UpdateIdentityProvider updates an identity provider.\n            operationId: IdentityProviderService_UpdateIdentityProvider\n            parameters:\n                - name: identity-provider\n                  in: path\n                  description: The identity-provider id.\n                  required: true\n                  schema:\n                    type: string\n                - name: updateMask\n                  in: query\n                  description: |-\n                    Required. The update mask applies to the resource. Only the top level fields of\n                     IdentityProvider are supported.\n                  schema:\n                    type: string\n                    format: field-mask\n            requestBody:\n                content:\n                    application/json:\n                        schema:\n                            $ref: '#/components/schemas/IdentityProvider'\n                required: true\n            responses:\n                \"200\":\n                    description: OK\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/IdentityProvider'\n                default:\n                    description: Default error response\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/Status'\n    /api/v1/instance/profile:\n        get:\n            tags:\n                - InstanceService\n            description: Gets the instance profile.\n            operationId: InstanceService_GetInstanceProfile\n            responses:\n                \"200\":\n                    description: OK\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/InstanceProfile'\n                default:\n                    description: Default error response\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/Status'\n    /api/v1/instance/{instance}/*:\n        get:\n            tags:\n                - InstanceService\n            description: Gets an instance setting.\n            operationId: InstanceService_GetInstanceSetting\n            parameters:\n                - name: instance\n                  in: path\n                  description: The instance id.\n                  required: true\n                  schema:\n                    type: string\n            responses:\n                \"200\":\n                    description: OK\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/InstanceSetting'\n                default:\n                    description: Default error response\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/Status'\n        patch:\n            tags:\n                - InstanceService\n            description: Updates an instance setting.\n            operationId: InstanceService_UpdateInstanceSetting\n            parameters:\n                - name: instance\n                  in: path\n                  description: The instance id.\n                  required: true\n                  schema:\n                    type: string\n                - name: updateMask\n                  in: query\n                  description: The list of fields to update.\n                  schema:\n                    type: string\n                    format: field-mask\n            requestBody:\n                content:\n                    application/json:\n                        schema:\n                            $ref: '#/components/schemas/InstanceSetting'\n                required: true\n            responses:\n                \"200\":\n                    description: OK\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/InstanceSetting'\n                default:\n                    description: Default error response\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/Status'\n    /api/v1/memos:\n        get:\n            tags:\n                - MemoService\n            description: ListMemos lists memos with pagination and filter.\n            operationId: MemoService_ListMemos\n            parameters:\n                - name: pageSize\n                  in: query\n                  description: |-\n                    Optional. The maximum number of memos to return.\n                     The service may return fewer than this value.\n                     If unspecified, at most 50 memos will be returned.\n                     The maximum value is 1000; values above 1000 will be coerced to 1000.\n                  schema:\n                    type: integer\n                    format: int32\n                - name: pageToken\n                  in: query\n                  description: |-\n                    Optional. A page token, received from a previous `ListMemos` call.\n                     Provide this to retrieve the subsequent page.\n                  schema:\n                    type: string\n                - name: state\n                  in: query\n                  description: |-\n                    Optional. The state of the memos to list.\n                     Default to `NORMAL`. Set to `ARCHIVED` to list archived memos.\n                  schema:\n                    enum:\n                        - STATE_UNSPECIFIED\n                        - NORMAL\n                        - ARCHIVED\n                    type: string\n                    format: enum\n                - name: orderBy\n                  in: query\n                  description: |-\n                    Optional. The order to sort results by.\n                     Default to \"display_time desc\".\n                     Supports comma-separated list of fields following AIP-132.\n                     Example: \"pinned desc, display_time desc\" or \"create_time asc\"\n                     Supported fields: pinned, display_time, create_time, update_time, name\n                  schema:\n                    type: string\n                - name: filter\n                  in: query\n                  description: |-\n                    Optional. Filter to apply to the list results.\n                     Filter is a CEL expression to filter memos.\n                     Refer to `Shortcut.filter`.\n                  schema:\n                    type: string\n                - name: showDeleted\n                  in: query\n                  description: Optional. If true, show deleted memos in the response.\n                  schema:\n                    type: boolean\n            responses:\n                \"200\":\n                    description: OK\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/ListMemosResponse'\n                default:\n                    description: Default error response\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/Status'\n        post:\n            tags:\n                - MemoService\n            description: CreateMemo creates a memo.\n            operationId: MemoService_CreateMemo\n            parameters:\n                - name: memoId\n                  in: query\n                  description: |-\n                    Optional. The memo ID to use for this memo.\n                     If empty, a unique ID will be generated.\n                  schema:\n                    type: string\n            requestBody:\n                content:\n                    application/json:\n                        schema:\n                            $ref: '#/components/schemas/Memo'\n                required: true\n            responses:\n                \"200\":\n                    description: OK\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/Memo'\n                default:\n                    description: Default error response\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/Status'\n    /api/v1/memos/{memo}:\n        get:\n            tags:\n                - MemoService\n            description: GetMemo gets a memo.\n            operationId: MemoService_GetMemo\n            parameters:\n                - name: memo\n                  in: path\n                  description: The memo id.\n                  required: true\n                  schema:\n                    type: string\n            responses:\n                \"200\":\n                    description: OK\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/Memo'\n                default:\n                    description: Default error response\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/Status'\n        delete:\n            tags:\n                - MemoService\n            description: DeleteMemo deletes a memo.\n            operationId: MemoService_DeleteMemo\n            parameters:\n                - name: memo\n                  in: path\n                  description: The memo id.\n                  required: true\n                  schema:\n                    type: string\n                - name: force\n                  in: query\n                  description: Optional. If set to true, the memo will be deleted even if it has associated data.\n                  schema:\n                    type: boolean\n            responses:\n                \"200\":\n                    description: OK\n                    content: {}\n                default:\n                    description: Default error response\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/Status'\n        patch:\n            tags:\n                - MemoService\n            description: UpdateMemo updates a memo.\n            operationId: MemoService_UpdateMemo\n            parameters:\n                - name: memo\n                  in: path\n                  description: The memo id.\n                  required: true\n                  schema:\n                    type: string\n                - name: updateMask\n                  in: query\n                  description: Required. The list of fields to update.\n                  schema:\n                    type: string\n                    format: field-mask\n            requestBody:\n                content:\n                    application/json:\n                        schema:\n                            $ref: '#/components/schemas/Memo'\n                required: true\n            responses:\n                \"200\":\n                    description: OK\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/Memo'\n                default:\n                    description: Default error response\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/Status'\n    /api/v1/memos/{memo}/attachments:\n        get:\n            tags:\n                - MemoService\n            description: ListMemoAttachments lists attachments for a memo.\n            operationId: MemoService_ListMemoAttachments\n            parameters:\n                - name: memo\n                  in: path\n                  description: The memo id.\n                  required: true\n                  schema:\n                    type: string\n                - name: pageSize\n                  in: query\n                  description: Optional. The maximum number of attachments to return.\n                  schema:\n                    type: integer\n                    format: int32\n                - name: pageToken\n                  in: query\n                  description: Optional. A page token for pagination.\n                  schema:\n                    type: string\n            responses:\n                \"200\":\n                    description: OK\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/ListMemoAttachmentsResponse'\n                default:\n                    description: Default error response\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/Status'\n        patch:\n            tags:\n                - MemoService\n            description: SetMemoAttachments sets attachments for a memo.\n            operationId: MemoService_SetMemoAttachments\n            parameters:\n                - name: memo\n                  in: path\n                  description: The memo id.\n                  required: true\n                  schema:\n                    type: string\n            requestBody:\n                content:\n                    application/json:\n                        schema:\n                            $ref: '#/components/schemas/SetMemoAttachmentsRequest'\n                required: true\n            responses:\n                \"200\":\n                    description: OK\n                    content: {}\n                default:\n                    description: Default error response\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/Status'\n    /api/v1/memos/{memo}/comments:\n        get:\n            tags:\n                - MemoService\n            description: ListMemoComments lists comments for a memo.\n            operationId: MemoService_ListMemoComments\n            parameters:\n                - name: memo\n                  in: path\n                  description: The memo id.\n                  required: true\n                  schema:\n                    type: string\n                - name: pageSize\n                  in: query\n                  description: Optional. The maximum number of comments to return.\n                  schema:\n                    type: integer\n                    format: int32\n                - name: pageToken\n                  in: query\n                  description: Optional. A page token for pagination.\n                  schema:\n                    type: string\n                - name: orderBy\n                  in: query\n                  description: Optional. The order to sort results by.\n                  schema:\n                    type: string\n            responses:\n                \"200\":\n                    description: OK\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/ListMemoCommentsResponse'\n                default:\n                    description: Default error response\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/Status'\n        post:\n            tags:\n                - MemoService\n            description: CreateMemoComment creates a comment for a memo.\n            operationId: MemoService_CreateMemoComment\n            parameters:\n                - name: memo\n                  in: path\n                  description: The memo id.\n                  required: true\n                  schema:\n                    type: string\n                - name: commentId\n                  in: query\n                  description: Optional. The comment ID to use.\n                  schema:\n                    type: string\n            requestBody:\n                content:\n                    application/json:\n                        schema:\n                            $ref: '#/components/schemas/Memo'\n                required: true\n            responses:\n                \"200\":\n                    description: OK\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/Memo'\n                default:\n                    description: Default error response\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/Status'\n    /api/v1/memos/{memo}/reactions:\n        get:\n            tags:\n                - MemoService\n            description: ListMemoReactions lists reactions for a memo.\n            operationId: MemoService_ListMemoReactions\n            parameters:\n                - name: memo\n                  in: path\n                  description: The memo id.\n                  required: true\n                  schema:\n                    type: string\n                - name: pageSize\n                  in: query\n                  description: Optional. The maximum number of reactions to return.\n                  schema:\n                    type: integer\n                    format: int32\n                - name: pageToken\n                  in: query\n                  description: Optional. A page token for pagination.\n                  schema:\n                    type: string\n            responses:\n                \"200\":\n                    description: OK\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/ListMemoReactionsResponse'\n                default:\n                    description: Default error response\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/Status'\n        post:\n            tags:\n                - MemoService\n            description: UpsertMemoReaction upserts a reaction for a memo.\n            operationId: MemoService_UpsertMemoReaction\n            parameters:\n                - name: memo\n                  in: path\n                  description: The memo id.\n                  required: true\n                  schema:\n                    type: string\n            requestBody:\n                content:\n                    application/json:\n                        schema:\n                            $ref: '#/components/schemas/UpsertMemoReactionRequest'\n                required: true\n            responses:\n                \"200\":\n                    description: OK\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/Reaction'\n                default:\n                    description: Default error response\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/Status'\n    /api/v1/memos/{memo}/reactions/{reaction}:\n        delete:\n            tags:\n                - MemoService\n            description: DeleteMemoReaction deletes a reaction for a memo.\n            operationId: MemoService_DeleteMemoReaction\n            parameters:\n                - name: memo\n                  in: path\n                  description: The memo id.\n                  required: true\n                  schema:\n                    type: string\n                - name: reaction\n                  in: path\n                  description: The reaction id.\n                  required: true\n                  schema:\n                    type: string\n            responses:\n                \"200\":\n                    description: OK\n                    content: {}\n                default:\n                    description: Default error response\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/Status'\n    /api/v1/memos/{memo}/relations:\n        get:\n            tags:\n                - MemoService\n            description: ListMemoRelations lists relations for a memo.\n            operationId: MemoService_ListMemoRelations\n            parameters:\n                - name: memo\n                  in: path\n                  description: The memo id.\n                  required: true\n                  schema:\n                    type: string\n                - name: pageSize\n                  in: query\n                  description: Optional. The maximum number of relations to return.\n                  schema:\n                    type: integer\n                    format: int32\n                - name: pageToken\n                  in: query\n                  description: Optional. A page token for pagination.\n                  schema:\n                    type: string\n            responses:\n                \"200\":\n                    description: OK\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/ListMemoRelationsResponse'\n                default:\n                    description: Default error response\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/Status'\n        patch:\n            tags:\n                - MemoService\n            description: SetMemoRelations sets relations for a memo.\n            operationId: MemoService_SetMemoRelations\n            parameters:\n                - name: memo\n                  in: path\n                  description: The memo id.\n                  required: true\n                  schema:\n                    type: string\n            requestBody:\n                content:\n                    application/json:\n                        schema:\n                            $ref: '#/components/schemas/SetMemoRelationsRequest'\n                required: true\n            responses:\n                \"200\":\n                    description: OK\n                    content: {}\n                default:\n                    description: Default error response\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/Status'\n    /api/v1/memos/{memo}/shares:\n        get:\n            tags:\n                - MemoService\n            description: ListMemoShares lists all share links for a memo. Requires authentication as the memo creator.\n            operationId: MemoService_ListMemoShares\n            parameters:\n                - name: memo\n                  in: path\n                  description: The memo id.\n                  required: true\n                  schema:\n                    type: string\n            responses:\n                \"200\":\n                    description: OK\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/ListMemoSharesResponse'\n                default:\n                    description: Default error response\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/Status'\n        post:\n            tags:\n                - MemoService\n            description: CreateMemoShare creates a share link for a memo. Requires authentication as the memo creator.\n            operationId: MemoService_CreateMemoShare\n            parameters:\n                - name: memo\n                  in: path\n                  description: The memo id.\n                  required: true\n                  schema:\n                    type: string\n            requestBody:\n                content:\n                    application/json:\n                        schema:\n                            $ref: '#/components/schemas/MemoShare'\n                required: true\n            responses:\n                \"200\":\n                    description: OK\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/MemoShare'\n                default:\n                    description: Default error response\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/Status'\n    /api/v1/memos/{memo}/shares/{share}:\n        delete:\n            tags:\n                - MemoService\n            description: DeleteMemoShare revokes a share link. Requires authentication as the memo creator.\n            operationId: MemoService_DeleteMemoShare\n            parameters:\n                - name: memo\n                  in: path\n                  description: The memo id.\n                  required: true\n                  schema:\n                    type: string\n                - name: share\n                  in: path\n                  description: The share id.\n                  required: true\n                  schema:\n                    type: string\n            responses:\n                \"200\":\n                    description: OK\n                    content: {}\n                default:\n                    description: Default error response\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/Status'\n    /api/v1/shares/{shareId}:\n        get:\n            tags:\n                - MemoService\n            description: |-\n                GetMemoByShare resolves a share token to its memo. No authentication required.\n                 Returns NOT_FOUND if the token is invalid or expired.\n            operationId: MemoService_GetMemoByShare\n            parameters:\n                - name: shareId\n                  in: path\n                  description: Required. The share token extracted from the share URL (/s/{share_id}).\n                  required: true\n                  schema:\n                    type: string\n            responses:\n                \"200\":\n                    description: OK\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/Memo'\n                default:\n                    description: Default error response\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/Status'\n    /api/v1/users:\n        get:\n            tags:\n                - UserService\n            description: ListUsers returns a list of users.\n            operationId: UserService_ListUsers\n            parameters:\n                - name: pageSize\n                  in: query\n                  description: |-\n                    Optional. The maximum number of users to return.\n                     The service may return fewer than this value.\n                     If unspecified, at most 50 users will be returned.\n                     The maximum value is 1000; values above 1000 will be coerced to 1000.\n                  schema:\n                    type: integer\n                    format: int32\n                - name: pageToken\n                  in: query\n                  description: |-\n                    Optional. A page token, received from a previous `ListUsers` call.\n                     Provide this to retrieve the subsequent page.\n                  schema:\n                    type: string\n                - name: filter\n                  in: query\n                  description: |-\n                    Optional. Filter to apply to the list results.\n                     Example: \"username == 'steven'\"\n                     Supported operators: ==\n                     Supported fields: username\n                  schema:\n                    type: string\n                - name: showDeleted\n                  in: query\n                  description: Optional. If true, show deleted users in the response.\n                  schema:\n                    type: boolean\n            responses:\n                \"200\":\n                    description: OK\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/ListUsersResponse'\n                default:\n                    description: Default error response\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/Status'\n        post:\n            tags:\n                - UserService\n            description: CreateUser creates a new user.\n            operationId: UserService_CreateUser\n            parameters:\n                - name: userId\n                  in: query\n                  description: |-\n                    Optional. The user ID to use for this user.\n                     If empty, a unique ID will be generated.\n                     Must match the pattern [a-z0-9-]+\n                  schema:\n                    type: string\n                - name: validateOnly\n                  in: query\n                  description: Optional. If set, validate the request but don't actually create the user.\n                  schema:\n                    type: boolean\n                - name: requestId\n                  in: query\n                  description: |-\n                    Optional. An idempotency token that can be used to ensure that multiple\n                     requests to create a user have the same result.\n                  schema:\n                    type: string\n            requestBody:\n                content:\n                    application/json:\n                        schema:\n                            $ref: '#/components/schemas/User'\n                required: true\n            responses:\n                \"200\":\n                    description: OK\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/User'\n                default:\n                    description: Default error response\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/Status'\n    /api/v1/users/{user}:\n        get:\n            tags:\n                - UserService\n            description: |-\n                GetUser gets a user by ID or username.\n                 Supports both numeric IDs and username strings:\n                   - users/{id}       (e.g., users/101)\n                   - users/{username} (e.g., users/steven)\n            operationId: UserService_GetUser\n            parameters:\n                - name: user\n                  in: path\n                  description: The user id.\n                  required: true\n                  schema:\n                    type: string\n                - name: readMask\n                  in: query\n                  description: |-\n                    Optional. The fields to return in the response.\n                     If not specified, all fields are returned.\n                  schema:\n                    type: string\n                    format: field-mask\n            responses:\n                \"200\":\n                    description: OK\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/User'\n                default:\n                    description: Default error response\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/Status'\n        delete:\n            tags:\n                - UserService\n            description: DeleteUser deletes a user.\n            operationId: UserService_DeleteUser\n            parameters:\n                - name: user\n                  in: path\n                  description: The user id.\n                  required: true\n                  schema:\n                    type: string\n                - name: force\n                  in: query\n                  description: Optional. If set to true, the user will be deleted even if they have associated data.\n                  schema:\n                    type: boolean\n            responses:\n                \"200\":\n                    description: OK\n                    content: {}\n                default:\n                    description: Default error response\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/Status'\n        patch:\n            tags:\n                - UserService\n            description: UpdateUser updates a user.\n            operationId: UserService_UpdateUser\n            parameters:\n                - name: user\n                  in: path\n                  description: The user id.\n                  required: true\n                  schema:\n                    type: string\n                - name: updateMask\n                  in: query\n                  description: Required. The list of fields to update.\n                  schema:\n                    type: string\n                    format: field-mask\n                - name: allowMissing\n                  in: query\n                  description: Optional. If set to true, allows updating sensitive fields.\n                  schema:\n                    type: boolean\n            requestBody:\n                content:\n                    application/json:\n                        schema:\n                            $ref: '#/components/schemas/User'\n                required: true\n            responses:\n                \"200\":\n                    description: OK\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/User'\n                default:\n                    description: Default error response\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/Status'\n    /api/v1/users/{user}/notifications:\n        get:\n            tags:\n                - UserService\n            description: ListUserNotifications lists notifications for a user.\n            operationId: UserService_ListUserNotifications\n            parameters:\n                - name: user\n                  in: path\n                  description: The user id.\n                  required: true\n                  schema:\n                    type: string\n                - name: pageSize\n                  in: query\n                  schema:\n                    type: integer\n                    format: int32\n                - name: pageToken\n                  in: query\n                  schema:\n                    type: string\n                - name: filter\n                  in: query\n                  schema:\n                    type: string\n            responses:\n                \"200\":\n                    description: OK\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/ListUserNotificationsResponse'\n                default:\n                    description: Default error response\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/Status'\n    /api/v1/users/{user}/notifications/{notification}:\n        delete:\n            tags:\n                - UserService\n            description: DeleteUserNotification deletes a notification.\n            operationId: UserService_DeleteUserNotification\n            parameters:\n                - name: user\n                  in: path\n                  description: The user id.\n                  required: true\n                  schema:\n                    type: string\n                - name: notification\n                  in: path\n                  description: The notification id.\n                  required: true\n                  schema:\n                    type: string\n            responses:\n                \"200\":\n                    description: OK\n                    content: {}\n                default:\n                    description: Default error response\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/Status'\n        patch:\n            tags:\n                - UserService\n            description: UpdateUserNotification updates a notification.\n            operationId: UserService_UpdateUserNotification\n            parameters:\n                - name: user\n                  in: path\n                  description: The user id.\n                  required: true\n                  schema:\n                    type: string\n                - name: notification\n                  in: path\n                  description: The notification id.\n                  required: true\n                  schema:\n                    type: string\n                - name: updateMask\n                  in: query\n                  schema:\n                    type: string\n                    format: field-mask\n            requestBody:\n                content:\n                    application/json:\n                        schema:\n                            $ref: '#/components/schemas/UserNotification'\n                required: true\n            responses:\n                \"200\":\n                    description: OK\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/UserNotification'\n                default:\n                    description: Default error response\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/Status'\n    /api/v1/users/{user}/personalAccessTokens:\n        get:\n            tags:\n                - UserService\n            description: |-\n                ListPersonalAccessTokens returns a list of Personal Access Tokens (PATs) for a user.\n                 PATs are long-lived tokens for API/script access, distinct from short-lived JWT access tokens.\n            operationId: UserService_ListPersonalAccessTokens\n            parameters:\n                - name: user\n                  in: path\n                  description: The user id.\n                  required: true\n                  schema:\n                    type: string\n                - name: pageSize\n                  in: query\n                  description: Optional. The maximum number of tokens to return.\n                  schema:\n                    type: integer\n                    format: int32\n                - name: pageToken\n                  in: query\n                  description: Optional. A page token for pagination.\n                  schema:\n                    type: string\n            responses:\n                \"200\":\n                    description: OK\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/ListPersonalAccessTokensResponse'\n                default:\n                    description: Default error response\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/Status'\n        post:\n            tags:\n                - UserService\n            description: |-\n                CreatePersonalAccessToken creates a new Personal Access Token for a user.\n                 The token value is only returned once upon creation.\n            operationId: UserService_CreatePersonalAccessToken\n            parameters:\n                - name: user\n                  in: path\n                  description: The user id.\n                  required: true\n                  schema:\n                    type: string\n            requestBody:\n                content:\n                    application/json:\n                        schema:\n                            $ref: '#/components/schemas/CreatePersonalAccessTokenRequest'\n                required: true\n            responses:\n                \"200\":\n                    description: OK\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/CreatePersonalAccessTokenResponse'\n                default:\n                    description: Default error response\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/Status'\n    /api/v1/users/{user}/personalAccessTokens/{personalAccessToken}:\n        delete:\n            tags:\n                - UserService\n            description: DeletePersonalAccessToken deletes a Personal Access Token.\n            operationId: UserService_DeletePersonalAccessToken\n            parameters:\n                - name: user\n                  in: path\n                  description: The user id.\n                  required: true\n                  schema:\n                    type: string\n                - name: personalAccessToken\n                  in: path\n                  description: The personalAccessToken id.\n                  required: true\n                  schema:\n                    type: string\n            responses:\n                \"200\":\n                    description: OK\n                    content: {}\n                default:\n                    description: Default error response\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/Status'\n    /api/v1/users/{user}/settings:\n        get:\n            tags:\n                - UserService\n            description: ListUserSettings returns a list of user settings.\n            operationId: UserService_ListUserSettings\n            parameters:\n                - name: user\n                  in: path\n                  description: The user id.\n                  required: true\n                  schema:\n                    type: string\n                - name: pageSize\n                  in: query\n                  description: |-\n                    Optional. The maximum number of settings to return.\n                     The service may return fewer than this value.\n                     If unspecified, at most 50 settings will be returned.\n                     The maximum value is 1000; values above 1000 will be coerced to 1000.\n                  schema:\n                    type: integer\n                    format: int32\n                - name: pageToken\n                  in: query\n                  description: |-\n                    Optional. A page token, received from a previous `ListUserSettings` call.\n                     Provide this to retrieve the subsequent page.\n                  schema:\n                    type: string\n            responses:\n                \"200\":\n                    description: OK\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/ListUserSettingsResponse'\n                default:\n                    description: Default error response\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/Status'\n    /api/v1/users/{user}/settings/{setting}:\n        get:\n            tags:\n                - UserService\n            description: GetUserSetting returns the user setting.\n            operationId: UserService_GetUserSetting\n            parameters:\n                - name: user\n                  in: path\n                  description: The user id.\n                  required: true\n                  schema:\n                    type: string\n                - name: setting\n                  in: path\n                  description: The setting id.\n                  required: true\n                  schema:\n                    type: string\n            responses:\n                \"200\":\n                    description: OK\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/UserSetting'\n                default:\n                    description: Default error response\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/Status'\n        patch:\n            tags:\n                - UserService\n            description: UpdateUserSetting updates the user setting.\n            operationId: UserService_UpdateUserSetting\n            parameters:\n                - name: user\n                  in: path\n                  description: The user id.\n                  required: true\n                  schema:\n                    type: string\n                - name: setting\n                  in: path\n                  description: The setting id.\n                  required: true\n                  schema:\n                    type: string\n                - name: updateMask\n                  in: query\n                  description: Required. The list of fields to update.\n                  schema:\n                    type: string\n                    format: field-mask\n            requestBody:\n                content:\n                    application/json:\n                        schema:\n                            $ref: '#/components/schemas/UserSetting'\n                required: true\n            responses:\n                \"200\":\n                    description: OK\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/UserSetting'\n                default:\n                    description: Default error response\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/Status'\n    /api/v1/users/{user}/shortcuts:\n        get:\n            tags:\n                - ShortcutService\n            description: ListShortcuts returns a list of shortcuts for a user.\n            operationId: ShortcutService_ListShortcuts\n            parameters:\n                - name: user\n                  in: path\n                  description: The user id.\n                  required: true\n                  schema:\n                    type: string\n            responses:\n                \"200\":\n                    description: OK\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/ListShortcutsResponse'\n                default:\n                    description: Default error response\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/Status'\n        post:\n            tags:\n                - ShortcutService\n            description: CreateShortcut creates a new shortcut for a user.\n            operationId: ShortcutService_CreateShortcut\n            parameters:\n                - name: user\n                  in: path\n                  description: The user id.\n                  required: true\n                  schema:\n                    type: string\n                - name: validateOnly\n                  in: query\n                  description: Optional. If set, validate the request, but do not actually create the shortcut.\n                  schema:\n                    type: boolean\n            requestBody:\n                content:\n                    application/json:\n                        schema:\n                            $ref: '#/components/schemas/Shortcut'\n                required: true\n            responses:\n                \"200\":\n                    description: OK\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/Shortcut'\n                default:\n                    description: Default error response\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/Status'\n    /api/v1/users/{user}/shortcuts/{shortcut}:\n        get:\n            tags:\n                - ShortcutService\n            description: GetShortcut gets a shortcut by name.\n            operationId: ShortcutService_GetShortcut\n            parameters:\n                - name: user\n                  in: path\n                  description: The user id.\n                  required: true\n                  schema:\n                    type: string\n                - name: shortcut\n                  in: path\n                  description: The shortcut id.\n                  required: true\n                  schema:\n                    type: string\n            responses:\n                \"200\":\n                    description: OK\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/Shortcut'\n                default:\n                    description: Default error response\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/Status'\n        delete:\n            tags:\n                - ShortcutService\n            description: DeleteShortcut deletes a shortcut for a user.\n            operationId: ShortcutService_DeleteShortcut\n            parameters:\n                - name: user\n                  in: path\n                  description: The user id.\n                  required: true\n                  schema:\n                    type: string\n                - name: shortcut\n                  in: path\n                  description: The shortcut id.\n                  required: true\n                  schema:\n                    type: string\n            responses:\n                \"200\":\n                    description: OK\n                    content: {}\n                default:\n                    description: Default error response\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/Status'\n        patch:\n            tags:\n                - ShortcutService\n            description: UpdateShortcut updates a shortcut for a user.\n            operationId: ShortcutService_UpdateShortcut\n            parameters:\n                - name: user\n                  in: path\n                  description: The user id.\n                  required: true\n                  schema:\n                    type: string\n                - name: shortcut\n                  in: path\n                  description: The shortcut id.\n                  required: true\n                  schema:\n                    type: string\n                - name: updateMask\n                  in: query\n                  description: Optional. The list of fields to update.\n                  schema:\n                    type: string\n                    format: field-mask\n            requestBody:\n                content:\n                    application/json:\n                        schema:\n                            $ref: '#/components/schemas/Shortcut'\n                required: true\n            responses:\n                \"200\":\n                    description: OK\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/Shortcut'\n                default:\n                    description: Default error response\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/Status'\n    /api/v1/users/{user}/webhooks:\n        get:\n            tags:\n                - UserService\n            description: ListUserWebhooks returns a list of webhooks for a user.\n            operationId: UserService_ListUserWebhooks\n            parameters:\n                - name: user\n                  in: path\n                  description: The user id.\n                  required: true\n                  schema:\n                    type: string\n            responses:\n                \"200\":\n                    description: OK\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/ListUserWebhooksResponse'\n                default:\n                    description: Default error response\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/Status'\n        post:\n            tags:\n                - UserService\n            description: CreateUserWebhook creates a new webhook for a user.\n            operationId: UserService_CreateUserWebhook\n            parameters:\n                - name: user\n                  in: path\n                  description: The user id.\n                  required: true\n                  schema:\n                    type: string\n            requestBody:\n                content:\n                    application/json:\n                        schema:\n                            $ref: '#/components/schemas/UserWebhook'\n                required: true\n            responses:\n                \"200\":\n                    description: OK\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/UserWebhook'\n                default:\n                    description: Default error response\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/Status'\n    /api/v1/users/{user}/webhooks/{webhook}:\n        delete:\n            tags:\n                - UserService\n            description: DeleteUserWebhook deletes a webhook for a user.\n            operationId: UserService_DeleteUserWebhook\n            parameters:\n                - name: user\n                  in: path\n                  description: The user id.\n                  required: true\n                  schema:\n                    type: string\n                - name: webhook\n                  in: path\n                  description: The webhook id.\n                  required: true\n                  schema:\n                    type: string\n            responses:\n                \"200\":\n                    description: OK\n                    content: {}\n                default:\n                    description: Default error response\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/Status'\n        patch:\n            tags:\n                - UserService\n            description: UpdateUserWebhook updates an existing webhook for a user.\n            operationId: UserService_UpdateUserWebhook\n            parameters:\n                - name: user\n                  in: path\n                  description: The user id.\n                  required: true\n                  schema:\n                    type: string\n                - name: webhook\n                  in: path\n                  description: The webhook id.\n                  required: true\n                  schema:\n                    type: string\n                - name: updateMask\n                  in: query\n                  description: The list of fields to update.\n                  schema:\n                    type: string\n                    format: field-mask\n            requestBody:\n                content:\n                    application/json:\n                        schema:\n                            $ref: '#/components/schemas/UserWebhook'\n                required: true\n            responses:\n                \"200\":\n                    description: OK\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/UserWebhook'\n                default:\n                    description: Default error response\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/Status'\n    /api/v1/users/{user}:getStats:\n        get:\n            tags:\n                - UserService\n            description: GetUserStats returns statistics for a specific user.\n            operationId: UserService_GetUserStats\n            parameters:\n                - name: user\n                  in: path\n                  description: The user id.\n                  required: true\n                  schema:\n                    type: string\n            responses:\n                \"200\":\n                    description: OK\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/UserStats'\n                default:\n                    description: Default error response\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/Status'\n    /api/v1/users:stats:\n        get:\n            tags:\n                - UserService\n            description: ListAllUserStats returns statistics for all users.\n            operationId: UserService_ListAllUserStats\n            responses:\n                \"200\":\n                    description: OK\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/ListAllUserStatsResponse'\n                default:\n                    description: Default error response\n                    content:\n                        application/json:\n                            schema:\n                                $ref: '#/components/schemas/Status'\ncomponents:\n    schemas:\n        Attachment:\n            required:\n                - filename\n                - type\n            type: object\n            properties:\n                name:\n                    type: string\n                    description: |-\n                        The name of the attachment.\n                         Format: attachments/{attachment}\n                createTime:\n                    readOnly: true\n                    type: string\n                    description: Output only. The creation timestamp.\n                    format: date-time\n                filename:\n                    type: string\n                    description: The filename of the attachment.\n                content:\n                    writeOnly: true\n                    type: string\n                    description: Input only. The content of the attachment.\n                    format: bytes\n                externalLink:\n                    type: string\n                    description: Optional. The external link of the attachment.\n                type:\n                    type: string\n                    description: The MIME type of the attachment.\n                size:\n                    readOnly: true\n                    type: string\n                    description: Output only. The size of the attachment in bytes.\n                memo:\n                    type: string\n                    description: |-\n                        Optional. The related memo. Refer to `Memo.name`.\n                         Format: memos/{memo}\n        Color:\n            type: object\n            properties:\n                red:\n                    type: number\n                    description: The amount of red in the color as a value in the interval [0, 1].\n                    format: float\n                green:\n                    type: number\n                    description: The amount of green in the color as a value in the interval [0, 1].\n                    format: float\n                blue:\n                    type: number\n                    description: The amount of blue in the color as a value in the interval [0, 1].\n                    format: float\n                alpha:\n                    type: number\n                    description: |-\n                        The fraction of this color that should be applied to the pixel. That is,\n                         the final pixel color is defined by the equation:\n\n                           `pixel color = alpha * (this color) + (1.0 - alpha) * (background color)`\n\n                         This means that a value of 1.0 corresponds to a solid color, whereas\n                         a value of 0.0 corresponds to a completely transparent color. This\n                         uses a wrapper message rather than a simple float scalar so that it is\n                         possible to distinguish between a default value and the value being unset.\n                         If omitted, this color object is rendered as a solid color\n                         (as if the alpha value had been explicitly given a value of 1.0).\n                    format: float\n            description: |-\n                Represents a color in the RGBA color space. This representation is designed\n                 for simplicity of conversion to/from color representations in various\n                 languages over compactness. For example, the fields of this representation\n                 can be trivially provided to the constructor of `java.awt.Color` in Java; it\n                 can also be trivially provided to UIColor's `+colorWithRed:green:blue:alpha`\n                 method in iOS; and, with just a little work, it can be easily formatted into\n                 a CSS `rgba()` string in JavaScript.\n\n                 This reference page doesn't carry information about the absolute color\n                 space\n                 that should be used to interpret the RGB value (e.g. sRGB, Adobe RGB,\n                 DCI-P3, BT.2020, etc.). By default, applications should assume the sRGB color\n                 space.\n\n                 When color equality needs to be decided, implementations, unless\n                 documented otherwise, treat two colors as equal if all their red,\n                 green, blue, and alpha values each differ by at most 1e-5.\n\n                 Example (Java):\n\n                      import com.google.type.Color;\n\n                      // ...\n                      public static java.awt.Color fromProto(Color protocolor) {\n                        float alpha = protocolor.hasAlpha()\n                            ? protocolor.getAlpha().getValue()\n                            : 1.0;\n\n                        return new java.awt.Color(\n                            protocolor.getRed(),\n                            protocolor.getGreen(),\n                            protocolor.getBlue(),\n                            alpha);\n                      }\n\n                      public static Color toProto(java.awt.Color color) {\n                        float red = (float) color.getRed();\n                        float green = (float) color.getGreen();\n                        float blue = (float) color.getBlue();\n                        float denominator = 255.0;\n                        Color.Builder resultBuilder =\n                            Color\n                                .newBuilder()\n                                .setRed(red / denominator)\n                                .setGreen(green / denominator)\n                                .setBlue(blue / denominator);\n                        int alpha = color.getAlpha();\n                        if (alpha != 255) {\n                          result.setAlpha(\n                              FloatValue\n                                  .newBuilder()\n                                  .setValue(((float) alpha) / denominator)\n                                  .build());\n                        }\n                        return resultBuilder.build();\n                      }\n                      // ...\n\n                 Example (iOS / Obj-C):\n\n                      // ...\n                      static UIColor* fromProto(Color* protocolor) {\n                         float red = [protocolor red];\n                         float green = [protocolor green];\n                         float blue = [protocolor blue];\n                         FloatValue* alpha_wrapper = [protocolor alpha];\n                         float alpha = 1.0;\n                         if (alpha_wrapper != nil) {\n                           alpha = [alpha_wrapper value];\n                         }\n                         return [UIColor colorWithRed:red green:green blue:blue alpha:alpha];\n                      }\n\n                      static Color* toProto(UIColor* color) {\n                          CGFloat red, green, blue, alpha;\n                          if (![color getRed:&red green:&green blue:&blue alpha:&alpha]) {\n                            return nil;\n                          }\n                          Color* result = [[Color alloc] init];\n                          [result setRed:red];\n                          [result setGreen:green];\n                          [result setBlue:blue];\n                          if (alpha <= 0.9999) {\n                            [result setAlpha:floatWrapperWithValue(alpha)];\n                          }\n                          [result autorelease];\n                          return result;\n                     }\n                     // ...\n\n                  Example (JavaScript):\n\n                     // ...\n\n                     var protoToCssColor = function(rgb_color) {\n                        var redFrac = rgb_color.red || 0.0;\n                        var greenFrac = rgb_color.green || 0.0;\n                        var blueFrac = rgb_color.blue || 0.0;\n                        var red = Math.floor(redFrac * 255);\n                        var green = Math.floor(greenFrac * 255);\n                        var blue = Math.floor(blueFrac * 255);\n\n                        if (!('alpha' in rgb_color)) {\n                           return rgbToCssColor(red, green, blue);\n                        }\n\n                        var alphaFrac = rgb_color.alpha.value || 0.0;\n                        var rgbParams = [red, green, blue].join(',');\n                        return ['rgba(', rgbParams, ',', alphaFrac, ')'].join('');\n                     };\n\n                     var rgbToCssColor = function(red, green, blue) {\n                       var rgbNumber = new Number((red << 16) | (green << 8) | blue);\n                       var hexString = rgbNumber.toString(16);\n                       var missingZeros = 6 - hexString.length;\n                       var resultBuilder = ['#'];\n                       for (var i = 0; i < missingZeros; i++) {\n                          resultBuilder.push('0');\n                       }\n                       resultBuilder.push(hexString);\n                       return resultBuilder.join('');\n                     };\n\n                     // ...\n        CreatePersonalAccessTokenRequest:\n            required:\n                - parent\n            type: object\n            properties:\n                parent:\n                    type: string\n                    description: |-\n                        Required. The parent resource where this token will be created.\n                         Format: users/{user}\n                description:\n                    type: string\n                    description: Optional. Description of the personal access token.\n                expiresInDays:\n                    type: integer\n                    description: Optional. Expiration duration in days (0 = never expires).\n                    format: int32\n        CreatePersonalAccessTokenResponse:\n            type: object\n            properties:\n                personalAccessToken:\n                    allOf:\n                        - $ref: '#/components/schemas/PersonalAccessToken'\n                    description: The personal access token metadata.\n                token:\n                    type: string\n                    description: |-\n                        The actual token value - only returned on creation.\n                         This is the only time the token value will be visible.\n        FieldMapping:\n            type: object\n            properties:\n                identifier:\n                    type: string\n                displayName:\n                    type: string\n                email:\n                    type: string\n                avatarUrl:\n                    type: string\n        GeneralSetting_CustomProfile:\n            type: object\n            properties:\n                title:\n                    type: string\n                description:\n                    type: string\n                logoUrl:\n                    type: string\n            description: Custom profile configuration for instance branding.\n        GetCurrentUserResponse:\n            type: object\n            properties:\n                user:\n                    allOf:\n                        - $ref: '#/components/schemas/User'\n                    description: The authenticated user's information.\n        GoogleProtobufAny:\n            type: object\n            properties:\n                '@type':\n                    type: string\n                    description: The type of the serialized message.\n            additionalProperties: true\n            description: Contains an arbitrary serialized message along with a @type that describes the type of the serialized message.\n        IdentityProvider:\n            required:\n                - type\n                - title\n                - config\n            type: object\n            properties:\n                name:\n                    type: string\n                    description: |-\n                        The resource name of the identity provider.\n                         Format: identity-providers/{idp}\n                type:\n                    enum:\n                        - TYPE_UNSPECIFIED\n                        - OAUTH2\n                    type: string\n                    description: Required. The type of the identity provider.\n                    format: enum\n                title:\n                    type: string\n                    description: Required. The display title of the identity provider.\n                identifierFilter:\n                    type: string\n                    description: Optional. Filter applied to user identifiers.\n                config:\n                    allOf:\n                        - $ref: '#/components/schemas/IdentityProviderConfig'\n                    description: Required. Configuration for the identity provider.\n        IdentityProviderConfig:\n            type: object\n            properties:\n                oauth2Config:\n                    $ref: '#/components/schemas/OAuth2Config'\n        InstanceProfile:\n            type: object\n            properties:\n                version:\n                    type: string\n                    description: Version is the current version of instance.\n                demo:\n                    type: boolean\n                    description: Demo indicates if the instance is in demo mode.\n                instanceUrl:\n                    type: string\n                    description: Instance URL is the URL of the instance.\n                admin:\n                    allOf:\n                        - $ref: '#/components/schemas/User'\n                    description: |-\n                        The first administrator who set up this instance.\n                         When null, instance requires initial setup (creating the first admin account).\n            description: Instance profile message containing basic instance information.\n        InstanceSetting:\n            type: object\n            properties:\n                name:\n                    type: string\n                    description: |-\n                        The name of the instance setting.\n                         Format: instance/settings/{setting}\n                generalSetting:\n                    $ref: '#/components/schemas/InstanceSetting_GeneralSetting'\n                storageSetting:\n                    $ref: '#/components/schemas/InstanceSetting_StorageSetting'\n                memoRelatedSetting:\n                    $ref: '#/components/schemas/InstanceSetting_MemoRelatedSetting'\n                tagsSetting:\n                    $ref: '#/components/schemas/InstanceSetting_TagsSetting'\n                notificationSetting:\n                    $ref: '#/components/schemas/InstanceSetting_NotificationSetting'\n            description: An instance setting resource.\n        InstanceSetting_GeneralSetting:\n            type: object\n            properties:\n                disallowUserRegistration:\n                    type: boolean\n                    description: disallow_user_registration disallows user registration.\n                disallowPasswordAuth:\n                    type: boolean\n                    description: disallow_password_auth disallows password authentication.\n                additionalScript:\n                    type: string\n                    description: additional_script is the additional script.\n                additionalStyle:\n                    type: string\n                    description: additional_style is the additional style.\n                customProfile:\n                    allOf:\n                        - $ref: '#/components/schemas/GeneralSetting_CustomProfile'\n                    description: custom_profile is the custom profile.\n                weekStartDayOffset:\n                    type: integer\n                    description: |-\n                        week_start_day_offset is the week start day offset from Sunday.\n                         0: Sunday, 1: Monday, 2: Tuesday, 3: Wednesday, 4: Thursday, 5: Friday, 6: Saturday\n                         Default is Sunday.\n                    format: int32\n                disallowChangeUsername:\n                    type: boolean\n                    description: disallow_change_username disallows changing username.\n                disallowChangeNickname:\n                    type: boolean\n                    description: disallow_change_nickname disallows changing nickname.\n            description: General instance settings configuration.\n        InstanceSetting_MemoRelatedSetting:\n            type: object\n            properties:\n                displayWithUpdateTime:\n                    type: boolean\n                    description: display_with_update_time orders and displays memo with update time.\n                contentLengthLimit:\n                    type: integer\n                    description: content_length_limit is the limit of content length. Unit is byte.\n                    format: int32\n                enableDoubleClickEdit:\n                    type: boolean\n                    description: enable_double_click_edit enables editing on double click.\n                reactions:\n                    type: array\n                    items:\n                        type: string\n                    description: reactions is the list of reactions.\n            description: Memo-related instance settings and policies.\n        InstanceSetting_NotificationSetting:\n            type: object\n            properties:\n                email:\n                    $ref: '#/components/schemas/NotificationSetting_EmailSetting'\n            description: Notification transport configuration.\n        InstanceSetting_StorageSetting:\n            type: object\n            properties:\n                storageType:\n                    enum:\n                        - STORAGE_TYPE_UNSPECIFIED\n                        - DATABASE\n                        - LOCAL\n                        - S3\n                    type: string\n                    description: storage_type is the storage type.\n                    format: enum\n                filepathTemplate:\n                    type: string\n                    description: |-\n                        The template of file path.\n                         e.g. assets/{timestamp}_{filename}\n                uploadSizeLimitMb:\n                    type: string\n                    description: The max upload size in megabytes.\n                s3Config:\n                    allOf:\n                        - $ref: '#/components/schemas/StorageSetting_S3Config'\n                    description: The S3 config.\n            description: Storage configuration settings for instance attachments.\n        InstanceSetting_TagMetadata:\n            type: object\n            properties:\n                backgroundColor:\n                    allOf:\n                        - $ref: '#/components/schemas/Color'\n                    description: Background color for the tag label.\n            description: Metadata for a tag.\n        InstanceSetting_TagsSetting:\n            type: object\n            properties:\n                tags:\n                    type: object\n                    additionalProperties:\n                        $ref: '#/components/schemas/InstanceSetting_TagMetadata'\n            description: Tag metadata configuration.\n        ListAllUserStatsResponse:\n            type: object\n            properties:\n                stats:\n                    type: array\n                    items:\n                        $ref: '#/components/schemas/UserStats'\n                    description: The list of user statistics.\n        ListAttachmentsResponse:\n            type: object\n            properties:\n                attachments:\n                    type: array\n                    items:\n                        $ref: '#/components/schemas/Attachment'\n                    description: The list of attachments.\n                nextPageToken:\n                    type: string\n                    description: |-\n                        A token that can be sent as `page_token` to retrieve the next page.\n                         If this field is omitted, there are no subsequent pages.\n                totalSize:\n                    type: integer\n                    description: The total count of attachments (may be approximate).\n                    format: int32\n        ListIdentityProvidersResponse:\n            type: object\n            properties:\n                identityProviders:\n                    type: array\n                    items:\n                        $ref: '#/components/schemas/IdentityProvider'\n                    description: The list of identity providers.\n        ListMemoAttachmentsResponse:\n            type: object\n            properties:\n                attachments:\n                    type: array\n                    items:\n                        $ref: '#/components/schemas/Attachment'\n                    description: The list of attachments.\n                nextPageToken:\n                    type: string\n                    description: A token for the next page of results.\n        ListMemoCommentsResponse:\n            type: object\n            properties:\n                memos:\n                    type: array\n                    items:\n                        $ref: '#/components/schemas/Memo'\n                    description: The list of comment memos.\n                nextPageToken:\n                    type: string\n                    description: A token for the next page of results.\n                totalSize:\n                    type: integer\n                    description: The total count of comments.\n                    format: int32\n        ListMemoReactionsResponse:\n            type: object\n            properties:\n                reactions:\n                    type: array\n                    items:\n                        $ref: '#/components/schemas/Reaction'\n                    description: The list of reactions.\n                nextPageToken:\n                    type: string\n                    description: A token for the next page of results.\n                totalSize:\n                    type: integer\n                    description: The total count of reactions.\n                    format: int32\n        ListMemoRelationsResponse:\n            type: object\n            properties:\n                relations:\n                    type: array\n                    items:\n                        $ref: '#/components/schemas/MemoRelation'\n                    description: The list of relations.\n                nextPageToken:\n                    type: string\n                    description: A token for the next page of results.\n        ListMemoSharesResponse:\n            type: object\n            properties:\n                memoShares:\n                    type: array\n                    items:\n                        $ref: '#/components/schemas/MemoShare'\n                    description: The list of share links.\n        ListMemosResponse:\n            type: object\n            properties:\n                memos:\n                    type: array\n                    items:\n                        $ref: '#/components/schemas/Memo'\n                    description: The list of memos.\n                nextPageToken:\n                    type: string\n                    description: |-\n                        A token that can be sent as `page_token` to retrieve the next page.\n                         If this field is omitted, there are no subsequent pages.\n        ListPersonalAccessTokensResponse:\n            type: object\n            properties:\n                personalAccessTokens:\n                    type: array\n                    items:\n                        $ref: '#/components/schemas/PersonalAccessToken'\n                    description: The list of personal access tokens.\n                nextPageToken:\n                    type: string\n                    description: A token for the next page of results.\n                totalSize:\n                    type: integer\n                    description: The total count of personal access tokens.\n                    format: int32\n        ListShortcutsResponse:\n            type: object\n            properties:\n                shortcuts:\n                    type: array\n                    items:\n                        $ref: '#/components/schemas/Shortcut'\n                    description: The list of shortcuts.\n        ListUserNotificationsResponse:\n            type: object\n            properties:\n                notifications:\n                    type: array\n                    items:\n                        $ref: '#/components/schemas/UserNotification'\n                nextPageToken:\n                    type: string\n        ListUserSettingsResponse:\n            type: object\n            properties:\n                settings:\n                    type: array\n                    items:\n                        $ref: '#/components/schemas/UserSetting'\n                    description: The list of user settings.\n                nextPageToken:\n                    type: string\n                    description: |-\n                        A token that can be sent as `page_token` to retrieve the next page.\n                         If this field is omitted, there are no subsequent pages.\n                totalSize:\n                    type: integer\n                    description: The total count of settings (may be approximate).\n                    format: int32\n            description: Response message for ListUserSettings method.\n        ListUserWebhooksResponse:\n            type: object\n            properties:\n                webhooks:\n                    type: array\n                    items:\n                        $ref: '#/components/schemas/UserWebhook'\n                    description: The list of webhooks.\n        ListUsersResponse:\n            type: object\n            properties:\n                users:\n                    type: array\n                    items:\n                        $ref: '#/components/schemas/User'\n                    description: The list of users.\n                nextPageToken:\n                    type: string\n                    description: |-\n                        A token that can be sent as `page_token` to retrieve the next page.\n                         If this field is omitted, there are no subsequent pages.\n                totalSize:\n                    type: integer\n                    description: The total count of users (may be approximate).\n                    format: int32\n        Location:\n            type: object\n            properties:\n                placeholder:\n                    type: string\n                    description: A placeholder text for the location.\n                latitude:\n                    type: number\n                    description: The latitude of the location.\n                    format: double\n                longitude:\n                    type: number\n                    description: The longitude of the location.\n                    format: double\n        Memo:\n            required:\n                - state\n                - content\n                - visibility\n            type: object\n            properties:\n                name:\n                    type: string\n                    description: |-\n                        The resource name of the memo.\n                         Format: memos/{memo}, memo is the user defined id or uuid.\n                state:\n                    enum:\n                        - STATE_UNSPECIFIED\n                        - NORMAL\n                        - ARCHIVED\n                    type: string\n                    description: The state of the memo.\n                    format: enum\n                creator:\n                    readOnly: true\n                    type: string\n                    description: |-\n                        The name of the creator.\n                         Format: users/{user}\n                createTime:\n                    type: string\n                    description: |-\n                        The creation timestamp.\n                         If not set on creation, the server will set it to the current time.\n                    format: date-time\n                updateTime:\n                    type: string\n                    description: |-\n                        The last update timestamp.\n                         If not set on creation, the server will set it to the current time.\n                    format: date-time\n                displayTime:\n                    type: string\n                    description: The display timestamp of the memo.\n                    format: date-time\n                content:\n                    type: string\n                    description: Required. The content of the memo in Markdown format.\n                visibility:\n                    enum:\n                        - VISIBILITY_UNSPECIFIED\n                        - PRIVATE\n                        - PROTECTED\n                        - PUBLIC\n                    type: string\n                    description: The visibility of the memo.\n                    format: enum\n                tags:\n                    readOnly: true\n                    type: array\n                    items:\n                        type: string\n                    description: Output only. The tags extracted from the content.\n                pinned:\n                    type: boolean\n                    description: Whether the memo is pinned.\n                attachments:\n                    type: array\n                    items:\n                        $ref: '#/components/schemas/Attachment'\n                    description: Optional. The attachments of the memo.\n                relations:\n                    type: array\n                    items:\n                        $ref: '#/components/schemas/MemoRelation'\n                    description: Optional. The relations of the memo.\n                reactions:\n                    readOnly: true\n                    type: array\n                    items:\n                        $ref: '#/components/schemas/Reaction'\n                    description: Output only. The reactions to the memo.\n                property:\n                    readOnly: true\n                    allOf:\n                        - $ref: '#/components/schemas/Memo_Property'\n                    description: Output only. The computed properties of the memo.\n                parent:\n                    readOnly: true\n                    type: string\n                    description: |-\n                        Output only. The name of the parent memo.\n                         Format: memos/{memo}\n                snippet:\n                    readOnly: true\n                    type: string\n                    description: Output only. The snippet of the memo content. Plain text only.\n                location:\n                    allOf:\n                        - $ref: '#/components/schemas/Location'\n                    description: Optional. The location of the memo.\n        MemoRelation:\n            required:\n                - memo\n                - relatedMemo\n                - type\n            type: object\n            properties:\n                memo:\n                    allOf:\n                        - $ref: '#/components/schemas/MemoRelation_Memo'\n                    description: The memo in the relation.\n                relatedMemo:\n                    allOf:\n                        - $ref: '#/components/schemas/MemoRelation_Memo'\n                    description: The related memo.\n                type:\n                    enum:\n                        - TYPE_UNSPECIFIED\n                        - REFERENCE\n                        - COMMENT\n                    type: string\n                    format: enum\n        MemoRelation_Memo:\n            required:\n                - name\n            type: object\n            properties:\n                name:\n                    type: string\n                    description: |-\n                        The resource name of the memo.\n                         Format: memos/{memo}\n                snippet:\n                    readOnly: true\n                    type: string\n                    description: Output only. The snippet of the memo content. Plain text only.\n            description: Memo reference in relations.\n        MemoShare:\n            type: object\n            properties:\n                name:\n                    type: string\n                    description: |-\n                        The resource name of the share. Format: memos/{memo}/shares/{share}\n                         The {share} segment is the opaque token used in the share URL.\n                createTime:\n                    readOnly: true\n                    type: string\n                    description: Output only. When this share link was created.\n                    format: date-time\n                expireTime:\n                    type: string\n                    description: |-\n                        Optional. When set, the share link stops working after this time.\n                         If unset, the link never expires.\n                    format: date-time\n            description: MemoShare is an access grant that permits read-only access to a memo via an opaque bearer token.\n        Memo_Property:\n            type: object\n            properties:\n                hasLink:\n                    type: boolean\n                hasTaskList:\n                    type: boolean\n                hasCode:\n                    type: boolean\n                hasIncompleteTasks:\n                    type: boolean\n                title:\n                    type: string\n                    description: The title extracted from the first H1 heading, if present.\n            description: Computed properties of a memo.\n        NotificationSetting_EmailSetting:\n            type: object\n            properties:\n                enabled:\n                    type: boolean\n                smtpHost:\n                    type: string\n                smtpPort:\n                    type: integer\n                    format: int32\n                smtpUsername:\n                    type: string\n                smtpPassword:\n                    type: string\n                fromEmail:\n                    type: string\n                fromName:\n                    type: string\n                replyTo:\n                    type: string\n                useTls:\n                    type: boolean\n                useSsl:\n                    type: boolean\n            description: Email delivery configuration for notifications.\n        OAuth2Config:\n            type: object\n            properties:\n                clientId:\n                    type: string\n                clientSecret:\n                    type: string\n                authUrl:\n                    type: string\n                tokenUrl:\n                    type: string\n                userInfoUrl:\n                    type: string\n                scopes:\n                    type: array\n                    items:\n                        type: string\n                fieldMapping:\n                    $ref: '#/components/schemas/FieldMapping'\n        PersonalAccessToken:\n            type: object\n            properties:\n                name:\n                    type: string\n                    description: |-\n                        The resource name of the personal access token.\n                         Format: users/{user}/personalAccessTokens/{personal_access_token}\n                description:\n                    type: string\n                    description: The description of the token.\n                createdAt:\n                    readOnly: true\n                    type: string\n                    description: Output only. The creation timestamp.\n                    format: date-time\n                expiresAt:\n                    type: string\n                    description: Optional. The expiration timestamp.\n                    format: date-time\n                lastUsedAt:\n                    readOnly: true\n                    type: string\n                    description: Output only. The last used timestamp.\n                    format: date-time\n            description: |-\n                PersonalAccessToken represents a long-lived token for API/script access.\n                 PATs are distinct from short-lived JWT access tokens used for session authentication.\n        Reaction:\n            required:\n                - contentId\n                - reactionType\n            type: object\n            properties:\n                name:\n                    readOnly: true\n                    type: string\n                    description: |-\n                        The resource name of the reaction.\n                         Format: memos/{memo}/reactions/{reaction}\n                creator:\n                    readOnly: true\n                    type: string\n                    description: |-\n                        The resource name of the creator.\n                         Format: users/{user}\n                contentId:\n                    type: string\n                    description: |-\n                        The resource name of the content.\n                         For memo reactions, this should be the memo's resource name.\n                         Format: memos/{memo}\n                reactionType:\n                    type: string\n                    description: \"Required. The type of reaction (e.g., \\\"\\U0001F44D\\\", \\\"❤️\\\", \\\"\\U0001F604\\\").\"\n                createTime:\n                    readOnly: true\n                    type: string\n                    description: Output only. The creation timestamp.\n                    format: date-time\n        RefreshTokenRequest:\n            type: object\n            properties: {}\n        RefreshTokenResponse:\n            type: object\n            properties:\n                accessToken:\n                    type: string\n                    description: The new short-lived access token.\n                expiresAt:\n                    type: string\n                    description: When the access token expires.\n                    format: date-time\n        SetMemoAttachmentsRequest:\n            required:\n                - name\n                - attachments\n            type: object\n            properties:\n                name:\n                    type: string\n                    description: |-\n                        Required. The resource name of the memo.\n                         Format: memos/{memo}\n                attachments:\n                    type: array\n                    items:\n                        $ref: '#/components/schemas/Attachment'\n                    description: Required. The attachments to set for the memo.\n        SetMemoRelationsRequest:\n            required:\n                - name\n                - relations\n            type: object\n            properties:\n                name:\n                    type: string\n                    description: |-\n                        Required. The resource name of the memo.\n                         Format: memos/{memo}\n                relations:\n                    type: array\n                    items:\n                        $ref: '#/components/schemas/MemoRelation'\n                    description: Required. The relations to set for the memo.\n        Shortcut:\n            required:\n                - title\n            type: object\n            properties:\n                name:\n                    type: string\n                    description: |-\n                        The resource name of the shortcut.\n                         Format: users/{user}/shortcuts/{shortcut}\n                title:\n                    type: string\n                    description: The title of the shortcut.\n                filter:\n                    type: string\n                    description: The filter expression for the shortcut.\n        SignInRequest:\n            type: object\n            properties:\n                passwordCredentials:\n                    allOf:\n                        - $ref: '#/components/schemas/SignInRequest_PasswordCredentials'\n                    description: Username and password authentication.\n                ssoCredentials:\n                    allOf:\n                        - $ref: '#/components/schemas/SignInRequest_SSOCredentials'\n                    description: SSO provider authentication.\n        SignInRequest_PasswordCredentials:\n            required:\n                - username\n                - password\n            type: object\n            properties:\n                username:\n                    type: string\n                    description: The username to sign in with.\n                password:\n                    type: string\n                    description: The password to sign in with.\n            description: Nested message for password-based authentication credentials.\n        SignInRequest_SSOCredentials:\n            required:\n                - idpName\n                - code\n                - redirectUri\n            type: object\n            properties:\n                idpName:\n                    type: string\n                    description: |-\n                        The resource name of the SSO provider.\n                         Format: identity-providers/{uid}\n                code:\n                    type: string\n                    description: The authorization code from the SSO provider.\n                redirectUri:\n                    type: string\n                    description: The redirect URI used in the SSO flow.\n                codeVerifier:\n                    type: string\n                    description: |-\n                        The PKCE code verifier for enhanced security (RFC 7636).\n                         Optional - enables PKCE flow protection against authorization code interception.\n            description: Nested message for SSO authentication credentials.\n        SignInResponse:\n            type: object\n            properties:\n                user:\n                    allOf:\n                        - $ref: '#/components/schemas/User'\n                    description: The authenticated user's information.\n                accessToken:\n                    type: string\n                    description: |-\n                        The short-lived access token for API requests.\n                         Store in memory only, not in localStorage.\n                accessTokenExpiresAt:\n                    type: string\n                    description: |-\n                        When the access token expires.\n                         Client should call RefreshToken before this time.\n                    format: date-time\n        Status:\n            type: object\n            properties:\n                code:\n                    type: integer\n                    description: The status code, which should be an enum value of [google.rpc.Code][google.rpc.Code].\n                    format: int32\n                message:\n                    type: string\n                    description: A developer-facing error message, which should be in English. Any user-facing error message should be localized and sent in the [google.rpc.Status.details][google.rpc.Status.details] field, or localized by the client.\n                details:\n                    type: array\n                    items:\n                        $ref: '#/components/schemas/GoogleProtobufAny'\n                    description: A list of messages that carry the error details.  There is a common set of message types for APIs to use.\n            description: 'The `Status` type defines a logical error model that is suitable for different programming environments, including REST APIs and RPC APIs. It is used by [gRPC](https://github.com/grpc). Each `Status` message contains three pieces of data: error code, error message, and error details. You can find out more about this error model and how to work with it in the [API Design Guide](https://cloud.google.com/apis/design/errors).'\n        StorageSetting_S3Config:\n            type: object\n            properties:\n                accessKeyId:\n                    type: string\n                accessKeySecret:\n                    type: string\n                endpoint:\n                    type: string\n                region:\n                    type: string\n                bucket:\n                    type: string\n                usePathStyle:\n                    type: boolean\n            description: |-\n                S3 configuration for cloud storage backend.\n                 Reference: https://developers.cloudflare.com/r2/examples/aws/aws-sdk-go/\n        UpsertMemoReactionRequest:\n            required:\n                - name\n                - reaction\n            type: object\n            properties:\n                name:\n                    type: string\n                    description: |-\n                        Required. The resource name of the memo.\n                         Format: memos/{memo}\n                reaction:\n                    allOf:\n                        - $ref: '#/components/schemas/Reaction'\n                    description: Required. The reaction to upsert.\n        User:\n            required:\n                - role\n                - username\n                - state\n            type: object\n            properties:\n                name:\n                    type: string\n                    description: |-\n                        The resource name of the user.\n                         Format: users/{user}\n                role:\n                    enum:\n                        - ROLE_UNSPECIFIED\n                        - ADMIN\n                        - USER\n                    type: string\n                    description: The role of the user.\n                    format: enum\n                username:\n                    type: string\n                    description: Required. The unique username for login.\n                email:\n                    type: string\n                    description: Optional. The email address of the user.\n                displayName:\n                    type: string\n                    description: Optional. The display name of the user.\n                avatarUrl:\n                    type: string\n                    description: Optional. The avatar URL of the user.\n                description:\n                    type: string\n                    description: Optional. The description of the user.\n                password:\n                    writeOnly: true\n                    type: string\n                    description: Input only. The password for the user.\n                state:\n                    enum:\n                        - STATE_UNSPECIFIED\n                        - NORMAL\n                        - ARCHIVED\n                    type: string\n                    description: The state of the user.\n                    format: enum\n                createTime:\n                    readOnly: true\n                    type: string\n                    description: Output only. The creation timestamp.\n                    format: date-time\n                updateTime:\n                    readOnly: true\n                    type: string\n                    description: Output only. The last update timestamp.\n                    format: date-time\n        UserNotification:\n            type: object\n            properties:\n                name:\n                    readOnly: true\n                    type: string\n                    description: |-\n                        The resource name of the notification.\n                         Format: users/{user}/notifications/{notification}\n                sender:\n                    readOnly: true\n                    type: string\n                    description: |-\n                        The sender of the notification.\n                         Format: users/{user}\n                status:\n                    enum:\n                        - STATUS_UNSPECIFIED\n                        - UNREAD\n                        - ARCHIVED\n                    type: string\n                    description: The status of the notification.\n                    format: enum\n                createTime:\n                    readOnly: true\n                    type: string\n                    description: The creation timestamp.\n                    format: date-time\n                type:\n                    readOnly: true\n                    enum:\n                        - TYPE_UNSPECIFIED\n                        - MEMO_COMMENT\n                    type: string\n                    description: The type of the notification.\n                    format: enum\n                memoComment:\n                    readOnly: true\n                    allOf:\n                        - $ref: '#/components/schemas/UserNotification_MemoCommentPayload'\n        UserNotification_MemoCommentPayload:\n            type: object\n            properties:\n                memo:\n                    type: string\n                    description: |-\n                        The memo name of comment.\n                         Format: memos/{memo}\n                relatedMemo:\n                    type: string\n                    description: |-\n                        The name of related memo.\n                         Format: memos/{memo}\n        UserSetting:\n            type: object\n            properties:\n                name:\n                    type: string\n                    description: |-\n                        The name of the user setting.\n                         Format: users/{user}/settings/{setting}, {setting} is the key for the setting.\n                         For example, \"users/123/settings/GENERAL\" for general settings.\n                generalSetting:\n                    $ref: '#/components/schemas/UserSetting_GeneralSetting'\n                webhooksSetting:\n                    $ref: '#/components/schemas/UserSetting_WebhooksSetting'\n            description: User settings message\n        UserSetting_GeneralSetting:\n            type: object\n            properties:\n                locale:\n                    type: string\n                    description: The preferred locale of the user.\n                memoVisibility:\n                    type: string\n                    description: The default visibility of the memo.\n                theme:\n                    type: string\n                    description: |-\n                        The preferred theme of the user.\n                         This references a CSS file in the web/public/themes/ directory.\n                         If not set, the default theme will be used.\n            description: General user settings configuration.\n        UserSetting_WebhooksSetting:\n            type: object\n            properties:\n                webhooks:\n                    type: array\n                    items:\n                        $ref: '#/components/schemas/UserWebhook'\n                    description: List of user webhooks.\n            description: User webhooks configuration.\n        UserStats:\n            type: object\n            properties:\n                name:\n                    type: string\n                    description: |-\n                        The resource name of the user whose stats these are.\n                         Format: users/{user}\n                memoDisplayTimestamps:\n                    type: array\n                    items:\n                        type: string\n                        format: date-time\n                    description: The timestamps when the memos were displayed.\n                memoTypeStats:\n                    allOf:\n                        - $ref: '#/components/schemas/UserStats_MemoTypeStats'\n                    description: The stats of memo types.\n                tagCount:\n                    type: object\n                    additionalProperties:\n                        type: integer\n                        format: int32\n                    description: The count of tags.\n                pinnedMemos:\n                    type: array\n                    items:\n                        type: string\n                    description: The pinned memos of the user.\n                totalMemoCount:\n                    type: integer\n                    description: Total memo count.\n                    format: int32\n            description: User statistics messages\n        UserStats_MemoTypeStats:\n            type: object\n            properties:\n                linkCount:\n                    type: integer\n                    format: int32\n                codeCount:\n                    type: integer\n                    format: int32\n                todoCount:\n                    type: integer\n                    format: int32\n                undoCount:\n                    type: integer\n                    format: int32\n            description: Memo type statistics.\n        UserWebhook:\n            type: object\n            properties:\n                name:\n                    type: string\n                    description: |-\n                        The name of the webhook.\n                         Format: users/{user}/webhooks/{webhook}\n                url:\n                    type: string\n                    description: The URL to send the webhook to.\n                displayName:\n                    type: string\n                    description: Optional. Human-readable name for the webhook.\n                createTime:\n                    readOnly: true\n                    type: string\n                    description: The creation time of the webhook.\n                    format: date-time\n                updateTime:\n                    readOnly: true\n                    type: string\n                    description: The last update time of the webhook.\n                    format: date-time\n            description: UserWebhook represents a webhook owned by a user.\ntags:\n    - name: AttachmentService\n    - name: AuthService\n    - name: IdentityProviderService\n    - name: InstanceService\n    - name: MemoService\n    - name: ShortcutService\n    - name: UserService\n"
  },
  {
    "path": "proto/gen/store/attachment.pb.go",
    "content": "// Code generated by protoc-gen-go. DO NOT EDIT.\n// versions:\n// \tprotoc-gen-go v1.36.11\n// \tprotoc        (unknown)\n// source: store/attachment.proto\n\npackage store\n\nimport (\n\tprotoreflect \"google.golang.org/protobuf/reflect/protoreflect\"\n\tprotoimpl \"google.golang.org/protobuf/runtime/protoimpl\"\n\ttimestamppb \"google.golang.org/protobuf/types/known/timestamppb\"\n\treflect \"reflect\"\n\tsync \"sync\"\n\tunsafe \"unsafe\"\n)\n\nconst (\n\t// Verify that this generated code is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)\n\t// Verify that runtime/protoimpl is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)\n)\n\ntype AttachmentStorageType int32\n\nconst (\n\tAttachmentStorageType_ATTACHMENT_STORAGE_TYPE_UNSPECIFIED AttachmentStorageType = 0\n\t// Attachment is stored locally. AKA, local file system.\n\tAttachmentStorageType_LOCAL AttachmentStorageType = 1\n\t// Attachment is stored in S3.\n\tAttachmentStorageType_S3 AttachmentStorageType = 2\n\t// Attachment is stored in an external storage. The reference is a URL.\n\tAttachmentStorageType_EXTERNAL AttachmentStorageType = 3\n)\n\n// Enum value maps for AttachmentStorageType.\nvar (\n\tAttachmentStorageType_name = map[int32]string{\n\t\t0: \"ATTACHMENT_STORAGE_TYPE_UNSPECIFIED\",\n\t\t1: \"LOCAL\",\n\t\t2: \"S3\",\n\t\t3: \"EXTERNAL\",\n\t}\n\tAttachmentStorageType_value = map[string]int32{\n\t\t\"ATTACHMENT_STORAGE_TYPE_UNSPECIFIED\": 0,\n\t\t\"LOCAL\":                               1,\n\t\t\"S3\":                                  2,\n\t\t\"EXTERNAL\":                            3,\n\t}\n)\n\nfunc (x AttachmentStorageType) Enum() *AttachmentStorageType {\n\tp := new(AttachmentStorageType)\n\t*p = x\n\treturn p\n}\n\nfunc (x AttachmentStorageType) String() string {\n\treturn protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))\n}\n\nfunc (AttachmentStorageType) Descriptor() protoreflect.EnumDescriptor {\n\treturn file_store_attachment_proto_enumTypes[0].Descriptor()\n}\n\nfunc (AttachmentStorageType) Type() protoreflect.EnumType {\n\treturn &file_store_attachment_proto_enumTypes[0]\n}\n\nfunc (x AttachmentStorageType) Number() protoreflect.EnumNumber {\n\treturn protoreflect.EnumNumber(x)\n}\n\n// Deprecated: Use AttachmentStorageType.Descriptor instead.\nfunc (AttachmentStorageType) EnumDescriptor() ([]byte, []int) {\n\treturn file_store_attachment_proto_rawDescGZIP(), []int{0}\n}\n\ntype AttachmentPayload struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// Types that are valid to be assigned to Payload:\n\t//\n\t//\t*AttachmentPayload_S3Object_\n\tPayload       isAttachmentPayload_Payload `protobuf_oneof:\"payload\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *AttachmentPayload) Reset() {\n\t*x = AttachmentPayload{}\n\tmi := &file_store_attachment_proto_msgTypes[0]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *AttachmentPayload) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*AttachmentPayload) ProtoMessage() {}\n\nfunc (x *AttachmentPayload) ProtoReflect() protoreflect.Message {\n\tmi := &file_store_attachment_proto_msgTypes[0]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use AttachmentPayload.ProtoReflect.Descriptor instead.\nfunc (*AttachmentPayload) Descriptor() ([]byte, []int) {\n\treturn file_store_attachment_proto_rawDescGZIP(), []int{0}\n}\n\nfunc (x *AttachmentPayload) GetPayload() isAttachmentPayload_Payload {\n\tif x != nil {\n\t\treturn x.Payload\n\t}\n\treturn nil\n}\n\nfunc (x *AttachmentPayload) GetS3Object() *AttachmentPayload_S3Object {\n\tif x != nil {\n\t\tif x, ok := x.Payload.(*AttachmentPayload_S3Object_); ok {\n\t\t\treturn x.S3Object\n\t\t}\n\t}\n\treturn nil\n}\n\ntype isAttachmentPayload_Payload interface {\n\tisAttachmentPayload_Payload()\n}\n\ntype AttachmentPayload_S3Object_ struct {\n\tS3Object *AttachmentPayload_S3Object `protobuf:\"bytes,1,opt,name=s3_object,json=s3Object,proto3,oneof\"`\n}\n\nfunc (*AttachmentPayload_S3Object_) isAttachmentPayload_Payload() {}\n\ntype AttachmentPayload_S3Object struct {\n\tstate    protoimpl.MessageState `protogen:\"open.v1\"`\n\tS3Config *StorageS3Config       `protobuf:\"bytes,1,opt,name=s3_config,json=s3Config,proto3\" json:\"s3_config,omitempty\"`\n\t// key is the S3 object key.\n\tKey string `protobuf:\"bytes,2,opt,name=key,proto3\" json:\"key,omitempty\"`\n\t// last_presigned_time is the last time the object was presigned.\n\t// This is used to determine if the presigned URL is still valid.\n\tLastPresignedTime *timestamppb.Timestamp `protobuf:\"bytes,3,opt,name=last_presigned_time,json=lastPresignedTime,proto3\" json:\"last_presigned_time,omitempty\"`\n\tunknownFields     protoimpl.UnknownFields\n\tsizeCache         protoimpl.SizeCache\n}\n\nfunc (x *AttachmentPayload_S3Object) Reset() {\n\t*x = AttachmentPayload_S3Object{}\n\tmi := &file_store_attachment_proto_msgTypes[1]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *AttachmentPayload_S3Object) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*AttachmentPayload_S3Object) ProtoMessage() {}\n\nfunc (x *AttachmentPayload_S3Object) ProtoReflect() protoreflect.Message {\n\tmi := &file_store_attachment_proto_msgTypes[1]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use AttachmentPayload_S3Object.ProtoReflect.Descriptor instead.\nfunc (*AttachmentPayload_S3Object) Descriptor() ([]byte, []int) {\n\treturn file_store_attachment_proto_rawDescGZIP(), []int{0, 0}\n}\n\nfunc (x *AttachmentPayload_S3Object) GetS3Config() *StorageS3Config {\n\tif x != nil {\n\t\treturn x.S3Config\n\t}\n\treturn nil\n}\n\nfunc (x *AttachmentPayload_S3Object) GetKey() string {\n\tif x != nil {\n\t\treturn x.Key\n\t}\n\treturn \"\"\n}\n\nfunc (x *AttachmentPayload_S3Object) GetLastPresignedTime() *timestamppb.Timestamp {\n\tif x != nil {\n\t\treturn x.LastPresignedTime\n\t}\n\treturn nil\n}\n\nvar File_store_attachment_proto protoreflect.FileDescriptor\n\nconst file_store_attachment_proto_rawDesc = \"\" +\n\t\"\\n\" +\n\t\"\\x16store/attachment.proto\\x12\\vmemos.store\\x1a\\x1fgoogle/protobuf/timestamp.proto\\x1a\\x1cstore/instance_setting.proto\\\"\\x8c\\x02\\n\" +\n\t\"\\x11AttachmentPayload\\x12F\\n\" +\n\t\"\\ts3_object\\x18\\x01 \\x01(\\v2'.memos.store.AttachmentPayload.S3ObjectH\\x00R\\bs3Object\\x1a\\xa3\\x01\\n\" +\n\t\"\\bS3Object\\x129\\n\" +\n\t\"\\ts3_config\\x18\\x01 \\x01(\\v2\\x1c.memos.store.StorageS3ConfigR\\bs3Config\\x12\\x10\\n\" +\n\t\"\\x03key\\x18\\x02 \\x01(\\tR\\x03key\\x12J\\n\" +\n\t\"\\x13last_presigned_time\\x18\\x03 \\x01(\\v2\\x1a.google.protobuf.TimestampR\\x11lastPresignedTimeB\\t\\n\" +\n\t\"\\apayload*a\\n\" +\n\t\"\\x15AttachmentStorageType\\x12'\\n\" +\n\t\"#ATTACHMENT_STORAGE_TYPE_UNSPECIFIED\\x10\\x00\\x12\\t\\n\" +\n\t\"\\x05LOCAL\\x10\\x01\\x12\\x06\\n\" +\n\t\"\\x02S3\\x10\\x02\\x12\\f\\n\" +\n\t\"\\bEXTERNAL\\x10\\x03B\\x9a\\x01\\n\" +\n\t\"\\x0fcom.memos.storeB\\x0fAttachmentProtoP\\x01Z)github.com/usememos/memos/proto/gen/store\\xa2\\x02\\x03MSX\\xaa\\x02\\vMemos.Store\\xca\\x02\\vMemos\\\\Store\\xe2\\x02\\x17Memos\\\\Store\\\\GPBMetadata\\xea\\x02\\fMemos::Storeb\\x06proto3\"\n\nvar (\n\tfile_store_attachment_proto_rawDescOnce sync.Once\n\tfile_store_attachment_proto_rawDescData []byte\n)\n\nfunc file_store_attachment_proto_rawDescGZIP() []byte {\n\tfile_store_attachment_proto_rawDescOnce.Do(func() {\n\t\tfile_store_attachment_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_store_attachment_proto_rawDesc), len(file_store_attachment_proto_rawDesc)))\n\t})\n\treturn file_store_attachment_proto_rawDescData\n}\n\nvar file_store_attachment_proto_enumTypes = make([]protoimpl.EnumInfo, 1)\nvar file_store_attachment_proto_msgTypes = make([]protoimpl.MessageInfo, 2)\nvar file_store_attachment_proto_goTypes = []any{\n\t(AttachmentStorageType)(0),         // 0: memos.store.AttachmentStorageType\n\t(*AttachmentPayload)(nil),          // 1: memos.store.AttachmentPayload\n\t(*AttachmentPayload_S3Object)(nil), // 2: memos.store.AttachmentPayload.S3Object\n\t(*StorageS3Config)(nil),            // 3: memos.store.StorageS3Config\n\t(*timestamppb.Timestamp)(nil),      // 4: google.protobuf.Timestamp\n}\nvar file_store_attachment_proto_depIdxs = []int32{\n\t2, // 0: memos.store.AttachmentPayload.s3_object:type_name -> memos.store.AttachmentPayload.S3Object\n\t3, // 1: memos.store.AttachmentPayload.S3Object.s3_config:type_name -> memos.store.StorageS3Config\n\t4, // 2: memos.store.AttachmentPayload.S3Object.last_presigned_time:type_name -> google.protobuf.Timestamp\n\t3, // [3:3] is the sub-list for method output_type\n\t3, // [3:3] is the sub-list for method input_type\n\t3, // [3:3] is the sub-list for extension type_name\n\t3, // [3:3] is the sub-list for extension extendee\n\t0, // [0:3] is the sub-list for field type_name\n}\n\nfunc init() { file_store_attachment_proto_init() }\nfunc file_store_attachment_proto_init() {\n\tif File_store_attachment_proto != nil {\n\t\treturn\n\t}\n\tfile_store_instance_setting_proto_init()\n\tfile_store_attachment_proto_msgTypes[0].OneofWrappers = []any{\n\t\t(*AttachmentPayload_S3Object_)(nil),\n\t}\n\ttype x struct{}\n\tout := protoimpl.TypeBuilder{\n\t\tFile: protoimpl.DescBuilder{\n\t\t\tGoPackagePath: reflect.TypeOf(x{}).PkgPath(),\n\t\t\tRawDescriptor: unsafe.Slice(unsafe.StringData(file_store_attachment_proto_rawDesc), len(file_store_attachment_proto_rawDesc)),\n\t\t\tNumEnums:      1,\n\t\t\tNumMessages:   2,\n\t\t\tNumExtensions: 0,\n\t\t\tNumServices:   0,\n\t\t},\n\t\tGoTypes:           file_store_attachment_proto_goTypes,\n\t\tDependencyIndexes: file_store_attachment_proto_depIdxs,\n\t\tEnumInfos:         file_store_attachment_proto_enumTypes,\n\t\tMessageInfos:      file_store_attachment_proto_msgTypes,\n\t}.Build()\n\tFile_store_attachment_proto = out.File\n\tfile_store_attachment_proto_goTypes = nil\n\tfile_store_attachment_proto_depIdxs = nil\n}\n"
  },
  {
    "path": "proto/gen/store/idp.pb.go",
    "content": "// Code generated by protoc-gen-go. DO NOT EDIT.\n// versions:\n// \tprotoc-gen-go v1.36.11\n// \tprotoc        (unknown)\n// source: store/idp.proto\n\npackage store\n\nimport (\n\tprotoreflect \"google.golang.org/protobuf/reflect/protoreflect\"\n\tprotoimpl \"google.golang.org/protobuf/runtime/protoimpl\"\n\treflect \"reflect\"\n\tsync \"sync\"\n\tunsafe \"unsafe\"\n)\n\nconst (\n\t// Verify that this generated code is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)\n\t// Verify that runtime/protoimpl is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)\n)\n\ntype IdentityProvider_Type int32\n\nconst (\n\tIdentityProvider_TYPE_UNSPECIFIED IdentityProvider_Type = 0\n\tIdentityProvider_OAUTH2           IdentityProvider_Type = 1\n)\n\n// Enum value maps for IdentityProvider_Type.\nvar (\n\tIdentityProvider_Type_name = map[int32]string{\n\t\t0: \"TYPE_UNSPECIFIED\",\n\t\t1: \"OAUTH2\",\n\t}\n\tIdentityProvider_Type_value = map[string]int32{\n\t\t\"TYPE_UNSPECIFIED\": 0,\n\t\t\"OAUTH2\":           1,\n\t}\n)\n\nfunc (x IdentityProvider_Type) Enum() *IdentityProvider_Type {\n\tp := new(IdentityProvider_Type)\n\t*p = x\n\treturn p\n}\n\nfunc (x IdentityProvider_Type) String() string {\n\treturn protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))\n}\n\nfunc (IdentityProvider_Type) Descriptor() protoreflect.EnumDescriptor {\n\treturn file_store_idp_proto_enumTypes[0].Descriptor()\n}\n\nfunc (IdentityProvider_Type) Type() protoreflect.EnumType {\n\treturn &file_store_idp_proto_enumTypes[0]\n}\n\nfunc (x IdentityProvider_Type) Number() protoreflect.EnumNumber {\n\treturn protoreflect.EnumNumber(x)\n}\n\n// Deprecated: Use IdentityProvider_Type.Descriptor instead.\nfunc (IdentityProvider_Type) EnumDescriptor() ([]byte, []int) {\n\treturn file_store_idp_proto_rawDescGZIP(), []int{0, 0}\n}\n\ntype IdentityProvider struct {\n\tstate            protoimpl.MessageState  `protogen:\"open.v1\"`\n\tId               int32                   `protobuf:\"varint,1,opt,name=id,proto3\" json:\"id,omitempty\"`\n\tName             string                  `protobuf:\"bytes,2,opt,name=name,proto3\" json:\"name,omitempty\"`\n\tType             IdentityProvider_Type   `protobuf:\"varint,3,opt,name=type,proto3,enum=memos.store.IdentityProvider_Type\" json:\"type,omitempty\"`\n\tIdentifierFilter string                  `protobuf:\"bytes,4,opt,name=identifier_filter,json=identifierFilter,proto3\" json:\"identifier_filter,omitempty\"`\n\tConfig           *IdentityProviderConfig `protobuf:\"bytes,5,opt,name=config,proto3\" json:\"config,omitempty\"`\n\tUid              string                  `protobuf:\"bytes,6,opt,name=uid,proto3\" json:\"uid,omitempty\"`\n\tunknownFields    protoimpl.UnknownFields\n\tsizeCache        protoimpl.SizeCache\n}\n\nfunc (x *IdentityProvider) Reset() {\n\t*x = IdentityProvider{}\n\tmi := &file_store_idp_proto_msgTypes[0]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *IdentityProvider) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*IdentityProvider) ProtoMessage() {}\n\nfunc (x *IdentityProvider) ProtoReflect() protoreflect.Message {\n\tmi := &file_store_idp_proto_msgTypes[0]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use IdentityProvider.ProtoReflect.Descriptor instead.\nfunc (*IdentityProvider) Descriptor() ([]byte, []int) {\n\treturn file_store_idp_proto_rawDescGZIP(), []int{0}\n}\n\nfunc (x *IdentityProvider) GetId() int32 {\n\tif x != nil {\n\t\treturn x.Id\n\t}\n\treturn 0\n}\n\nfunc (x *IdentityProvider) GetName() string {\n\tif x != nil {\n\t\treturn x.Name\n\t}\n\treturn \"\"\n}\n\nfunc (x *IdentityProvider) GetType() IdentityProvider_Type {\n\tif x != nil {\n\t\treturn x.Type\n\t}\n\treturn IdentityProvider_TYPE_UNSPECIFIED\n}\n\nfunc (x *IdentityProvider) GetIdentifierFilter() string {\n\tif x != nil {\n\t\treturn x.IdentifierFilter\n\t}\n\treturn \"\"\n}\n\nfunc (x *IdentityProvider) GetConfig() *IdentityProviderConfig {\n\tif x != nil {\n\t\treturn x.Config\n\t}\n\treturn nil\n}\n\nfunc (x *IdentityProvider) GetUid() string {\n\tif x != nil {\n\t\treturn x.Uid\n\t}\n\treturn \"\"\n}\n\ntype IdentityProviderConfig struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// Types that are valid to be assigned to Config:\n\t//\n\t//\t*IdentityProviderConfig_Oauth2Config\n\tConfig        isIdentityProviderConfig_Config `protobuf_oneof:\"config\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *IdentityProviderConfig) Reset() {\n\t*x = IdentityProviderConfig{}\n\tmi := &file_store_idp_proto_msgTypes[1]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *IdentityProviderConfig) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*IdentityProviderConfig) ProtoMessage() {}\n\nfunc (x *IdentityProviderConfig) ProtoReflect() protoreflect.Message {\n\tmi := &file_store_idp_proto_msgTypes[1]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use IdentityProviderConfig.ProtoReflect.Descriptor instead.\nfunc (*IdentityProviderConfig) Descriptor() ([]byte, []int) {\n\treturn file_store_idp_proto_rawDescGZIP(), []int{1}\n}\n\nfunc (x *IdentityProviderConfig) GetConfig() isIdentityProviderConfig_Config {\n\tif x != nil {\n\t\treturn x.Config\n\t}\n\treturn nil\n}\n\nfunc (x *IdentityProviderConfig) GetOauth2Config() *OAuth2Config {\n\tif x != nil {\n\t\tif x, ok := x.Config.(*IdentityProviderConfig_Oauth2Config); ok {\n\t\t\treturn x.Oauth2Config\n\t\t}\n\t}\n\treturn nil\n}\n\ntype isIdentityProviderConfig_Config interface {\n\tisIdentityProviderConfig_Config()\n}\n\ntype IdentityProviderConfig_Oauth2Config struct {\n\tOauth2Config *OAuth2Config `protobuf:\"bytes,1,opt,name=oauth2_config,json=oauth2Config,proto3,oneof\"`\n}\n\nfunc (*IdentityProviderConfig_Oauth2Config) isIdentityProviderConfig_Config() {}\n\ntype FieldMapping struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tIdentifier    string                 `protobuf:\"bytes,1,opt,name=identifier,proto3\" json:\"identifier,omitempty\"`\n\tDisplayName   string                 `protobuf:\"bytes,2,opt,name=display_name,json=displayName,proto3\" json:\"display_name,omitempty\"`\n\tEmail         string                 `protobuf:\"bytes,3,opt,name=email,proto3\" json:\"email,omitempty\"`\n\tAvatarUrl     string                 `protobuf:\"bytes,4,opt,name=avatar_url,json=avatarUrl,proto3\" json:\"avatar_url,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *FieldMapping) Reset() {\n\t*x = FieldMapping{}\n\tmi := &file_store_idp_proto_msgTypes[2]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *FieldMapping) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*FieldMapping) ProtoMessage() {}\n\nfunc (x *FieldMapping) ProtoReflect() protoreflect.Message {\n\tmi := &file_store_idp_proto_msgTypes[2]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use FieldMapping.ProtoReflect.Descriptor instead.\nfunc (*FieldMapping) Descriptor() ([]byte, []int) {\n\treturn file_store_idp_proto_rawDescGZIP(), []int{2}\n}\n\nfunc (x *FieldMapping) GetIdentifier() string {\n\tif x != nil {\n\t\treturn x.Identifier\n\t}\n\treturn \"\"\n}\n\nfunc (x *FieldMapping) GetDisplayName() string {\n\tif x != nil {\n\t\treturn x.DisplayName\n\t}\n\treturn \"\"\n}\n\nfunc (x *FieldMapping) GetEmail() string {\n\tif x != nil {\n\t\treturn x.Email\n\t}\n\treturn \"\"\n}\n\nfunc (x *FieldMapping) GetAvatarUrl() string {\n\tif x != nil {\n\t\treturn x.AvatarUrl\n\t}\n\treturn \"\"\n}\n\ntype OAuth2Config struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tClientId      string                 `protobuf:\"bytes,1,opt,name=client_id,json=clientId,proto3\" json:\"client_id,omitempty\"`\n\tClientSecret  string                 `protobuf:\"bytes,2,opt,name=client_secret,json=clientSecret,proto3\" json:\"client_secret,omitempty\"`\n\tAuthUrl       string                 `protobuf:\"bytes,3,opt,name=auth_url,json=authUrl,proto3\" json:\"auth_url,omitempty\"`\n\tTokenUrl      string                 `protobuf:\"bytes,4,opt,name=token_url,json=tokenUrl,proto3\" json:\"token_url,omitempty\"`\n\tUserInfoUrl   string                 `protobuf:\"bytes,5,opt,name=user_info_url,json=userInfoUrl,proto3\" json:\"user_info_url,omitempty\"`\n\tScopes        []string               `protobuf:\"bytes,6,rep,name=scopes,proto3\" json:\"scopes,omitempty\"`\n\tFieldMapping  *FieldMapping          `protobuf:\"bytes,7,opt,name=field_mapping,json=fieldMapping,proto3\" json:\"field_mapping,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *OAuth2Config) Reset() {\n\t*x = OAuth2Config{}\n\tmi := &file_store_idp_proto_msgTypes[3]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *OAuth2Config) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*OAuth2Config) ProtoMessage() {}\n\nfunc (x *OAuth2Config) ProtoReflect() protoreflect.Message {\n\tmi := &file_store_idp_proto_msgTypes[3]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use OAuth2Config.ProtoReflect.Descriptor instead.\nfunc (*OAuth2Config) Descriptor() ([]byte, []int) {\n\treturn file_store_idp_proto_rawDescGZIP(), []int{3}\n}\n\nfunc (x *OAuth2Config) GetClientId() string {\n\tif x != nil {\n\t\treturn x.ClientId\n\t}\n\treturn \"\"\n}\n\nfunc (x *OAuth2Config) GetClientSecret() string {\n\tif x != nil {\n\t\treturn x.ClientSecret\n\t}\n\treturn \"\"\n}\n\nfunc (x *OAuth2Config) GetAuthUrl() string {\n\tif x != nil {\n\t\treturn x.AuthUrl\n\t}\n\treturn \"\"\n}\n\nfunc (x *OAuth2Config) GetTokenUrl() string {\n\tif x != nil {\n\t\treturn x.TokenUrl\n\t}\n\treturn \"\"\n}\n\nfunc (x *OAuth2Config) GetUserInfoUrl() string {\n\tif x != nil {\n\t\treturn x.UserInfoUrl\n\t}\n\treturn \"\"\n}\n\nfunc (x *OAuth2Config) GetScopes() []string {\n\tif x != nil {\n\t\treturn x.Scopes\n\t}\n\treturn nil\n}\n\nfunc (x *OAuth2Config) GetFieldMapping() *FieldMapping {\n\tif x != nil {\n\t\treturn x.FieldMapping\n\t}\n\treturn nil\n}\n\nvar File_store_idp_proto protoreflect.FileDescriptor\n\nconst file_store_idp_proto_rawDesc = \"\" +\n\t\"\\n\" +\n\t\"\\x0fstore/idp.proto\\x12\\vmemos.store\\\"\\x94\\x02\\n\" +\n\t\"\\x10IdentityProvider\\x12\\x0e\\n\" +\n\t\"\\x02id\\x18\\x01 \\x01(\\x05R\\x02id\\x12\\x12\\n\" +\n\t\"\\x04name\\x18\\x02 \\x01(\\tR\\x04name\\x126\\n\" +\n\t\"\\x04type\\x18\\x03 \\x01(\\x0e2\\\".memos.store.IdentityProvider.TypeR\\x04type\\x12+\\n\" +\n\t\"\\x11identifier_filter\\x18\\x04 \\x01(\\tR\\x10identifierFilter\\x12;\\n\" +\n\t\"\\x06config\\x18\\x05 \\x01(\\v2#.memos.store.IdentityProviderConfigR\\x06config\\x12\\x10\\n\" +\n\t\"\\x03uid\\x18\\x06 \\x01(\\tR\\x03uid\\\"(\\n\" +\n\t\"\\x04Type\\x12\\x14\\n\" +\n\t\"\\x10TYPE_UNSPECIFIED\\x10\\x00\\x12\\n\" +\n\t\"\\n\" +\n\t\"\\x06OAUTH2\\x10\\x01\\\"d\\n\" +\n\t\"\\x16IdentityProviderConfig\\x12@\\n\" +\n\t\"\\roauth2_config\\x18\\x01 \\x01(\\v2\\x19.memos.store.OAuth2ConfigH\\x00R\\foauth2ConfigB\\b\\n\" +\n\t\"\\x06config\\\"\\x86\\x01\\n\" +\n\t\"\\fFieldMapping\\x12\\x1e\\n\" +\n\t\"\\n\" +\n\t\"identifier\\x18\\x01 \\x01(\\tR\\n\" +\n\t\"identifier\\x12!\\n\" +\n\t\"\\fdisplay_name\\x18\\x02 \\x01(\\tR\\vdisplayName\\x12\\x14\\n\" +\n\t\"\\x05email\\x18\\x03 \\x01(\\tR\\x05email\\x12\\x1d\\n\" +\n\t\"\\n\" +\n\t\"avatar_url\\x18\\x04 \\x01(\\tR\\tavatarUrl\\\"\\x84\\x02\\n\" +\n\t\"\\fOAuth2Config\\x12\\x1b\\n\" +\n\t\"\\tclient_id\\x18\\x01 \\x01(\\tR\\bclientId\\x12#\\n\" +\n\t\"\\rclient_secret\\x18\\x02 \\x01(\\tR\\fclientSecret\\x12\\x19\\n\" +\n\t\"\\bauth_url\\x18\\x03 \\x01(\\tR\\aauthUrl\\x12\\x1b\\n\" +\n\t\"\\ttoken_url\\x18\\x04 \\x01(\\tR\\btokenUrl\\x12\\\"\\n\" +\n\t\"\\ruser_info_url\\x18\\x05 \\x01(\\tR\\vuserInfoUrl\\x12\\x16\\n\" +\n\t\"\\x06scopes\\x18\\x06 \\x03(\\tR\\x06scopes\\x12>\\n\" +\n\t\"\\rfield_mapping\\x18\\a \\x01(\\v2\\x19.memos.store.FieldMappingR\\ffieldMappingB\\x93\\x01\\n\" +\n\t\"\\x0fcom.memos.storeB\\bIdpProtoP\\x01Z)github.com/usememos/memos/proto/gen/store\\xa2\\x02\\x03MSX\\xaa\\x02\\vMemos.Store\\xca\\x02\\vMemos\\\\Store\\xe2\\x02\\x17Memos\\\\Store\\\\GPBMetadata\\xea\\x02\\fMemos::Storeb\\x06proto3\"\n\nvar (\n\tfile_store_idp_proto_rawDescOnce sync.Once\n\tfile_store_idp_proto_rawDescData []byte\n)\n\nfunc file_store_idp_proto_rawDescGZIP() []byte {\n\tfile_store_idp_proto_rawDescOnce.Do(func() {\n\t\tfile_store_idp_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_store_idp_proto_rawDesc), len(file_store_idp_proto_rawDesc)))\n\t})\n\treturn file_store_idp_proto_rawDescData\n}\n\nvar file_store_idp_proto_enumTypes = make([]protoimpl.EnumInfo, 1)\nvar file_store_idp_proto_msgTypes = make([]protoimpl.MessageInfo, 4)\nvar file_store_idp_proto_goTypes = []any{\n\t(IdentityProvider_Type)(0),     // 0: memos.store.IdentityProvider.Type\n\t(*IdentityProvider)(nil),       // 1: memos.store.IdentityProvider\n\t(*IdentityProviderConfig)(nil), // 2: memos.store.IdentityProviderConfig\n\t(*FieldMapping)(nil),           // 3: memos.store.FieldMapping\n\t(*OAuth2Config)(nil),           // 4: memos.store.OAuth2Config\n}\nvar file_store_idp_proto_depIdxs = []int32{\n\t0, // 0: memos.store.IdentityProvider.type:type_name -> memos.store.IdentityProvider.Type\n\t2, // 1: memos.store.IdentityProvider.config:type_name -> memos.store.IdentityProviderConfig\n\t4, // 2: memos.store.IdentityProviderConfig.oauth2_config:type_name -> memos.store.OAuth2Config\n\t3, // 3: memos.store.OAuth2Config.field_mapping:type_name -> memos.store.FieldMapping\n\t4, // [4:4] is the sub-list for method output_type\n\t4, // [4:4] is the sub-list for method input_type\n\t4, // [4:4] is the sub-list for extension type_name\n\t4, // [4:4] is the sub-list for extension extendee\n\t0, // [0:4] is the sub-list for field type_name\n}\n\nfunc init() { file_store_idp_proto_init() }\nfunc file_store_idp_proto_init() {\n\tif File_store_idp_proto != nil {\n\t\treturn\n\t}\n\tfile_store_idp_proto_msgTypes[1].OneofWrappers = []any{\n\t\t(*IdentityProviderConfig_Oauth2Config)(nil),\n\t}\n\ttype x struct{}\n\tout := protoimpl.TypeBuilder{\n\t\tFile: protoimpl.DescBuilder{\n\t\t\tGoPackagePath: reflect.TypeOf(x{}).PkgPath(),\n\t\t\tRawDescriptor: unsafe.Slice(unsafe.StringData(file_store_idp_proto_rawDesc), len(file_store_idp_proto_rawDesc)),\n\t\t\tNumEnums:      1,\n\t\t\tNumMessages:   4,\n\t\t\tNumExtensions: 0,\n\t\t\tNumServices:   0,\n\t\t},\n\t\tGoTypes:           file_store_idp_proto_goTypes,\n\t\tDependencyIndexes: file_store_idp_proto_depIdxs,\n\t\tEnumInfos:         file_store_idp_proto_enumTypes,\n\t\tMessageInfos:      file_store_idp_proto_msgTypes,\n\t}.Build()\n\tFile_store_idp_proto = out.File\n\tfile_store_idp_proto_goTypes = nil\n\tfile_store_idp_proto_depIdxs = nil\n}\n"
  },
  {
    "path": "proto/gen/store/inbox.pb.go",
    "content": "// Code generated by protoc-gen-go. DO NOT EDIT.\n// versions:\n// \tprotoc-gen-go v1.36.11\n// \tprotoc        (unknown)\n// source: store/inbox.proto\n\npackage store\n\nimport (\n\tprotoreflect \"google.golang.org/protobuf/reflect/protoreflect\"\n\tprotoimpl \"google.golang.org/protobuf/runtime/protoimpl\"\n\treflect \"reflect\"\n\tsync \"sync\"\n\tunsafe \"unsafe\"\n)\n\nconst (\n\t// Verify that this generated code is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)\n\t// Verify that runtime/protoimpl is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)\n)\n\ntype InboxMessage_Type int32\n\nconst (\n\tInboxMessage_TYPE_UNSPECIFIED InboxMessage_Type = 0\n\t// Memo comment notification.\n\tInboxMessage_MEMO_COMMENT InboxMessage_Type = 1\n)\n\n// Enum value maps for InboxMessage_Type.\nvar (\n\tInboxMessage_Type_name = map[int32]string{\n\t\t0: \"TYPE_UNSPECIFIED\",\n\t\t1: \"MEMO_COMMENT\",\n\t}\n\tInboxMessage_Type_value = map[string]int32{\n\t\t\"TYPE_UNSPECIFIED\": 0,\n\t\t\"MEMO_COMMENT\":     1,\n\t}\n)\n\nfunc (x InboxMessage_Type) Enum() *InboxMessage_Type {\n\tp := new(InboxMessage_Type)\n\t*p = x\n\treturn p\n}\n\nfunc (x InboxMessage_Type) String() string {\n\treturn protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))\n}\n\nfunc (InboxMessage_Type) Descriptor() protoreflect.EnumDescriptor {\n\treturn file_store_inbox_proto_enumTypes[0].Descriptor()\n}\n\nfunc (InboxMessage_Type) Type() protoreflect.EnumType {\n\treturn &file_store_inbox_proto_enumTypes[0]\n}\n\nfunc (x InboxMessage_Type) Number() protoreflect.EnumNumber {\n\treturn protoreflect.EnumNumber(x)\n}\n\n// Deprecated: Use InboxMessage_Type.Descriptor instead.\nfunc (InboxMessage_Type) EnumDescriptor() ([]byte, []int) {\n\treturn file_store_inbox_proto_rawDescGZIP(), []int{0, 0}\n}\n\ntype InboxMessage struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// The type of the inbox message.\n\tType InboxMessage_Type `protobuf:\"varint,1,opt,name=type,proto3,enum=memos.store.InboxMessage_Type\" json:\"type,omitempty\"`\n\t// Types that are valid to be assigned to Payload:\n\t//\n\t//\t*InboxMessage_MemoComment\n\tPayload       isInboxMessage_Payload `protobuf_oneof:\"payload\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *InboxMessage) Reset() {\n\t*x = InboxMessage{}\n\tmi := &file_store_inbox_proto_msgTypes[0]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *InboxMessage) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*InboxMessage) ProtoMessage() {}\n\nfunc (x *InboxMessage) ProtoReflect() protoreflect.Message {\n\tmi := &file_store_inbox_proto_msgTypes[0]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use InboxMessage.ProtoReflect.Descriptor instead.\nfunc (*InboxMessage) Descriptor() ([]byte, []int) {\n\treturn file_store_inbox_proto_rawDescGZIP(), []int{0}\n}\n\nfunc (x *InboxMessage) GetType() InboxMessage_Type {\n\tif x != nil {\n\t\treturn x.Type\n\t}\n\treturn InboxMessage_TYPE_UNSPECIFIED\n}\n\nfunc (x *InboxMessage) GetPayload() isInboxMessage_Payload {\n\tif x != nil {\n\t\treturn x.Payload\n\t}\n\treturn nil\n}\n\nfunc (x *InboxMessage) GetMemoComment() *InboxMessage_MemoCommentPayload {\n\tif x != nil {\n\t\tif x, ok := x.Payload.(*InboxMessage_MemoComment); ok {\n\t\t\treturn x.MemoComment\n\t\t}\n\t}\n\treturn nil\n}\n\ntype isInboxMessage_Payload interface {\n\tisInboxMessage_Payload()\n}\n\ntype InboxMessage_MemoComment struct {\n\tMemoComment *InboxMessage_MemoCommentPayload `protobuf:\"bytes,2,opt,name=memo_comment,json=memoComment,proto3,oneof\"`\n}\n\nfunc (*InboxMessage_MemoComment) isInboxMessage_Payload() {}\n\ntype InboxMessage_MemoCommentPayload struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tMemoId        int32                  `protobuf:\"varint,1,opt,name=memo_id,json=memoId,proto3\" json:\"memo_id,omitempty\"`\n\tRelatedMemoId int32                  `protobuf:\"varint,2,opt,name=related_memo_id,json=relatedMemoId,proto3\" json:\"related_memo_id,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *InboxMessage_MemoCommentPayload) Reset() {\n\t*x = InboxMessage_MemoCommentPayload{}\n\tmi := &file_store_inbox_proto_msgTypes[1]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *InboxMessage_MemoCommentPayload) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*InboxMessage_MemoCommentPayload) ProtoMessage() {}\n\nfunc (x *InboxMessage_MemoCommentPayload) ProtoReflect() protoreflect.Message {\n\tmi := &file_store_inbox_proto_msgTypes[1]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use InboxMessage_MemoCommentPayload.ProtoReflect.Descriptor instead.\nfunc (*InboxMessage_MemoCommentPayload) Descriptor() ([]byte, []int) {\n\treturn file_store_inbox_proto_rawDescGZIP(), []int{0, 0}\n}\n\nfunc (x *InboxMessage_MemoCommentPayload) GetMemoId() int32 {\n\tif x != nil {\n\t\treturn x.MemoId\n\t}\n\treturn 0\n}\n\nfunc (x *InboxMessage_MemoCommentPayload) GetRelatedMemoId() int32 {\n\tif x != nil {\n\t\treturn x.RelatedMemoId\n\t}\n\treturn 0\n}\n\nvar File_store_inbox_proto protoreflect.FileDescriptor\n\nconst file_store_inbox_proto_rawDesc = \"\" +\n\t\"\\n\" +\n\t\"\\x11store/inbox.proto\\x12\\vmemos.store\\\"\\xa7\\x02\\n\" +\n\t\"\\fInboxMessage\\x122\\n\" +\n\t\"\\x04type\\x18\\x01 \\x01(\\x0e2\\x1e.memos.store.InboxMessage.TypeR\\x04type\\x12Q\\n\" +\n\t\"\\fmemo_comment\\x18\\x02 \\x01(\\v2,.memos.store.InboxMessage.MemoCommentPayloadH\\x00R\\vmemoComment\\x1aU\\n\" +\n\t\"\\x12MemoCommentPayload\\x12\\x17\\n\" +\n\t\"\\amemo_id\\x18\\x01 \\x01(\\x05R\\x06memoId\\x12&\\n\" +\n\t\"\\x0frelated_memo_id\\x18\\x02 \\x01(\\x05R\\rrelatedMemoId\\\".\\n\" +\n\t\"\\x04Type\\x12\\x14\\n\" +\n\t\"\\x10TYPE_UNSPECIFIED\\x10\\x00\\x12\\x10\\n\" +\n\t\"\\fMEMO_COMMENT\\x10\\x01B\\t\\n\" +\n\t\"\\apayloadB\\x95\\x01\\n\" +\n\t\"\\x0fcom.memos.storeB\\n\" +\n\t\"InboxProtoP\\x01Z)github.com/usememos/memos/proto/gen/store\\xa2\\x02\\x03MSX\\xaa\\x02\\vMemos.Store\\xca\\x02\\vMemos\\\\Store\\xe2\\x02\\x17Memos\\\\Store\\\\GPBMetadata\\xea\\x02\\fMemos::Storeb\\x06proto3\"\n\nvar (\n\tfile_store_inbox_proto_rawDescOnce sync.Once\n\tfile_store_inbox_proto_rawDescData []byte\n)\n\nfunc file_store_inbox_proto_rawDescGZIP() []byte {\n\tfile_store_inbox_proto_rawDescOnce.Do(func() {\n\t\tfile_store_inbox_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_store_inbox_proto_rawDesc), len(file_store_inbox_proto_rawDesc)))\n\t})\n\treturn file_store_inbox_proto_rawDescData\n}\n\nvar file_store_inbox_proto_enumTypes = make([]protoimpl.EnumInfo, 1)\nvar file_store_inbox_proto_msgTypes = make([]protoimpl.MessageInfo, 2)\nvar file_store_inbox_proto_goTypes = []any{\n\t(InboxMessage_Type)(0),                  // 0: memos.store.InboxMessage.Type\n\t(*InboxMessage)(nil),                    // 1: memos.store.InboxMessage\n\t(*InboxMessage_MemoCommentPayload)(nil), // 2: memos.store.InboxMessage.MemoCommentPayload\n}\nvar file_store_inbox_proto_depIdxs = []int32{\n\t0, // 0: memos.store.InboxMessage.type:type_name -> memos.store.InboxMessage.Type\n\t2, // 1: memos.store.InboxMessage.memo_comment:type_name -> memos.store.InboxMessage.MemoCommentPayload\n\t2, // [2:2] is the sub-list for method output_type\n\t2, // [2:2] is the sub-list for method input_type\n\t2, // [2:2] is the sub-list for extension type_name\n\t2, // [2:2] is the sub-list for extension extendee\n\t0, // [0:2] is the sub-list for field type_name\n}\n\nfunc init() { file_store_inbox_proto_init() }\nfunc file_store_inbox_proto_init() {\n\tif File_store_inbox_proto != nil {\n\t\treturn\n\t}\n\tfile_store_inbox_proto_msgTypes[0].OneofWrappers = []any{\n\t\t(*InboxMessage_MemoComment)(nil),\n\t}\n\ttype x struct{}\n\tout := protoimpl.TypeBuilder{\n\t\tFile: protoimpl.DescBuilder{\n\t\t\tGoPackagePath: reflect.TypeOf(x{}).PkgPath(),\n\t\t\tRawDescriptor: unsafe.Slice(unsafe.StringData(file_store_inbox_proto_rawDesc), len(file_store_inbox_proto_rawDesc)),\n\t\t\tNumEnums:      1,\n\t\t\tNumMessages:   2,\n\t\t\tNumExtensions: 0,\n\t\t\tNumServices:   0,\n\t\t},\n\t\tGoTypes:           file_store_inbox_proto_goTypes,\n\t\tDependencyIndexes: file_store_inbox_proto_depIdxs,\n\t\tEnumInfos:         file_store_inbox_proto_enumTypes,\n\t\tMessageInfos:      file_store_inbox_proto_msgTypes,\n\t}.Build()\n\tFile_store_inbox_proto = out.File\n\tfile_store_inbox_proto_goTypes = nil\n\tfile_store_inbox_proto_depIdxs = nil\n}\n"
  },
  {
    "path": "proto/gen/store/instance_setting.pb.go",
    "content": "// Code generated by protoc-gen-go. DO NOT EDIT.\n// versions:\n// \tprotoc-gen-go v1.36.11\n// \tprotoc        (unknown)\n// source: store/instance_setting.proto\n\npackage store\n\nimport (\n\tcolor \"google.golang.org/genproto/googleapis/type/color\"\n\tprotoreflect \"google.golang.org/protobuf/reflect/protoreflect\"\n\tprotoimpl \"google.golang.org/protobuf/runtime/protoimpl\"\n\treflect \"reflect\"\n\tsync \"sync\"\n\tunsafe \"unsafe\"\n)\n\nconst (\n\t// Verify that this generated code is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)\n\t// Verify that runtime/protoimpl is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)\n)\n\ntype InstanceSettingKey int32\n\nconst (\n\tInstanceSettingKey_INSTANCE_SETTING_KEY_UNSPECIFIED InstanceSettingKey = 0\n\t// BASIC is the key for basic settings.\n\tInstanceSettingKey_BASIC InstanceSettingKey = 1\n\t// GENERAL is the key for general settings.\n\tInstanceSettingKey_GENERAL InstanceSettingKey = 2\n\t// STORAGE is the key for storage settings.\n\tInstanceSettingKey_STORAGE InstanceSettingKey = 3\n\t// MEMO_RELATED is the key for memo related settings.\n\tInstanceSettingKey_MEMO_RELATED InstanceSettingKey = 4\n\t// TAGS is the key for tag metadata.\n\tInstanceSettingKey_TAGS InstanceSettingKey = 5\n\t// NOTIFICATION is the key for notification transport settings.\n\tInstanceSettingKey_NOTIFICATION InstanceSettingKey = 6\n)\n\n// Enum value maps for InstanceSettingKey.\nvar (\n\tInstanceSettingKey_name = map[int32]string{\n\t\t0: \"INSTANCE_SETTING_KEY_UNSPECIFIED\",\n\t\t1: \"BASIC\",\n\t\t2: \"GENERAL\",\n\t\t3: \"STORAGE\",\n\t\t4: \"MEMO_RELATED\",\n\t\t5: \"TAGS\",\n\t\t6: \"NOTIFICATION\",\n\t}\n\tInstanceSettingKey_value = map[string]int32{\n\t\t\"INSTANCE_SETTING_KEY_UNSPECIFIED\": 0,\n\t\t\"BASIC\":                            1,\n\t\t\"GENERAL\":                          2,\n\t\t\"STORAGE\":                          3,\n\t\t\"MEMO_RELATED\":                     4,\n\t\t\"TAGS\":                             5,\n\t\t\"NOTIFICATION\":                     6,\n\t}\n)\n\nfunc (x InstanceSettingKey) Enum() *InstanceSettingKey {\n\tp := new(InstanceSettingKey)\n\t*p = x\n\treturn p\n}\n\nfunc (x InstanceSettingKey) String() string {\n\treturn protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))\n}\n\nfunc (InstanceSettingKey) Descriptor() protoreflect.EnumDescriptor {\n\treturn file_store_instance_setting_proto_enumTypes[0].Descriptor()\n}\n\nfunc (InstanceSettingKey) Type() protoreflect.EnumType {\n\treturn &file_store_instance_setting_proto_enumTypes[0]\n}\n\nfunc (x InstanceSettingKey) Number() protoreflect.EnumNumber {\n\treturn protoreflect.EnumNumber(x)\n}\n\n// Deprecated: Use InstanceSettingKey.Descriptor instead.\nfunc (InstanceSettingKey) EnumDescriptor() ([]byte, []int) {\n\treturn file_store_instance_setting_proto_rawDescGZIP(), []int{0}\n}\n\ntype InstanceStorageSetting_StorageType int32\n\nconst (\n\tInstanceStorageSetting_STORAGE_TYPE_UNSPECIFIED InstanceStorageSetting_StorageType = 0\n\t// STORAGE_TYPE_DATABASE is the database storage type.\n\tInstanceStorageSetting_DATABASE InstanceStorageSetting_StorageType = 1\n\t// STORAGE_TYPE_LOCAL is the local storage type.\n\tInstanceStorageSetting_LOCAL InstanceStorageSetting_StorageType = 2\n\t// STORAGE_TYPE_S3 is the S3 storage type.\n\tInstanceStorageSetting_S3 InstanceStorageSetting_StorageType = 3\n)\n\n// Enum value maps for InstanceStorageSetting_StorageType.\nvar (\n\tInstanceStorageSetting_StorageType_name = map[int32]string{\n\t\t0: \"STORAGE_TYPE_UNSPECIFIED\",\n\t\t1: \"DATABASE\",\n\t\t2: \"LOCAL\",\n\t\t3: \"S3\",\n\t}\n\tInstanceStorageSetting_StorageType_value = map[string]int32{\n\t\t\"STORAGE_TYPE_UNSPECIFIED\": 0,\n\t\t\"DATABASE\":                 1,\n\t\t\"LOCAL\":                    2,\n\t\t\"S3\":                       3,\n\t}\n)\n\nfunc (x InstanceStorageSetting_StorageType) Enum() *InstanceStorageSetting_StorageType {\n\tp := new(InstanceStorageSetting_StorageType)\n\t*p = x\n\treturn p\n}\n\nfunc (x InstanceStorageSetting_StorageType) String() string {\n\treturn protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))\n}\n\nfunc (InstanceStorageSetting_StorageType) Descriptor() protoreflect.EnumDescriptor {\n\treturn file_store_instance_setting_proto_enumTypes[1].Descriptor()\n}\n\nfunc (InstanceStorageSetting_StorageType) Type() protoreflect.EnumType {\n\treturn &file_store_instance_setting_proto_enumTypes[1]\n}\n\nfunc (x InstanceStorageSetting_StorageType) Number() protoreflect.EnumNumber {\n\treturn protoreflect.EnumNumber(x)\n}\n\n// Deprecated: Use InstanceStorageSetting_StorageType.Descriptor instead.\nfunc (InstanceStorageSetting_StorageType) EnumDescriptor() ([]byte, []int) {\n\treturn file_store_instance_setting_proto_rawDescGZIP(), []int{4, 0}\n}\n\ntype InstanceSetting struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\tKey   InstanceSettingKey     `protobuf:\"varint,1,opt,name=key,proto3,enum=memos.store.InstanceSettingKey\" json:\"key,omitempty\"`\n\t// Types that are valid to be assigned to Value:\n\t//\n\t//\t*InstanceSetting_BasicSetting\n\t//\t*InstanceSetting_GeneralSetting\n\t//\t*InstanceSetting_StorageSetting\n\t//\t*InstanceSetting_MemoRelatedSetting\n\t//\t*InstanceSetting_TagsSetting\n\t//\t*InstanceSetting_NotificationSetting\n\tValue         isInstanceSetting_Value `protobuf_oneof:\"value\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *InstanceSetting) Reset() {\n\t*x = InstanceSetting{}\n\tmi := &file_store_instance_setting_proto_msgTypes[0]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *InstanceSetting) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*InstanceSetting) ProtoMessage() {}\n\nfunc (x *InstanceSetting) ProtoReflect() protoreflect.Message {\n\tmi := &file_store_instance_setting_proto_msgTypes[0]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use InstanceSetting.ProtoReflect.Descriptor instead.\nfunc (*InstanceSetting) Descriptor() ([]byte, []int) {\n\treturn file_store_instance_setting_proto_rawDescGZIP(), []int{0}\n}\n\nfunc (x *InstanceSetting) GetKey() InstanceSettingKey {\n\tif x != nil {\n\t\treturn x.Key\n\t}\n\treturn InstanceSettingKey_INSTANCE_SETTING_KEY_UNSPECIFIED\n}\n\nfunc (x *InstanceSetting) GetValue() isInstanceSetting_Value {\n\tif x != nil {\n\t\treturn x.Value\n\t}\n\treturn nil\n}\n\nfunc (x *InstanceSetting) GetBasicSetting() *InstanceBasicSetting {\n\tif x != nil {\n\t\tif x, ok := x.Value.(*InstanceSetting_BasicSetting); ok {\n\t\t\treturn x.BasicSetting\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *InstanceSetting) GetGeneralSetting() *InstanceGeneralSetting {\n\tif x != nil {\n\t\tif x, ok := x.Value.(*InstanceSetting_GeneralSetting); ok {\n\t\t\treturn x.GeneralSetting\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *InstanceSetting) GetStorageSetting() *InstanceStorageSetting {\n\tif x != nil {\n\t\tif x, ok := x.Value.(*InstanceSetting_StorageSetting); ok {\n\t\t\treturn x.StorageSetting\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *InstanceSetting) GetMemoRelatedSetting() *InstanceMemoRelatedSetting {\n\tif x != nil {\n\t\tif x, ok := x.Value.(*InstanceSetting_MemoRelatedSetting); ok {\n\t\t\treturn x.MemoRelatedSetting\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *InstanceSetting) GetTagsSetting() *InstanceTagsSetting {\n\tif x != nil {\n\t\tif x, ok := x.Value.(*InstanceSetting_TagsSetting); ok {\n\t\t\treturn x.TagsSetting\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *InstanceSetting) GetNotificationSetting() *InstanceNotificationSetting {\n\tif x != nil {\n\t\tif x, ok := x.Value.(*InstanceSetting_NotificationSetting); ok {\n\t\t\treturn x.NotificationSetting\n\t\t}\n\t}\n\treturn nil\n}\n\ntype isInstanceSetting_Value interface {\n\tisInstanceSetting_Value()\n}\n\ntype InstanceSetting_BasicSetting struct {\n\tBasicSetting *InstanceBasicSetting `protobuf:\"bytes,2,opt,name=basic_setting,json=basicSetting,proto3,oneof\"`\n}\n\ntype InstanceSetting_GeneralSetting struct {\n\tGeneralSetting *InstanceGeneralSetting `protobuf:\"bytes,3,opt,name=general_setting,json=generalSetting,proto3,oneof\"`\n}\n\ntype InstanceSetting_StorageSetting struct {\n\tStorageSetting *InstanceStorageSetting `protobuf:\"bytes,4,opt,name=storage_setting,json=storageSetting,proto3,oneof\"`\n}\n\ntype InstanceSetting_MemoRelatedSetting struct {\n\tMemoRelatedSetting *InstanceMemoRelatedSetting `protobuf:\"bytes,5,opt,name=memo_related_setting,json=memoRelatedSetting,proto3,oneof\"`\n}\n\ntype InstanceSetting_TagsSetting struct {\n\tTagsSetting *InstanceTagsSetting `protobuf:\"bytes,6,opt,name=tags_setting,json=tagsSetting,proto3,oneof\"`\n}\n\ntype InstanceSetting_NotificationSetting struct {\n\tNotificationSetting *InstanceNotificationSetting `protobuf:\"bytes,7,opt,name=notification_setting,json=notificationSetting,proto3,oneof\"`\n}\n\nfunc (*InstanceSetting_BasicSetting) isInstanceSetting_Value() {}\n\nfunc (*InstanceSetting_GeneralSetting) isInstanceSetting_Value() {}\n\nfunc (*InstanceSetting_StorageSetting) isInstanceSetting_Value() {}\n\nfunc (*InstanceSetting_MemoRelatedSetting) isInstanceSetting_Value() {}\n\nfunc (*InstanceSetting_TagsSetting) isInstanceSetting_Value() {}\n\nfunc (*InstanceSetting_NotificationSetting) isInstanceSetting_Value() {}\n\ntype InstanceBasicSetting struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// The secret key for instance. Mainly used for session management.\n\tSecretKey string `protobuf:\"bytes,1,opt,name=secret_key,json=secretKey,proto3\" json:\"secret_key,omitempty\"`\n\t// The current schema version of database.\n\tSchemaVersion string `protobuf:\"bytes,2,opt,name=schema_version,json=schemaVersion,proto3\" json:\"schema_version,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *InstanceBasicSetting) Reset() {\n\t*x = InstanceBasicSetting{}\n\tmi := &file_store_instance_setting_proto_msgTypes[1]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *InstanceBasicSetting) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*InstanceBasicSetting) ProtoMessage() {}\n\nfunc (x *InstanceBasicSetting) ProtoReflect() protoreflect.Message {\n\tmi := &file_store_instance_setting_proto_msgTypes[1]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use InstanceBasicSetting.ProtoReflect.Descriptor instead.\nfunc (*InstanceBasicSetting) Descriptor() ([]byte, []int) {\n\treturn file_store_instance_setting_proto_rawDescGZIP(), []int{1}\n}\n\nfunc (x *InstanceBasicSetting) GetSecretKey() string {\n\tif x != nil {\n\t\treturn x.SecretKey\n\t}\n\treturn \"\"\n}\n\nfunc (x *InstanceBasicSetting) GetSchemaVersion() string {\n\tif x != nil {\n\t\treturn x.SchemaVersion\n\t}\n\treturn \"\"\n}\n\ntype InstanceGeneralSetting struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// disallow_user_registration disallows user registration.\n\tDisallowUserRegistration bool `protobuf:\"varint,2,opt,name=disallow_user_registration,json=disallowUserRegistration,proto3\" json:\"disallow_user_registration,omitempty\"`\n\t// disallow_password_auth disallows password authentication.\n\tDisallowPasswordAuth bool `protobuf:\"varint,3,opt,name=disallow_password_auth,json=disallowPasswordAuth,proto3\" json:\"disallow_password_auth,omitempty\"`\n\t// additional_script is the additional script.\n\tAdditionalScript string `protobuf:\"bytes,4,opt,name=additional_script,json=additionalScript,proto3\" json:\"additional_script,omitempty\"`\n\t// additional_style is the additional style.\n\tAdditionalStyle string `protobuf:\"bytes,5,opt,name=additional_style,json=additionalStyle,proto3\" json:\"additional_style,omitempty\"`\n\t// custom_profile is the custom profile.\n\tCustomProfile *InstanceCustomProfile `protobuf:\"bytes,6,opt,name=custom_profile,json=customProfile,proto3\" json:\"custom_profile,omitempty\"`\n\t// week_start_day_offset is the week start day offset from Sunday.\n\t// 0: Sunday, 1: Monday, 2: Tuesday, 3: Wednesday, 4: Thursday, 5: Friday, 6: Saturday\n\t// Default is Sunday.\n\tWeekStartDayOffset int32 `protobuf:\"varint,7,opt,name=week_start_day_offset,json=weekStartDayOffset,proto3\" json:\"week_start_day_offset,omitempty\"`\n\t// disallow_change_username disallows changing username.\n\tDisallowChangeUsername bool `protobuf:\"varint,8,opt,name=disallow_change_username,json=disallowChangeUsername,proto3\" json:\"disallow_change_username,omitempty\"`\n\t// disallow_change_nickname disallows changing nickname.\n\tDisallowChangeNickname bool `protobuf:\"varint,9,opt,name=disallow_change_nickname,json=disallowChangeNickname,proto3\" json:\"disallow_change_nickname,omitempty\"`\n\tunknownFields          protoimpl.UnknownFields\n\tsizeCache              protoimpl.SizeCache\n}\n\nfunc (x *InstanceGeneralSetting) Reset() {\n\t*x = InstanceGeneralSetting{}\n\tmi := &file_store_instance_setting_proto_msgTypes[2]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *InstanceGeneralSetting) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*InstanceGeneralSetting) ProtoMessage() {}\n\nfunc (x *InstanceGeneralSetting) ProtoReflect() protoreflect.Message {\n\tmi := &file_store_instance_setting_proto_msgTypes[2]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use InstanceGeneralSetting.ProtoReflect.Descriptor instead.\nfunc (*InstanceGeneralSetting) Descriptor() ([]byte, []int) {\n\treturn file_store_instance_setting_proto_rawDescGZIP(), []int{2}\n}\n\nfunc (x *InstanceGeneralSetting) GetDisallowUserRegistration() bool {\n\tif x != nil {\n\t\treturn x.DisallowUserRegistration\n\t}\n\treturn false\n}\n\nfunc (x *InstanceGeneralSetting) GetDisallowPasswordAuth() bool {\n\tif x != nil {\n\t\treturn x.DisallowPasswordAuth\n\t}\n\treturn false\n}\n\nfunc (x *InstanceGeneralSetting) GetAdditionalScript() string {\n\tif x != nil {\n\t\treturn x.AdditionalScript\n\t}\n\treturn \"\"\n}\n\nfunc (x *InstanceGeneralSetting) GetAdditionalStyle() string {\n\tif x != nil {\n\t\treturn x.AdditionalStyle\n\t}\n\treturn \"\"\n}\n\nfunc (x *InstanceGeneralSetting) GetCustomProfile() *InstanceCustomProfile {\n\tif x != nil {\n\t\treturn x.CustomProfile\n\t}\n\treturn nil\n}\n\nfunc (x *InstanceGeneralSetting) GetWeekStartDayOffset() int32 {\n\tif x != nil {\n\t\treturn x.WeekStartDayOffset\n\t}\n\treturn 0\n}\n\nfunc (x *InstanceGeneralSetting) GetDisallowChangeUsername() bool {\n\tif x != nil {\n\t\treturn x.DisallowChangeUsername\n\t}\n\treturn false\n}\n\nfunc (x *InstanceGeneralSetting) GetDisallowChangeNickname() bool {\n\tif x != nil {\n\t\treturn x.DisallowChangeNickname\n\t}\n\treturn false\n}\n\ntype InstanceCustomProfile struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tTitle         string                 `protobuf:\"bytes,1,opt,name=title,proto3\" json:\"title,omitempty\"`\n\tDescription   string                 `protobuf:\"bytes,2,opt,name=description,proto3\" json:\"description,omitempty\"`\n\tLogoUrl       string                 `protobuf:\"bytes,3,opt,name=logo_url,json=logoUrl,proto3\" json:\"logo_url,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *InstanceCustomProfile) Reset() {\n\t*x = InstanceCustomProfile{}\n\tmi := &file_store_instance_setting_proto_msgTypes[3]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *InstanceCustomProfile) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*InstanceCustomProfile) ProtoMessage() {}\n\nfunc (x *InstanceCustomProfile) ProtoReflect() protoreflect.Message {\n\tmi := &file_store_instance_setting_proto_msgTypes[3]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use InstanceCustomProfile.ProtoReflect.Descriptor instead.\nfunc (*InstanceCustomProfile) Descriptor() ([]byte, []int) {\n\treturn file_store_instance_setting_proto_rawDescGZIP(), []int{3}\n}\n\nfunc (x *InstanceCustomProfile) GetTitle() string {\n\tif x != nil {\n\t\treturn x.Title\n\t}\n\treturn \"\"\n}\n\nfunc (x *InstanceCustomProfile) GetDescription() string {\n\tif x != nil {\n\t\treturn x.Description\n\t}\n\treturn \"\"\n}\n\nfunc (x *InstanceCustomProfile) GetLogoUrl() string {\n\tif x != nil {\n\t\treturn x.LogoUrl\n\t}\n\treturn \"\"\n}\n\ntype InstanceStorageSetting struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// storage_type is the storage type.\n\tStorageType InstanceStorageSetting_StorageType `protobuf:\"varint,1,opt,name=storage_type,json=storageType,proto3,enum=memos.store.InstanceStorageSetting_StorageType\" json:\"storage_type,omitempty\"`\n\t// The template of file path.\n\t// e.g. assets/{timestamp}_{filename}\n\tFilepathTemplate string `protobuf:\"bytes,2,opt,name=filepath_template,json=filepathTemplate,proto3\" json:\"filepath_template,omitempty\"`\n\t// The max upload size in megabytes.\n\tUploadSizeLimitMb int64 `protobuf:\"varint,3,opt,name=upload_size_limit_mb,json=uploadSizeLimitMb,proto3\" json:\"upload_size_limit_mb,omitempty\"`\n\t// The S3 config.\n\tS3Config      *StorageS3Config `protobuf:\"bytes,4,opt,name=s3_config,json=s3Config,proto3\" json:\"s3_config,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *InstanceStorageSetting) Reset() {\n\t*x = InstanceStorageSetting{}\n\tmi := &file_store_instance_setting_proto_msgTypes[4]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *InstanceStorageSetting) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*InstanceStorageSetting) ProtoMessage() {}\n\nfunc (x *InstanceStorageSetting) ProtoReflect() protoreflect.Message {\n\tmi := &file_store_instance_setting_proto_msgTypes[4]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use InstanceStorageSetting.ProtoReflect.Descriptor instead.\nfunc (*InstanceStorageSetting) Descriptor() ([]byte, []int) {\n\treturn file_store_instance_setting_proto_rawDescGZIP(), []int{4}\n}\n\nfunc (x *InstanceStorageSetting) GetStorageType() InstanceStorageSetting_StorageType {\n\tif x != nil {\n\t\treturn x.StorageType\n\t}\n\treturn InstanceStorageSetting_STORAGE_TYPE_UNSPECIFIED\n}\n\nfunc (x *InstanceStorageSetting) GetFilepathTemplate() string {\n\tif x != nil {\n\t\treturn x.FilepathTemplate\n\t}\n\treturn \"\"\n}\n\nfunc (x *InstanceStorageSetting) GetUploadSizeLimitMb() int64 {\n\tif x != nil {\n\t\treturn x.UploadSizeLimitMb\n\t}\n\treturn 0\n}\n\nfunc (x *InstanceStorageSetting) GetS3Config() *StorageS3Config {\n\tif x != nil {\n\t\treturn x.S3Config\n\t}\n\treturn nil\n}\n\n// Reference: https://developers.cloudflare.com/r2/examples/aws/aws-sdk-go/\ntype StorageS3Config struct {\n\tstate           protoimpl.MessageState `protogen:\"open.v1\"`\n\tAccessKeyId     string                 `protobuf:\"bytes,1,opt,name=access_key_id,json=accessKeyId,proto3\" json:\"access_key_id,omitempty\"`\n\tAccessKeySecret string                 `protobuf:\"bytes,2,opt,name=access_key_secret,json=accessKeySecret,proto3\" json:\"access_key_secret,omitempty\"`\n\tEndpoint        string                 `protobuf:\"bytes,3,opt,name=endpoint,proto3\" json:\"endpoint,omitempty\"`\n\tRegion          string                 `protobuf:\"bytes,4,opt,name=region,proto3\" json:\"region,omitempty\"`\n\tBucket          string                 `protobuf:\"bytes,5,opt,name=bucket,proto3\" json:\"bucket,omitempty\"`\n\tUsePathStyle    bool                   `protobuf:\"varint,6,opt,name=use_path_style,json=usePathStyle,proto3\" json:\"use_path_style,omitempty\"`\n\tunknownFields   protoimpl.UnknownFields\n\tsizeCache       protoimpl.SizeCache\n}\n\nfunc (x *StorageS3Config) Reset() {\n\t*x = StorageS3Config{}\n\tmi := &file_store_instance_setting_proto_msgTypes[5]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *StorageS3Config) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*StorageS3Config) ProtoMessage() {}\n\nfunc (x *StorageS3Config) ProtoReflect() protoreflect.Message {\n\tmi := &file_store_instance_setting_proto_msgTypes[5]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use StorageS3Config.ProtoReflect.Descriptor instead.\nfunc (*StorageS3Config) Descriptor() ([]byte, []int) {\n\treturn file_store_instance_setting_proto_rawDescGZIP(), []int{5}\n}\n\nfunc (x *StorageS3Config) GetAccessKeyId() string {\n\tif x != nil {\n\t\treturn x.AccessKeyId\n\t}\n\treturn \"\"\n}\n\nfunc (x *StorageS3Config) GetAccessKeySecret() string {\n\tif x != nil {\n\t\treturn x.AccessKeySecret\n\t}\n\treturn \"\"\n}\n\nfunc (x *StorageS3Config) GetEndpoint() string {\n\tif x != nil {\n\t\treturn x.Endpoint\n\t}\n\treturn \"\"\n}\n\nfunc (x *StorageS3Config) GetRegion() string {\n\tif x != nil {\n\t\treturn x.Region\n\t}\n\treturn \"\"\n}\n\nfunc (x *StorageS3Config) GetBucket() string {\n\tif x != nil {\n\t\treturn x.Bucket\n\t}\n\treturn \"\"\n}\n\nfunc (x *StorageS3Config) GetUsePathStyle() bool {\n\tif x != nil {\n\t\treturn x.UsePathStyle\n\t}\n\treturn false\n}\n\ntype InstanceMemoRelatedSetting struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// display_with_update_time orders and displays memo with update time.\n\tDisplayWithUpdateTime bool `protobuf:\"varint,2,opt,name=display_with_update_time,json=displayWithUpdateTime,proto3\" json:\"display_with_update_time,omitempty\"`\n\t// content_length_limit is the limit of content length. Unit is byte.\n\tContentLengthLimit int32 `protobuf:\"varint,3,opt,name=content_length_limit,json=contentLengthLimit,proto3\" json:\"content_length_limit,omitempty\"`\n\t// enable_double_click_edit enables editing on double click.\n\tEnableDoubleClickEdit bool `protobuf:\"varint,4,opt,name=enable_double_click_edit,json=enableDoubleClickEdit,proto3\" json:\"enable_double_click_edit,omitempty\"`\n\t// reactions is the list of reactions.\n\tReactions     []string `protobuf:\"bytes,7,rep,name=reactions,proto3\" json:\"reactions,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *InstanceMemoRelatedSetting) Reset() {\n\t*x = InstanceMemoRelatedSetting{}\n\tmi := &file_store_instance_setting_proto_msgTypes[6]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *InstanceMemoRelatedSetting) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*InstanceMemoRelatedSetting) ProtoMessage() {}\n\nfunc (x *InstanceMemoRelatedSetting) ProtoReflect() protoreflect.Message {\n\tmi := &file_store_instance_setting_proto_msgTypes[6]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use InstanceMemoRelatedSetting.ProtoReflect.Descriptor instead.\nfunc (*InstanceMemoRelatedSetting) Descriptor() ([]byte, []int) {\n\treturn file_store_instance_setting_proto_rawDescGZIP(), []int{6}\n}\n\nfunc (x *InstanceMemoRelatedSetting) GetDisplayWithUpdateTime() bool {\n\tif x != nil {\n\t\treturn x.DisplayWithUpdateTime\n\t}\n\treturn false\n}\n\nfunc (x *InstanceMemoRelatedSetting) GetContentLengthLimit() int32 {\n\tif x != nil {\n\t\treturn x.ContentLengthLimit\n\t}\n\treturn 0\n}\n\nfunc (x *InstanceMemoRelatedSetting) GetEnableDoubleClickEdit() bool {\n\tif x != nil {\n\t\treturn x.EnableDoubleClickEdit\n\t}\n\treturn false\n}\n\nfunc (x *InstanceMemoRelatedSetting) GetReactions() []string {\n\tif x != nil {\n\t\treturn x.Reactions\n\t}\n\treturn nil\n}\n\ntype InstanceTagMetadata struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// Background color for the tag label.\n\tBackgroundColor *color.Color `protobuf:\"bytes,1,opt,name=background_color,json=backgroundColor,proto3\" json:\"background_color,omitempty\"`\n\tunknownFields   protoimpl.UnknownFields\n\tsizeCache       protoimpl.SizeCache\n}\n\nfunc (x *InstanceTagMetadata) Reset() {\n\t*x = InstanceTagMetadata{}\n\tmi := &file_store_instance_setting_proto_msgTypes[7]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *InstanceTagMetadata) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*InstanceTagMetadata) ProtoMessage() {}\n\nfunc (x *InstanceTagMetadata) ProtoReflect() protoreflect.Message {\n\tmi := &file_store_instance_setting_proto_msgTypes[7]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use InstanceTagMetadata.ProtoReflect.Descriptor instead.\nfunc (*InstanceTagMetadata) Descriptor() ([]byte, []int) {\n\treturn file_store_instance_setting_proto_rawDescGZIP(), []int{7}\n}\n\nfunc (x *InstanceTagMetadata) GetBackgroundColor() *color.Color {\n\tif x != nil {\n\t\treturn x.BackgroundColor\n\t}\n\treturn nil\n}\n\ntype InstanceTagsSetting struct {\n\tstate         protoimpl.MessageState          `protogen:\"open.v1\"`\n\tTags          map[string]*InstanceTagMetadata `protobuf:\"bytes,1,rep,name=tags,proto3\" json:\"tags,omitempty\" protobuf_key:\"bytes,1,opt,name=key\" protobuf_val:\"bytes,2,opt,name=value\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *InstanceTagsSetting) Reset() {\n\t*x = InstanceTagsSetting{}\n\tmi := &file_store_instance_setting_proto_msgTypes[8]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *InstanceTagsSetting) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*InstanceTagsSetting) ProtoMessage() {}\n\nfunc (x *InstanceTagsSetting) ProtoReflect() protoreflect.Message {\n\tmi := &file_store_instance_setting_proto_msgTypes[8]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use InstanceTagsSetting.ProtoReflect.Descriptor instead.\nfunc (*InstanceTagsSetting) Descriptor() ([]byte, []int) {\n\treturn file_store_instance_setting_proto_rawDescGZIP(), []int{8}\n}\n\nfunc (x *InstanceTagsSetting) GetTags() map[string]*InstanceTagMetadata {\n\tif x != nil {\n\t\treturn x.Tags\n\t}\n\treturn nil\n}\n\ntype InstanceNotificationSetting struct {\n\tstate         protoimpl.MessageState                    `protogen:\"open.v1\"`\n\tEmail         *InstanceNotificationSetting_EmailSetting `protobuf:\"bytes,1,opt,name=email,proto3\" json:\"email,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *InstanceNotificationSetting) Reset() {\n\t*x = InstanceNotificationSetting{}\n\tmi := &file_store_instance_setting_proto_msgTypes[9]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *InstanceNotificationSetting) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*InstanceNotificationSetting) ProtoMessage() {}\n\nfunc (x *InstanceNotificationSetting) ProtoReflect() protoreflect.Message {\n\tmi := &file_store_instance_setting_proto_msgTypes[9]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use InstanceNotificationSetting.ProtoReflect.Descriptor instead.\nfunc (*InstanceNotificationSetting) Descriptor() ([]byte, []int) {\n\treturn file_store_instance_setting_proto_rawDescGZIP(), []int{9}\n}\n\nfunc (x *InstanceNotificationSetting) GetEmail() *InstanceNotificationSetting_EmailSetting {\n\tif x != nil {\n\t\treturn x.Email\n\t}\n\treturn nil\n}\n\ntype InstanceNotificationSetting_EmailSetting struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tEnabled       bool                   `protobuf:\"varint,1,opt,name=enabled,proto3\" json:\"enabled,omitempty\"`\n\tSmtpHost      string                 `protobuf:\"bytes,2,opt,name=smtp_host,json=smtpHost,proto3\" json:\"smtp_host,omitempty\"`\n\tSmtpPort      int32                  `protobuf:\"varint,3,opt,name=smtp_port,json=smtpPort,proto3\" json:\"smtp_port,omitempty\"`\n\tSmtpUsername  string                 `protobuf:\"bytes,4,opt,name=smtp_username,json=smtpUsername,proto3\" json:\"smtp_username,omitempty\"`\n\tSmtpPassword  string                 `protobuf:\"bytes,5,opt,name=smtp_password,json=smtpPassword,proto3\" json:\"smtp_password,omitempty\"`\n\tFromEmail     string                 `protobuf:\"bytes,6,opt,name=from_email,json=fromEmail,proto3\" json:\"from_email,omitempty\"`\n\tFromName      string                 `protobuf:\"bytes,7,opt,name=from_name,json=fromName,proto3\" json:\"from_name,omitempty\"`\n\tReplyTo       string                 `protobuf:\"bytes,8,opt,name=reply_to,json=replyTo,proto3\" json:\"reply_to,omitempty\"`\n\tUseTls        bool                   `protobuf:\"varint,9,opt,name=use_tls,json=useTls,proto3\" json:\"use_tls,omitempty\"`\n\tUseSsl        bool                   `protobuf:\"varint,10,opt,name=use_ssl,json=useSsl,proto3\" json:\"use_ssl,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *InstanceNotificationSetting_EmailSetting) Reset() {\n\t*x = InstanceNotificationSetting_EmailSetting{}\n\tmi := &file_store_instance_setting_proto_msgTypes[11]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *InstanceNotificationSetting_EmailSetting) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*InstanceNotificationSetting_EmailSetting) ProtoMessage() {}\n\nfunc (x *InstanceNotificationSetting_EmailSetting) ProtoReflect() protoreflect.Message {\n\tmi := &file_store_instance_setting_proto_msgTypes[11]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use InstanceNotificationSetting_EmailSetting.ProtoReflect.Descriptor instead.\nfunc (*InstanceNotificationSetting_EmailSetting) Descriptor() ([]byte, []int) {\n\treturn file_store_instance_setting_proto_rawDescGZIP(), []int{9, 0}\n}\n\nfunc (x *InstanceNotificationSetting_EmailSetting) GetEnabled() bool {\n\tif x != nil {\n\t\treturn x.Enabled\n\t}\n\treturn false\n}\n\nfunc (x *InstanceNotificationSetting_EmailSetting) GetSmtpHost() string {\n\tif x != nil {\n\t\treturn x.SmtpHost\n\t}\n\treturn \"\"\n}\n\nfunc (x *InstanceNotificationSetting_EmailSetting) GetSmtpPort() int32 {\n\tif x != nil {\n\t\treturn x.SmtpPort\n\t}\n\treturn 0\n}\n\nfunc (x *InstanceNotificationSetting_EmailSetting) GetSmtpUsername() string {\n\tif x != nil {\n\t\treturn x.SmtpUsername\n\t}\n\treturn \"\"\n}\n\nfunc (x *InstanceNotificationSetting_EmailSetting) GetSmtpPassword() string {\n\tif x != nil {\n\t\treturn x.SmtpPassword\n\t}\n\treturn \"\"\n}\n\nfunc (x *InstanceNotificationSetting_EmailSetting) GetFromEmail() string {\n\tif x != nil {\n\t\treturn x.FromEmail\n\t}\n\treturn \"\"\n}\n\nfunc (x *InstanceNotificationSetting_EmailSetting) GetFromName() string {\n\tif x != nil {\n\t\treturn x.FromName\n\t}\n\treturn \"\"\n}\n\nfunc (x *InstanceNotificationSetting_EmailSetting) GetReplyTo() string {\n\tif x != nil {\n\t\treturn x.ReplyTo\n\t}\n\treturn \"\"\n}\n\nfunc (x *InstanceNotificationSetting_EmailSetting) GetUseTls() bool {\n\tif x != nil {\n\t\treturn x.UseTls\n\t}\n\treturn false\n}\n\nfunc (x *InstanceNotificationSetting_EmailSetting) GetUseSsl() bool {\n\tif x != nil {\n\t\treturn x.UseSsl\n\t}\n\treturn false\n}\n\nvar File_store_instance_setting_proto protoreflect.FileDescriptor\n\nconst file_store_instance_setting_proto_rawDesc = \"\" +\n\t\"\\n\" +\n\t\"\\x1cstore/instance_setting.proto\\x12\\vmemos.store\\x1a\\x17google/type/color.proto\\\"\\xba\\x04\\n\" +\n\t\"\\x0fInstanceSetting\\x121\\n\" +\n\t\"\\x03key\\x18\\x01 \\x01(\\x0e2\\x1f.memos.store.InstanceSettingKeyR\\x03key\\x12H\\n\" +\n\t\"\\rbasic_setting\\x18\\x02 \\x01(\\v2!.memos.store.InstanceBasicSettingH\\x00R\\fbasicSetting\\x12N\\n\" +\n\t\"\\x0fgeneral_setting\\x18\\x03 \\x01(\\v2#.memos.store.InstanceGeneralSettingH\\x00R\\x0egeneralSetting\\x12N\\n\" +\n\t\"\\x0fstorage_setting\\x18\\x04 \\x01(\\v2#.memos.store.InstanceStorageSettingH\\x00R\\x0estorageSetting\\x12[\\n\" +\n\t\"\\x14memo_related_setting\\x18\\x05 \\x01(\\v2'.memos.store.InstanceMemoRelatedSettingH\\x00R\\x12memoRelatedSetting\\x12E\\n\" +\n\t\"\\ftags_setting\\x18\\x06 \\x01(\\v2 .memos.store.InstanceTagsSettingH\\x00R\\vtagsSetting\\x12]\\n\" +\n\t\"\\x14notification_setting\\x18\\a \\x01(\\v2(.memos.store.InstanceNotificationSettingH\\x00R\\x13notificationSettingB\\a\\n\" +\n\t\"\\x05value\\\"\\\\\\n\" +\n\t\"\\x14InstanceBasicSetting\\x12\\x1d\\n\" +\n\t\"\\n\" +\n\t\"secret_key\\x18\\x01 \\x01(\\tR\\tsecretKey\\x12%\\n\" +\n\t\"\\x0eschema_version\\x18\\x02 \\x01(\\tR\\rschemaVersion\\\"\\xd6\\x03\\n\" +\n\t\"\\x16InstanceGeneralSetting\\x12<\\n\" +\n\t\"\\x1adisallow_user_registration\\x18\\x02 \\x01(\\bR\\x18disallowUserRegistration\\x124\\n\" +\n\t\"\\x16disallow_password_auth\\x18\\x03 \\x01(\\bR\\x14disallowPasswordAuth\\x12+\\n\" +\n\t\"\\x11additional_script\\x18\\x04 \\x01(\\tR\\x10additionalScript\\x12)\\n\" +\n\t\"\\x10additional_style\\x18\\x05 \\x01(\\tR\\x0fadditionalStyle\\x12I\\n\" +\n\t\"\\x0ecustom_profile\\x18\\x06 \\x01(\\v2\\\".memos.store.InstanceCustomProfileR\\rcustomProfile\\x121\\n\" +\n\t\"\\x15week_start_day_offset\\x18\\a \\x01(\\x05R\\x12weekStartDayOffset\\x128\\n\" +\n\t\"\\x18disallow_change_username\\x18\\b \\x01(\\bR\\x16disallowChangeUsername\\x128\\n\" +\n\t\"\\x18disallow_change_nickname\\x18\\t \\x01(\\bR\\x16disallowChangeNickname\\\"j\\n\" +\n\t\"\\x15InstanceCustomProfile\\x12\\x14\\n\" +\n\t\"\\x05title\\x18\\x01 \\x01(\\tR\\x05title\\x12 \\n\" +\n\t\"\\vdescription\\x18\\x02 \\x01(\\tR\\vdescription\\x12\\x19\\n\" +\n\t\"\\blogo_url\\x18\\x03 \\x01(\\tR\\alogoUrl\\\"\\xd3\\x02\\n\" +\n\t\"\\x16InstanceStorageSetting\\x12R\\n\" +\n\t\"\\fstorage_type\\x18\\x01 \\x01(\\x0e2/.memos.store.InstanceStorageSetting.StorageTypeR\\vstorageType\\x12+\\n\" +\n\t\"\\x11filepath_template\\x18\\x02 \\x01(\\tR\\x10filepathTemplate\\x12/\\n\" +\n\t\"\\x14upload_size_limit_mb\\x18\\x03 \\x01(\\x03R\\x11uploadSizeLimitMb\\x129\\n\" +\n\t\"\\ts3_config\\x18\\x04 \\x01(\\v2\\x1c.memos.store.StorageS3ConfigR\\bs3Config\\\"L\\n\" +\n\t\"\\vStorageType\\x12\\x1c\\n\" +\n\t\"\\x18STORAGE_TYPE_UNSPECIFIED\\x10\\x00\\x12\\f\\n\" +\n\t\"\\bDATABASE\\x10\\x01\\x12\\t\\n\" +\n\t\"\\x05LOCAL\\x10\\x02\\x12\\x06\\n\" +\n\t\"\\x02S3\\x10\\x03\\\"\\xd3\\x01\\n\" +\n\t\"\\x0fStorageS3Config\\x12\\\"\\n\" +\n\t\"\\raccess_key_id\\x18\\x01 \\x01(\\tR\\vaccessKeyId\\x12*\\n\" +\n\t\"\\x11access_key_secret\\x18\\x02 \\x01(\\tR\\x0faccessKeySecret\\x12\\x1a\\n\" +\n\t\"\\bendpoint\\x18\\x03 \\x01(\\tR\\bendpoint\\x12\\x16\\n\" +\n\t\"\\x06region\\x18\\x04 \\x01(\\tR\\x06region\\x12\\x16\\n\" +\n\t\"\\x06bucket\\x18\\x05 \\x01(\\tR\\x06bucket\\x12$\\n\" +\n\t\"\\x0euse_path_style\\x18\\x06 \\x01(\\bR\\fusePathStyle\\\"\\xde\\x01\\n\" +\n\t\"\\x1aInstanceMemoRelatedSetting\\x127\\n\" +\n\t\"\\x18display_with_update_time\\x18\\x02 \\x01(\\bR\\x15displayWithUpdateTime\\x120\\n\" +\n\t\"\\x14content_length_limit\\x18\\x03 \\x01(\\x05R\\x12contentLengthLimit\\x127\\n\" +\n\t\"\\x18enable_double_click_edit\\x18\\x04 \\x01(\\bR\\x15enableDoubleClickEdit\\x12\\x1c\\n\" +\n\t\"\\treactions\\x18\\a \\x03(\\tR\\treactions\\\"T\\n\" +\n\t\"\\x13InstanceTagMetadata\\x12=\\n\" +\n\t\"\\x10background_color\\x18\\x01 \\x01(\\v2\\x12.google.type.ColorR\\x0fbackgroundColor\\\"\\xb0\\x01\\n\" +\n\t\"\\x13InstanceTagsSetting\\x12>\\n\" +\n\t\"\\x04tags\\x18\\x01 \\x03(\\v2*.memos.store.InstanceTagsSetting.TagsEntryR\\x04tags\\x1aY\\n\" +\n\t\"\\tTagsEntry\\x12\\x10\\n\" +\n\t\"\\x03key\\x18\\x01 \\x01(\\tR\\x03key\\x126\\n\" +\n\t\"\\x05value\\x18\\x02 \\x01(\\v2 .memos.store.InstanceTagMetadataR\\x05value:\\x028\\x01\\\"\\xa2\\x03\\n\" +\n\t\"\\x1bInstanceNotificationSetting\\x12K\\n\" +\n\t\"\\x05email\\x18\\x01 \\x01(\\v25.memos.store.InstanceNotificationSetting.EmailSettingR\\x05email\\x1a\\xb5\\x02\\n\" +\n\t\"\\fEmailSetting\\x12\\x18\\n\" +\n\t\"\\aenabled\\x18\\x01 \\x01(\\bR\\aenabled\\x12\\x1b\\n\" +\n\t\"\\tsmtp_host\\x18\\x02 \\x01(\\tR\\bsmtpHost\\x12\\x1b\\n\" +\n\t\"\\tsmtp_port\\x18\\x03 \\x01(\\x05R\\bsmtpPort\\x12#\\n\" +\n\t\"\\rsmtp_username\\x18\\x04 \\x01(\\tR\\fsmtpUsername\\x12#\\n\" +\n\t\"\\rsmtp_password\\x18\\x05 \\x01(\\tR\\fsmtpPassword\\x12\\x1d\\n\" +\n\t\"\\n\" +\n\t\"from_email\\x18\\x06 \\x01(\\tR\\tfromEmail\\x12\\x1b\\n\" +\n\t\"\\tfrom_name\\x18\\a \\x01(\\tR\\bfromName\\x12\\x19\\n\" +\n\t\"\\breply_to\\x18\\b \\x01(\\tR\\areplyTo\\x12\\x17\\n\" +\n\t\"\\ause_tls\\x18\\t \\x01(\\bR\\x06useTls\\x12\\x17\\n\" +\n\t\"\\ause_ssl\\x18\\n\" +\n\t\" \\x01(\\bR\\x06useSsl*\\x8d\\x01\\n\" +\n\t\"\\x12InstanceSettingKey\\x12$\\n\" +\n\t\" INSTANCE_SETTING_KEY_UNSPECIFIED\\x10\\x00\\x12\\t\\n\" +\n\t\"\\x05BASIC\\x10\\x01\\x12\\v\\n\" +\n\t\"\\aGENERAL\\x10\\x02\\x12\\v\\n\" +\n\t\"\\aSTORAGE\\x10\\x03\\x12\\x10\\n\" +\n\t\"\\fMEMO_RELATED\\x10\\x04\\x12\\b\\n\" +\n\t\"\\x04TAGS\\x10\\x05\\x12\\x10\\n\" +\n\t\"\\fNOTIFICATION\\x10\\x06B\\x9f\\x01\\n\" +\n\t\"\\x0fcom.memos.storeB\\x14InstanceSettingProtoP\\x01Z)github.com/usememos/memos/proto/gen/store\\xa2\\x02\\x03MSX\\xaa\\x02\\vMemos.Store\\xca\\x02\\vMemos\\\\Store\\xe2\\x02\\x17Memos\\\\Store\\\\GPBMetadata\\xea\\x02\\fMemos::Storeb\\x06proto3\"\n\nvar (\n\tfile_store_instance_setting_proto_rawDescOnce sync.Once\n\tfile_store_instance_setting_proto_rawDescData []byte\n)\n\nfunc file_store_instance_setting_proto_rawDescGZIP() []byte {\n\tfile_store_instance_setting_proto_rawDescOnce.Do(func() {\n\t\tfile_store_instance_setting_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_store_instance_setting_proto_rawDesc), len(file_store_instance_setting_proto_rawDesc)))\n\t})\n\treturn file_store_instance_setting_proto_rawDescData\n}\n\nvar file_store_instance_setting_proto_enumTypes = make([]protoimpl.EnumInfo, 2)\nvar file_store_instance_setting_proto_msgTypes = make([]protoimpl.MessageInfo, 12)\nvar file_store_instance_setting_proto_goTypes = []any{\n\t(InstanceSettingKey)(0),                          // 0: memos.store.InstanceSettingKey\n\t(InstanceStorageSetting_StorageType)(0),          // 1: memos.store.InstanceStorageSetting.StorageType\n\t(*InstanceSetting)(nil),                          // 2: memos.store.InstanceSetting\n\t(*InstanceBasicSetting)(nil),                     // 3: memos.store.InstanceBasicSetting\n\t(*InstanceGeneralSetting)(nil),                   // 4: memos.store.InstanceGeneralSetting\n\t(*InstanceCustomProfile)(nil),                    // 5: memos.store.InstanceCustomProfile\n\t(*InstanceStorageSetting)(nil),                   // 6: memos.store.InstanceStorageSetting\n\t(*StorageS3Config)(nil),                          // 7: memos.store.StorageS3Config\n\t(*InstanceMemoRelatedSetting)(nil),               // 8: memos.store.InstanceMemoRelatedSetting\n\t(*InstanceTagMetadata)(nil),                      // 9: memos.store.InstanceTagMetadata\n\t(*InstanceTagsSetting)(nil),                      // 10: memos.store.InstanceTagsSetting\n\t(*InstanceNotificationSetting)(nil),              // 11: memos.store.InstanceNotificationSetting\n\tnil,                                              // 12: memos.store.InstanceTagsSetting.TagsEntry\n\t(*InstanceNotificationSetting_EmailSetting)(nil), // 13: memos.store.InstanceNotificationSetting.EmailSetting\n\t(*color.Color)(nil),                              // 14: google.type.Color\n}\nvar file_store_instance_setting_proto_depIdxs = []int32{\n\t0,  // 0: memos.store.InstanceSetting.key:type_name -> memos.store.InstanceSettingKey\n\t3,  // 1: memos.store.InstanceSetting.basic_setting:type_name -> memos.store.InstanceBasicSetting\n\t4,  // 2: memos.store.InstanceSetting.general_setting:type_name -> memos.store.InstanceGeneralSetting\n\t6,  // 3: memos.store.InstanceSetting.storage_setting:type_name -> memos.store.InstanceStorageSetting\n\t8,  // 4: memos.store.InstanceSetting.memo_related_setting:type_name -> memos.store.InstanceMemoRelatedSetting\n\t10, // 5: memos.store.InstanceSetting.tags_setting:type_name -> memos.store.InstanceTagsSetting\n\t11, // 6: memos.store.InstanceSetting.notification_setting:type_name -> memos.store.InstanceNotificationSetting\n\t5,  // 7: memos.store.InstanceGeneralSetting.custom_profile:type_name -> memos.store.InstanceCustomProfile\n\t1,  // 8: memos.store.InstanceStorageSetting.storage_type:type_name -> memos.store.InstanceStorageSetting.StorageType\n\t7,  // 9: memos.store.InstanceStorageSetting.s3_config:type_name -> memos.store.StorageS3Config\n\t14, // 10: memos.store.InstanceTagMetadata.background_color:type_name -> google.type.Color\n\t12, // 11: memos.store.InstanceTagsSetting.tags:type_name -> memos.store.InstanceTagsSetting.TagsEntry\n\t13, // 12: memos.store.InstanceNotificationSetting.email:type_name -> memos.store.InstanceNotificationSetting.EmailSetting\n\t9,  // 13: memos.store.InstanceTagsSetting.TagsEntry.value:type_name -> memos.store.InstanceTagMetadata\n\t14, // [14:14] is the sub-list for method output_type\n\t14, // [14:14] is the sub-list for method input_type\n\t14, // [14:14] is the sub-list for extension type_name\n\t14, // [14:14] is the sub-list for extension extendee\n\t0,  // [0:14] is the sub-list for field type_name\n}\n\nfunc init() { file_store_instance_setting_proto_init() }\nfunc file_store_instance_setting_proto_init() {\n\tif File_store_instance_setting_proto != nil {\n\t\treturn\n\t}\n\tfile_store_instance_setting_proto_msgTypes[0].OneofWrappers = []any{\n\t\t(*InstanceSetting_BasicSetting)(nil),\n\t\t(*InstanceSetting_GeneralSetting)(nil),\n\t\t(*InstanceSetting_StorageSetting)(nil),\n\t\t(*InstanceSetting_MemoRelatedSetting)(nil),\n\t\t(*InstanceSetting_TagsSetting)(nil),\n\t\t(*InstanceSetting_NotificationSetting)(nil),\n\t}\n\ttype x struct{}\n\tout := protoimpl.TypeBuilder{\n\t\tFile: protoimpl.DescBuilder{\n\t\t\tGoPackagePath: reflect.TypeOf(x{}).PkgPath(),\n\t\t\tRawDescriptor: unsafe.Slice(unsafe.StringData(file_store_instance_setting_proto_rawDesc), len(file_store_instance_setting_proto_rawDesc)),\n\t\t\tNumEnums:      2,\n\t\t\tNumMessages:   12,\n\t\t\tNumExtensions: 0,\n\t\t\tNumServices:   0,\n\t\t},\n\t\tGoTypes:           file_store_instance_setting_proto_goTypes,\n\t\tDependencyIndexes: file_store_instance_setting_proto_depIdxs,\n\t\tEnumInfos:         file_store_instance_setting_proto_enumTypes,\n\t\tMessageInfos:      file_store_instance_setting_proto_msgTypes,\n\t}.Build()\n\tFile_store_instance_setting_proto = out.File\n\tfile_store_instance_setting_proto_goTypes = nil\n\tfile_store_instance_setting_proto_depIdxs = nil\n}\n"
  },
  {
    "path": "proto/gen/store/memo.pb.go",
    "content": "// Code generated by protoc-gen-go. DO NOT EDIT.\n// versions:\n// \tprotoc-gen-go v1.36.11\n// \tprotoc        (unknown)\n// source: store/memo.proto\n\npackage store\n\nimport (\n\tprotoreflect \"google.golang.org/protobuf/reflect/protoreflect\"\n\tprotoimpl \"google.golang.org/protobuf/runtime/protoimpl\"\n\treflect \"reflect\"\n\tsync \"sync\"\n\tunsafe \"unsafe\"\n)\n\nconst (\n\t// Verify that this generated code is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)\n\t// Verify that runtime/protoimpl is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)\n)\n\ntype MemoPayload struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tProperty      *MemoPayload_Property  `protobuf:\"bytes,1,opt,name=property,proto3\" json:\"property,omitempty\"`\n\tLocation      *MemoPayload_Location  `protobuf:\"bytes,2,opt,name=location,proto3\" json:\"location,omitempty\"`\n\tTags          []string               `protobuf:\"bytes,3,rep,name=tags,proto3\" json:\"tags,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *MemoPayload) Reset() {\n\t*x = MemoPayload{}\n\tmi := &file_store_memo_proto_msgTypes[0]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *MemoPayload) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*MemoPayload) ProtoMessage() {}\n\nfunc (x *MemoPayload) ProtoReflect() protoreflect.Message {\n\tmi := &file_store_memo_proto_msgTypes[0]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use MemoPayload.ProtoReflect.Descriptor instead.\nfunc (*MemoPayload) Descriptor() ([]byte, []int) {\n\treturn file_store_memo_proto_rawDescGZIP(), []int{0}\n}\n\nfunc (x *MemoPayload) GetProperty() *MemoPayload_Property {\n\tif x != nil {\n\t\treturn x.Property\n\t}\n\treturn nil\n}\n\nfunc (x *MemoPayload) GetLocation() *MemoPayload_Location {\n\tif x != nil {\n\t\treturn x.Location\n\t}\n\treturn nil\n}\n\nfunc (x *MemoPayload) GetTags() []string {\n\tif x != nil {\n\t\treturn x.Tags\n\t}\n\treturn nil\n}\n\n// The calculated properties from the memo content.\ntype MemoPayload_Property struct {\n\tstate              protoimpl.MessageState `protogen:\"open.v1\"`\n\tHasLink            bool                   `protobuf:\"varint,1,opt,name=has_link,json=hasLink,proto3\" json:\"has_link,omitempty\"`\n\tHasTaskList        bool                   `protobuf:\"varint,2,opt,name=has_task_list,json=hasTaskList,proto3\" json:\"has_task_list,omitempty\"`\n\tHasCode            bool                   `protobuf:\"varint,3,opt,name=has_code,json=hasCode,proto3\" json:\"has_code,omitempty\"`\n\tHasIncompleteTasks bool                   `protobuf:\"varint,4,opt,name=has_incomplete_tasks,json=hasIncompleteTasks,proto3\" json:\"has_incomplete_tasks,omitempty\"`\n\t// The title extracted from the first H1 heading, if present.\n\tTitle         string `protobuf:\"bytes,5,opt,name=title,proto3\" json:\"title,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *MemoPayload_Property) Reset() {\n\t*x = MemoPayload_Property{}\n\tmi := &file_store_memo_proto_msgTypes[1]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *MemoPayload_Property) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*MemoPayload_Property) ProtoMessage() {}\n\nfunc (x *MemoPayload_Property) ProtoReflect() protoreflect.Message {\n\tmi := &file_store_memo_proto_msgTypes[1]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use MemoPayload_Property.ProtoReflect.Descriptor instead.\nfunc (*MemoPayload_Property) Descriptor() ([]byte, []int) {\n\treturn file_store_memo_proto_rawDescGZIP(), []int{0, 0}\n}\n\nfunc (x *MemoPayload_Property) GetHasLink() bool {\n\tif x != nil {\n\t\treturn x.HasLink\n\t}\n\treturn false\n}\n\nfunc (x *MemoPayload_Property) GetHasTaskList() bool {\n\tif x != nil {\n\t\treturn x.HasTaskList\n\t}\n\treturn false\n}\n\nfunc (x *MemoPayload_Property) GetHasCode() bool {\n\tif x != nil {\n\t\treturn x.HasCode\n\t}\n\treturn false\n}\n\nfunc (x *MemoPayload_Property) GetHasIncompleteTasks() bool {\n\tif x != nil {\n\t\treturn x.HasIncompleteTasks\n\t}\n\treturn false\n}\n\nfunc (x *MemoPayload_Property) GetTitle() string {\n\tif x != nil {\n\t\treturn x.Title\n\t}\n\treturn \"\"\n}\n\ntype MemoPayload_Location struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tPlaceholder   string                 `protobuf:\"bytes,1,opt,name=placeholder,proto3\" json:\"placeholder,omitempty\"`\n\tLatitude      float64                `protobuf:\"fixed64,2,opt,name=latitude,proto3\" json:\"latitude,omitempty\"`\n\tLongitude     float64                `protobuf:\"fixed64,3,opt,name=longitude,proto3\" json:\"longitude,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *MemoPayload_Location) Reset() {\n\t*x = MemoPayload_Location{}\n\tmi := &file_store_memo_proto_msgTypes[2]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *MemoPayload_Location) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*MemoPayload_Location) ProtoMessage() {}\n\nfunc (x *MemoPayload_Location) ProtoReflect() protoreflect.Message {\n\tmi := &file_store_memo_proto_msgTypes[2]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use MemoPayload_Location.ProtoReflect.Descriptor instead.\nfunc (*MemoPayload_Location) Descriptor() ([]byte, []int) {\n\treturn file_store_memo_proto_rawDescGZIP(), []int{0, 1}\n}\n\nfunc (x *MemoPayload_Location) GetPlaceholder() string {\n\tif x != nil {\n\t\treturn x.Placeholder\n\t}\n\treturn \"\"\n}\n\nfunc (x *MemoPayload_Location) GetLatitude() float64 {\n\tif x != nil {\n\t\treturn x.Latitude\n\t}\n\treturn 0\n}\n\nfunc (x *MemoPayload_Location) GetLongitude() float64 {\n\tif x != nil {\n\t\treturn x.Longitude\n\t}\n\treturn 0\n}\n\nvar File_store_memo_proto protoreflect.FileDescriptor\n\nconst file_store_memo_proto_rawDesc = \"\" +\n\t\"\\n\" +\n\t\"\\x10store/memo.proto\\x12\\vmemos.store\\\"\\xb6\\x03\\n\" +\n\t\"\\vMemoPayload\\x12=\\n\" +\n\t\"\\bproperty\\x18\\x01 \\x01(\\v2!.memos.store.MemoPayload.PropertyR\\bproperty\\x12=\\n\" +\n\t\"\\blocation\\x18\\x02 \\x01(\\v2!.memos.store.MemoPayload.LocationR\\blocation\\x12\\x12\\n\" +\n\t\"\\x04tags\\x18\\x03 \\x03(\\tR\\x04tags\\x1a\\xac\\x01\\n\" +\n\t\"\\bProperty\\x12\\x19\\n\" +\n\t\"\\bhas_link\\x18\\x01 \\x01(\\bR\\ahasLink\\x12\\\"\\n\" +\n\t\"\\rhas_task_list\\x18\\x02 \\x01(\\bR\\vhasTaskList\\x12\\x19\\n\" +\n\t\"\\bhas_code\\x18\\x03 \\x01(\\bR\\ahasCode\\x120\\n\" +\n\t\"\\x14has_incomplete_tasks\\x18\\x04 \\x01(\\bR\\x12hasIncompleteTasks\\x12\\x14\\n\" +\n\t\"\\x05title\\x18\\x05 \\x01(\\tR\\x05title\\x1af\\n\" +\n\t\"\\bLocation\\x12 \\n\" +\n\t\"\\vplaceholder\\x18\\x01 \\x01(\\tR\\vplaceholder\\x12\\x1a\\n\" +\n\t\"\\blatitude\\x18\\x02 \\x01(\\x01R\\blatitude\\x12\\x1c\\n\" +\n\t\"\\tlongitude\\x18\\x03 \\x01(\\x01R\\tlongitudeB\\x94\\x01\\n\" +\n\t\"\\x0fcom.memos.storeB\\tMemoProtoP\\x01Z)github.com/usememos/memos/proto/gen/store\\xa2\\x02\\x03MSX\\xaa\\x02\\vMemos.Store\\xca\\x02\\vMemos\\\\Store\\xe2\\x02\\x17Memos\\\\Store\\\\GPBMetadata\\xea\\x02\\fMemos::Storeb\\x06proto3\"\n\nvar (\n\tfile_store_memo_proto_rawDescOnce sync.Once\n\tfile_store_memo_proto_rawDescData []byte\n)\n\nfunc file_store_memo_proto_rawDescGZIP() []byte {\n\tfile_store_memo_proto_rawDescOnce.Do(func() {\n\t\tfile_store_memo_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_store_memo_proto_rawDesc), len(file_store_memo_proto_rawDesc)))\n\t})\n\treturn file_store_memo_proto_rawDescData\n}\n\nvar file_store_memo_proto_msgTypes = make([]protoimpl.MessageInfo, 3)\nvar file_store_memo_proto_goTypes = []any{\n\t(*MemoPayload)(nil),          // 0: memos.store.MemoPayload\n\t(*MemoPayload_Property)(nil), // 1: memos.store.MemoPayload.Property\n\t(*MemoPayload_Location)(nil), // 2: memos.store.MemoPayload.Location\n}\nvar file_store_memo_proto_depIdxs = []int32{\n\t1, // 0: memos.store.MemoPayload.property:type_name -> memos.store.MemoPayload.Property\n\t2, // 1: memos.store.MemoPayload.location:type_name -> memos.store.MemoPayload.Location\n\t2, // [2:2] is the sub-list for method output_type\n\t2, // [2:2] is the sub-list for method input_type\n\t2, // [2:2] is the sub-list for extension type_name\n\t2, // [2:2] is the sub-list for extension extendee\n\t0, // [0:2] is the sub-list for field type_name\n}\n\nfunc init() { file_store_memo_proto_init() }\nfunc file_store_memo_proto_init() {\n\tif File_store_memo_proto != nil {\n\t\treturn\n\t}\n\ttype x struct{}\n\tout := protoimpl.TypeBuilder{\n\t\tFile: protoimpl.DescBuilder{\n\t\t\tGoPackagePath: reflect.TypeOf(x{}).PkgPath(),\n\t\t\tRawDescriptor: unsafe.Slice(unsafe.StringData(file_store_memo_proto_rawDesc), len(file_store_memo_proto_rawDesc)),\n\t\t\tNumEnums:      0,\n\t\t\tNumMessages:   3,\n\t\t\tNumExtensions: 0,\n\t\t\tNumServices:   0,\n\t\t},\n\t\tGoTypes:           file_store_memo_proto_goTypes,\n\t\tDependencyIndexes: file_store_memo_proto_depIdxs,\n\t\tMessageInfos:      file_store_memo_proto_msgTypes,\n\t}.Build()\n\tFile_store_memo_proto = out.File\n\tfile_store_memo_proto_goTypes = nil\n\tfile_store_memo_proto_depIdxs = nil\n}\n"
  },
  {
    "path": "proto/gen/store/user_setting.pb.go",
    "content": "// Code generated by protoc-gen-go. DO NOT EDIT.\n// versions:\n// \tprotoc-gen-go v1.36.11\n// \tprotoc        (unknown)\n// source: store/user_setting.proto\n\npackage store\n\nimport (\n\tprotoreflect \"google.golang.org/protobuf/reflect/protoreflect\"\n\tprotoimpl \"google.golang.org/protobuf/runtime/protoimpl\"\n\ttimestamppb \"google.golang.org/protobuf/types/known/timestamppb\"\n\treflect \"reflect\"\n\tsync \"sync\"\n\tunsafe \"unsafe\"\n)\n\nconst (\n\t// Verify that this generated code is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)\n\t// Verify that runtime/protoimpl is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)\n)\n\ntype UserSetting_Key int32\n\nconst (\n\tUserSetting_KEY_UNSPECIFIED UserSetting_Key = 0\n\t// General user settings.\n\tUserSetting_GENERAL UserSetting_Key = 1\n\t// The shortcuts of the user.\n\tUserSetting_SHORTCUTS UserSetting_Key = 4\n\t// The webhooks of the user.\n\tUserSetting_WEBHOOKS UserSetting_Key = 5\n\t// Refresh tokens for the user.\n\tUserSetting_REFRESH_TOKENS UserSetting_Key = 6\n\t// Personal access tokens for the user.\n\tUserSetting_PERSONAL_ACCESS_TOKENS UserSetting_Key = 7\n)\n\n// Enum value maps for UserSetting_Key.\nvar (\n\tUserSetting_Key_name = map[int32]string{\n\t\t0: \"KEY_UNSPECIFIED\",\n\t\t1: \"GENERAL\",\n\t\t4: \"SHORTCUTS\",\n\t\t5: \"WEBHOOKS\",\n\t\t6: \"REFRESH_TOKENS\",\n\t\t7: \"PERSONAL_ACCESS_TOKENS\",\n\t}\n\tUserSetting_Key_value = map[string]int32{\n\t\t\"KEY_UNSPECIFIED\":        0,\n\t\t\"GENERAL\":                1,\n\t\t\"SHORTCUTS\":              4,\n\t\t\"WEBHOOKS\":               5,\n\t\t\"REFRESH_TOKENS\":         6,\n\t\t\"PERSONAL_ACCESS_TOKENS\": 7,\n\t}\n)\n\nfunc (x UserSetting_Key) Enum() *UserSetting_Key {\n\tp := new(UserSetting_Key)\n\t*p = x\n\treturn p\n}\n\nfunc (x UserSetting_Key) String() string {\n\treturn protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))\n}\n\nfunc (UserSetting_Key) Descriptor() protoreflect.EnumDescriptor {\n\treturn file_store_user_setting_proto_enumTypes[0].Descriptor()\n}\n\nfunc (UserSetting_Key) Type() protoreflect.EnumType {\n\treturn &file_store_user_setting_proto_enumTypes[0]\n}\n\nfunc (x UserSetting_Key) Number() protoreflect.EnumNumber {\n\treturn protoreflect.EnumNumber(x)\n}\n\n// Deprecated: Use UserSetting_Key.Descriptor instead.\nfunc (UserSetting_Key) EnumDescriptor() ([]byte, []int) {\n\treturn file_store_user_setting_proto_rawDescGZIP(), []int{0, 0}\n}\n\ntype UserSetting struct {\n\tstate  protoimpl.MessageState `protogen:\"open.v1\"`\n\tUserId int32                  `protobuf:\"varint,1,opt,name=user_id,json=userId,proto3\" json:\"user_id,omitempty\"`\n\tKey    UserSetting_Key        `protobuf:\"varint,2,opt,name=key,proto3,enum=memos.store.UserSetting_Key\" json:\"key,omitempty\"`\n\t// Types that are valid to be assigned to Value:\n\t//\n\t//\t*UserSetting_General\n\t//\t*UserSetting_Shortcuts\n\t//\t*UserSetting_Webhooks\n\t//\t*UserSetting_RefreshTokens\n\t//\t*UserSetting_PersonalAccessTokens\n\tValue         isUserSetting_Value `protobuf_oneof:\"value\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *UserSetting) Reset() {\n\t*x = UserSetting{}\n\tmi := &file_store_user_setting_proto_msgTypes[0]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *UserSetting) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*UserSetting) ProtoMessage() {}\n\nfunc (x *UserSetting) ProtoReflect() protoreflect.Message {\n\tmi := &file_store_user_setting_proto_msgTypes[0]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use UserSetting.ProtoReflect.Descriptor instead.\nfunc (*UserSetting) Descriptor() ([]byte, []int) {\n\treturn file_store_user_setting_proto_rawDescGZIP(), []int{0}\n}\n\nfunc (x *UserSetting) GetUserId() int32 {\n\tif x != nil {\n\t\treturn x.UserId\n\t}\n\treturn 0\n}\n\nfunc (x *UserSetting) GetKey() UserSetting_Key {\n\tif x != nil {\n\t\treturn x.Key\n\t}\n\treturn UserSetting_KEY_UNSPECIFIED\n}\n\nfunc (x *UserSetting) GetValue() isUserSetting_Value {\n\tif x != nil {\n\t\treturn x.Value\n\t}\n\treturn nil\n}\n\nfunc (x *UserSetting) GetGeneral() *GeneralUserSetting {\n\tif x != nil {\n\t\tif x, ok := x.Value.(*UserSetting_General); ok {\n\t\t\treturn x.General\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *UserSetting) GetShortcuts() *ShortcutsUserSetting {\n\tif x != nil {\n\t\tif x, ok := x.Value.(*UserSetting_Shortcuts); ok {\n\t\t\treturn x.Shortcuts\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *UserSetting) GetWebhooks() *WebhooksUserSetting {\n\tif x != nil {\n\t\tif x, ok := x.Value.(*UserSetting_Webhooks); ok {\n\t\t\treturn x.Webhooks\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *UserSetting) GetRefreshTokens() *RefreshTokensUserSetting {\n\tif x != nil {\n\t\tif x, ok := x.Value.(*UserSetting_RefreshTokens); ok {\n\t\t\treturn x.RefreshTokens\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *UserSetting) GetPersonalAccessTokens() *PersonalAccessTokensUserSetting {\n\tif x != nil {\n\t\tif x, ok := x.Value.(*UserSetting_PersonalAccessTokens); ok {\n\t\t\treturn x.PersonalAccessTokens\n\t\t}\n\t}\n\treturn nil\n}\n\ntype isUserSetting_Value interface {\n\tisUserSetting_Value()\n}\n\ntype UserSetting_General struct {\n\tGeneral *GeneralUserSetting `protobuf:\"bytes,3,opt,name=general,proto3,oneof\"`\n}\n\ntype UserSetting_Shortcuts struct {\n\tShortcuts *ShortcutsUserSetting `protobuf:\"bytes,6,opt,name=shortcuts,proto3,oneof\"`\n}\n\ntype UserSetting_Webhooks struct {\n\tWebhooks *WebhooksUserSetting `protobuf:\"bytes,7,opt,name=webhooks,proto3,oneof\"`\n}\n\ntype UserSetting_RefreshTokens struct {\n\tRefreshTokens *RefreshTokensUserSetting `protobuf:\"bytes,8,opt,name=refresh_tokens,json=refreshTokens,proto3,oneof\"`\n}\n\ntype UserSetting_PersonalAccessTokens struct {\n\tPersonalAccessTokens *PersonalAccessTokensUserSetting `protobuf:\"bytes,9,opt,name=personal_access_tokens,json=personalAccessTokens,proto3,oneof\"`\n}\n\nfunc (*UserSetting_General) isUserSetting_Value() {}\n\nfunc (*UserSetting_Shortcuts) isUserSetting_Value() {}\n\nfunc (*UserSetting_Webhooks) isUserSetting_Value() {}\n\nfunc (*UserSetting_RefreshTokens) isUserSetting_Value() {}\n\nfunc (*UserSetting_PersonalAccessTokens) isUserSetting_Value() {}\n\ntype GeneralUserSetting struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// The user's locale.\n\tLocale string `protobuf:\"bytes,1,opt,name=locale,proto3\" json:\"locale,omitempty\"`\n\t// The user's memo visibility setting.\n\tMemoVisibility string `protobuf:\"bytes,2,opt,name=memo_visibility,json=memoVisibility,proto3\" json:\"memo_visibility,omitempty\"`\n\t// The user's theme preference.\n\t// This references a CSS file in the web/public/themes/ directory.\n\tTheme         string `protobuf:\"bytes,3,opt,name=theme,proto3\" json:\"theme,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *GeneralUserSetting) Reset() {\n\t*x = GeneralUserSetting{}\n\tmi := &file_store_user_setting_proto_msgTypes[1]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *GeneralUserSetting) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*GeneralUserSetting) ProtoMessage() {}\n\nfunc (x *GeneralUserSetting) ProtoReflect() protoreflect.Message {\n\tmi := &file_store_user_setting_proto_msgTypes[1]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use GeneralUserSetting.ProtoReflect.Descriptor instead.\nfunc (*GeneralUserSetting) Descriptor() ([]byte, []int) {\n\treturn file_store_user_setting_proto_rawDescGZIP(), []int{1}\n}\n\nfunc (x *GeneralUserSetting) GetLocale() string {\n\tif x != nil {\n\t\treturn x.Locale\n\t}\n\treturn \"\"\n}\n\nfunc (x *GeneralUserSetting) GetMemoVisibility() string {\n\tif x != nil {\n\t\treturn x.MemoVisibility\n\t}\n\treturn \"\"\n}\n\nfunc (x *GeneralUserSetting) GetTheme() string {\n\tif x != nil {\n\t\treturn x.Theme\n\t}\n\treturn \"\"\n}\n\ntype RefreshTokensUserSetting struct {\n\tstate         protoimpl.MessageState                   `protogen:\"open.v1\"`\n\tRefreshTokens []*RefreshTokensUserSetting_RefreshToken `protobuf:\"bytes,1,rep,name=refresh_tokens,json=refreshTokens,proto3\" json:\"refresh_tokens,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *RefreshTokensUserSetting) Reset() {\n\t*x = RefreshTokensUserSetting{}\n\tmi := &file_store_user_setting_proto_msgTypes[2]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *RefreshTokensUserSetting) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*RefreshTokensUserSetting) ProtoMessage() {}\n\nfunc (x *RefreshTokensUserSetting) ProtoReflect() protoreflect.Message {\n\tmi := &file_store_user_setting_proto_msgTypes[2]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use RefreshTokensUserSetting.ProtoReflect.Descriptor instead.\nfunc (*RefreshTokensUserSetting) Descriptor() ([]byte, []int) {\n\treturn file_store_user_setting_proto_rawDescGZIP(), []int{2}\n}\n\nfunc (x *RefreshTokensUserSetting) GetRefreshTokens() []*RefreshTokensUserSetting_RefreshToken {\n\tif x != nil {\n\t\treturn x.RefreshTokens\n\t}\n\treturn nil\n}\n\ntype PersonalAccessTokensUserSetting struct {\n\tstate         protoimpl.MessageState                                 `protogen:\"open.v1\"`\n\tTokens        []*PersonalAccessTokensUserSetting_PersonalAccessToken `protobuf:\"bytes,1,rep,name=tokens,proto3\" json:\"tokens,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *PersonalAccessTokensUserSetting) Reset() {\n\t*x = PersonalAccessTokensUserSetting{}\n\tmi := &file_store_user_setting_proto_msgTypes[3]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *PersonalAccessTokensUserSetting) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*PersonalAccessTokensUserSetting) ProtoMessage() {}\n\nfunc (x *PersonalAccessTokensUserSetting) ProtoReflect() protoreflect.Message {\n\tmi := &file_store_user_setting_proto_msgTypes[3]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use PersonalAccessTokensUserSetting.ProtoReflect.Descriptor instead.\nfunc (*PersonalAccessTokensUserSetting) Descriptor() ([]byte, []int) {\n\treturn file_store_user_setting_proto_rawDescGZIP(), []int{3}\n}\n\nfunc (x *PersonalAccessTokensUserSetting) GetTokens() []*PersonalAccessTokensUserSetting_PersonalAccessToken {\n\tif x != nil {\n\t\treturn x.Tokens\n\t}\n\treturn nil\n}\n\ntype ShortcutsUserSetting struct {\n\tstate         protoimpl.MessageState           `protogen:\"open.v1\"`\n\tShortcuts     []*ShortcutsUserSetting_Shortcut `protobuf:\"bytes,1,rep,name=shortcuts,proto3\" json:\"shortcuts,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *ShortcutsUserSetting) Reset() {\n\t*x = ShortcutsUserSetting{}\n\tmi := &file_store_user_setting_proto_msgTypes[4]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *ShortcutsUserSetting) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*ShortcutsUserSetting) ProtoMessage() {}\n\nfunc (x *ShortcutsUserSetting) ProtoReflect() protoreflect.Message {\n\tmi := &file_store_user_setting_proto_msgTypes[4]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use ShortcutsUserSetting.ProtoReflect.Descriptor instead.\nfunc (*ShortcutsUserSetting) Descriptor() ([]byte, []int) {\n\treturn file_store_user_setting_proto_rawDescGZIP(), []int{4}\n}\n\nfunc (x *ShortcutsUserSetting) GetShortcuts() []*ShortcutsUserSetting_Shortcut {\n\tif x != nil {\n\t\treturn x.Shortcuts\n\t}\n\treturn nil\n}\n\ntype WebhooksUserSetting struct {\n\tstate         protoimpl.MessageState         `protogen:\"open.v1\"`\n\tWebhooks      []*WebhooksUserSetting_Webhook `protobuf:\"bytes,1,rep,name=webhooks,proto3\" json:\"webhooks,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *WebhooksUserSetting) Reset() {\n\t*x = WebhooksUserSetting{}\n\tmi := &file_store_user_setting_proto_msgTypes[5]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *WebhooksUserSetting) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*WebhooksUserSetting) ProtoMessage() {}\n\nfunc (x *WebhooksUserSetting) ProtoReflect() protoreflect.Message {\n\tmi := &file_store_user_setting_proto_msgTypes[5]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use WebhooksUserSetting.ProtoReflect.Descriptor instead.\nfunc (*WebhooksUserSetting) Descriptor() ([]byte, []int) {\n\treturn file_store_user_setting_proto_rawDescGZIP(), []int{5}\n}\n\nfunc (x *WebhooksUserSetting) GetWebhooks() []*WebhooksUserSetting_Webhook {\n\tif x != nil {\n\t\treturn x.Webhooks\n\t}\n\treturn nil\n}\n\ntype RefreshTokensUserSetting_RefreshToken struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// Unique identifier (matches 'tid' claim in JWT)\n\tTokenId string `protobuf:\"bytes,1,opt,name=token_id,json=tokenId,proto3\" json:\"token_id,omitempty\"`\n\t// When the token expires\n\tExpiresAt *timestamppb.Timestamp `protobuf:\"bytes,2,opt,name=expires_at,json=expiresAt,proto3\" json:\"expires_at,omitempty\"`\n\t// When the token was created\n\tCreatedAt *timestamppb.Timestamp `protobuf:\"bytes,3,opt,name=created_at,json=createdAt,proto3\" json:\"created_at,omitempty\"`\n\t// Client information for session management UI\n\tClientInfo *RefreshTokensUserSetting_ClientInfo `protobuf:\"bytes,4,opt,name=client_info,json=clientInfo,proto3\" json:\"client_info,omitempty\"`\n\t// Optional description\n\tDescription   string `protobuf:\"bytes,5,opt,name=description,proto3\" json:\"description,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *RefreshTokensUserSetting_RefreshToken) Reset() {\n\t*x = RefreshTokensUserSetting_RefreshToken{}\n\tmi := &file_store_user_setting_proto_msgTypes[6]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *RefreshTokensUserSetting_RefreshToken) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*RefreshTokensUserSetting_RefreshToken) ProtoMessage() {}\n\nfunc (x *RefreshTokensUserSetting_RefreshToken) ProtoReflect() protoreflect.Message {\n\tmi := &file_store_user_setting_proto_msgTypes[6]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use RefreshTokensUserSetting_RefreshToken.ProtoReflect.Descriptor instead.\nfunc (*RefreshTokensUserSetting_RefreshToken) Descriptor() ([]byte, []int) {\n\treturn file_store_user_setting_proto_rawDescGZIP(), []int{2, 0}\n}\n\nfunc (x *RefreshTokensUserSetting_RefreshToken) GetTokenId() string {\n\tif x != nil {\n\t\treturn x.TokenId\n\t}\n\treturn \"\"\n}\n\nfunc (x *RefreshTokensUserSetting_RefreshToken) GetExpiresAt() *timestamppb.Timestamp {\n\tif x != nil {\n\t\treturn x.ExpiresAt\n\t}\n\treturn nil\n}\n\nfunc (x *RefreshTokensUserSetting_RefreshToken) GetCreatedAt() *timestamppb.Timestamp {\n\tif x != nil {\n\t\treturn x.CreatedAt\n\t}\n\treturn nil\n}\n\nfunc (x *RefreshTokensUserSetting_RefreshToken) GetClientInfo() *RefreshTokensUserSetting_ClientInfo {\n\tif x != nil {\n\t\treturn x.ClientInfo\n\t}\n\treturn nil\n}\n\nfunc (x *RefreshTokensUserSetting_RefreshToken) GetDescription() string {\n\tif x != nil {\n\t\treturn x.Description\n\t}\n\treturn \"\"\n}\n\ntype RefreshTokensUserSetting_ClientInfo struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// User agent string of the client.\n\tUserAgent string `protobuf:\"bytes,1,opt,name=user_agent,json=userAgent,proto3\" json:\"user_agent,omitempty\"`\n\t// IP address of the client.\n\tIpAddress string `protobuf:\"bytes,2,opt,name=ip_address,json=ipAddress,proto3\" json:\"ip_address,omitempty\"`\n\t// Optional. Device type (e.g., \"mobile\", \"desktop\", \"tablet\").\n\tDeviceType string `protobuf:\"bytes,3,opt,name=device_type,json=deviceType,proto3\" json:\"device_type,omitempty\"`\n\t// Optional. Operating system (e.g., \"iOS 17.0\", \"Windows 11\").\n\tOs string `protobuf:\"bytes,4,opt,name=os,proto3\" json:\"os,omitempty\"`\n\t// Optional. Browser name and version (e.g., \"Chrome 119.0\").\n\tBrowser       string `protobuf:\"bytes,5,opt,name=browser,proto3\" json:\"browser,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *RefreshTokensUserSetting_ClientInfo) Reset() {\n\t*x = RefreshTokensUserSetting_ClientInfo{}\n\tmi := &file_store_user_setting_proto_msgTypes[7]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *RefreshTokensUserSetting_ClientInfo) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*RefreshTokensUserSetting_ClientInfo) ProtoMessage() {}\n\nfunc (x *RefreshTokensUserSetting_ClientInfo) ProtoReflect() protoreflect.Message {\n\tmi := &file_store_user_setting_proto_msgTypes[7]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use RefreshTokensUserSetting_ClientInfo.ProtoReflect.Descriptor instead.\nfunc (*RefreshTokensUserSetting_ClientInfo) Descriptor() ([]byte, []int) {\n\treturn file_store_user_setting_proto_rawDescGZIP(), []int{2, 1}\n}\n\nfunc (x *RefreshTokensUserSetting_ClientInfo) GetUserAgent() string {\n\tif x != nil {\n\t\treturn x.UserAgent\n\t}\n\treturn \"\"\n}\n\nfunc (x *RefreshTokensUserSetting_ClientInfo) GetIpAddress() string {\n\tif x != nil {\n\t\treturn x.IpAddress\n\t}\n\treturn \"\"\n}\n\nfunc (x *RefreshTokensUserSetting_ClientInfo) GetDeviceType() string {\n\tif x != nil {\n\t\treturn x.DeviceType\n\t}\n\treturn \"\"\n}\n\nfunc (x *RefreshTokensUserSetting_ClientInfo) GetOs() string {\n\tif x != nil {\n\t\treturn x.Os\n\t}\n\treturn \"\"\n}\n\nfunc (x *RefreshTokensUserSetting_ClientInfo) GetBrowser() string {\n\tif x != nil {\n\t\treturn x.Browser\n\t}\n\treturn \"\"\n}\n\ntype PersonalAccessTokensUserSetting_PersonalAccessToken struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// Unique identifier for this token\n\tTokenId string `protobuf:\"bytes,1,opt,name=token_id,json=tokenId,proto3\" json:\"token_id,omitempty\"`\n\t// SHA-256 hash of the actual token\n\tTokenHash string `protobuf:\"bytes,2,opt,name=token_hash,json=tokenHash,proto3\" json:\"token_hash,omitempty\"`\n\t// User-provided description\n\tDescription string `protobuf:\"bytes,3,opt,name=description,proto3\" json:\"description,omitempty\"`\n\t// When the token expires (null = never)\n\tExpiresAt *timestamppb.Timestamp `protobuf:\"bytes,4,opt,name=expires_at,json=expiresAt,proto3\" json:\"expires_at,omitempty\"`\n\t// When the token was created\n\tCreatedAt *timestamppb.Timestamp `protobuf:\"bytes,5,opt,name=created_at,json=createdAt,proto3\" json:\"created_at,omitempty\"`\n\t// When the token was last used\n\tLastUsedAt    *timestamppb.Timestamp `protobuf:\"bytes,6,opt,name=last_used_at,json=lastUsedAt,proto3\" json:\"last_used_at,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *PersonalAccessTokensUserSetting_PersonalAccessToken) Reset() {\n\t*x = PersonalAccessTokensUserSetting_PersonalAccessToken{}\n\tmi := &file_store_user_setting_proto_msgTypes[8]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *PersonalAccessTokensUserSetting_PersonalAccessToken) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*PersonalAccessTokensUserSetting_PersonalAccessToken) ProtoMessage() {}\n\nfunc (x *PersonalAccessTokensUserSetting_PersonalAccessToken) ProtoReflect() protoreflect.Message {\n\tmi := &file_store_user_setting_proto_msgTypes[8]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use PersonalAccessTokensUserSetting_PersonalAccessToken.ProtoReflect.Descriptor instead.\nfunc (*PersonalAccessTokensUserSetting_PersonalAccessToken) Descriptor() ([]byte, []int) {\n\treturn file_store_user_setting_proto_rawDescGZIP(), []int{3, 0}\n}\n\nfunc (x *PersonalAccessTokensUserSetting_PersonalAccessToken) GetTokenId() string {\n\tif x != nil {\n\t\treturn x.TokenId\n\t}\n\treturn \"\"\n}\n\nfunc (x *PersonalAccessTokensUserSetting_PersonalAccessToken) GetTokenHash() string {\n\tif x != nil {\n\t\treturn x.TokenHash\n\t}\n\treturn \"\"\n}\n\nfunc (x *PersonalAccessTokensUserSetting_PersonalAccessToken) GetDescription() string {\n\tif x != nil {\n\t\treturn x.Description\n\t}\n\treturn \"\"\n}\n\nfunc (x *PersonalAccessTokensUserSetting_PersonalAccessToken) GetExpiresAt() *timestamppb.Timestamp {\n\tif x != nil {\n\t\treturn x.ExpiresAt\n\t}\n\treturn nil\n}\n\nfunc (x *PersonalAccessTokensUserSetting_PersonalAccessToken) GetCreatedAt() *timestamppb.Timestamp {\n\tif x != nil {\n\t\treturn x.CreatedAt\n\t}\n\treturn nil\n}\n\nfunc (x *PersonalAccessTokensUserSetting_PersonalAccessToken) GetLastUsedAt() *timestamppb.Timestamp {\n\tif x != nil {\n\t\treturn x.LastUsedAt\n\t}\n\treturn nil\n}\n\ntype ShortcutsUserSetting_Shortcut struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tId            string                 `protobuf:\"bytes,1,opt,name=id,proto3\" json:\"id,omitempty\"`\n\tTitle         string                 `protobuf:\"bytes,2,opt,name=title,proto3\" json:\"title,omitempty\"`\n\tFilter        string                 `protobuf:\"bytes,3,opt,name=filter,proto3\" json:\"filter,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *ShortcutsUserSetting_Shortcut) Reset() {\n\t*x = ShortcutsUserSetting_Shortcut{}\n\tmi := &file_store_user_setting_proto_msgTypes[9]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *ShortcutsUserSetting_Shortcut) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*ShortcutsUserSetting_Shortcut) ProtoMessage() {}\n\nfunc (x *ShortcutsUserSetting_Shortcut) ProtoReflect() protoreflect.Message {\n\tmi := &file_store_user_setting_proto_msgTypes[9]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use ShortcutsUserSetting_Shortcut.ProtoReflect.Descriptor instead.\nfunc (*ShortcutsUserSetting_Shortcut) Descriptor() ([]byte, []int) {\n\treturn file_store_user_setting_proto_rawDescGZIP(), []int{4, 0}\n}\n\nfunc (x *ShortcutsUserSetting_Shortcut) GetId() string {\n\tif x != nil {\n\t\treturn x.Id\n\t}\n\treturn \"\"\n}\n\nfunc (x *ShortcutsUserSetting_Shortcut) GetTitle() string {\n\tif x != nil {\n\t\treturn x.Title\n\t}\n\treturn \"\"\n}\n\nfunc (x *ShortcutsUserSetting_Shortcut) GetFilter() string {\n\tif x != nil {\n\t\treturn x.Filter\n\t}\n\treturn \"\"\n}\n\ntype WebhooksUserSetting_Webhook struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// Unique identifier for the webhook\n\tId string `protobuf:\"bytes,1,opt,name=id,proto3\" json:\"id,omitempty\"`\n\t// Descriptive title for the webhook\n\tTitle string `protobuf:\"bytes,2,opt,name=title,proto3\" json:\"title,omitempty\"`\n\t// The webhook URL endpoint\n\tUrl           string `protobuf:\"bytes,3,opt,name=url,proto3\" json:\"url,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *WebhooksUserSetting_Webhook) Reset() {\n\t*x = WebhooksUserSetting_Webhook{}\n\tmi := &file_store_user_setting_proto_msgTypes[10]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *WebhooksUserSetting_Webhook) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*WebhooksUserSetting_Webhook) ProtoMessage() {}\n\nfunc (x *WebhooksUserSetting_Webhook) ProtoReflect() protoreflect.Message {\n\tmi := &file_store_user_setting_proto_msgTypes[10]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use WebhooksUserSetting_Webhook.ProtoReflect.Descriptor instead.\nfunc (*WebhooksUserSetting_Webhook) Descriptor() ([]byte, []int) {\n\treturn file_store_user_setting_proto_rawDescGZIP(), []int{5, 0}\n}\n\nfunc (x *WebhooksUserSetting_Webhook) GetId() string {\n\tif x != nil {\n\t\treturn x.Id\n\t}\n\treturn \"\"\n}\n\nfunc (x *WebhooksUserSetting_Webhook) GetTitle() string {\n\tif x != nil {\n\t\treturn x.Title\n\t}\n\treturn \"\"\n}\n\nfunc (x *WebhooksUserSetting_Webhook) GetUrl() string {\n\tif x != nil {\n\t\treturn x.Url\n\t}\n\treturn \"\"\n}\n\nvar File_store_user_setting_proto protoreflect.FileDescriptor\n\nconst file_store_user_setting_proto_rawDesc = \"\" +\n\t\"\\n\" +\n\t\"\\x18store/user_setting.proto\\x12\\vmemos.store\\x1a\\x1fgoogle/protobuf/timestamp.proto\\\"\\xcb\\x04\\n\" +\n\t\"\\vUserSetting\\x12\\x17\\n\" +\n\t\"\\auser_id\\x18\\x01 \\x01(\\x05R\\x06userId\\x12.\\n\" +\n\t\"\\x03key\\x18\\x02 \\x01(\\x0e2\\x1c.memos.store.UserSetting.KeyR\\x03key\\x12;\\n\" +\n\t\"\\ageneral\\x18\\x03 \\x01(\\v2\\x1f.memos.store.GeneralUserSettingH\\x00R\\ageneral\\x12A\\n\" +\n\t\"\\tshortcuts\\x18\\x06 \\x01(\\v2!.memos.store.ShortcutsUserSettingH\\x00R\\tshortcuts\\x12>\\n\" +\n\t\"\\bwebhooks\\x18\\a \\x01(\\v2 .memos.store.WebhooksUserSettingH\\x00R\\bwebhooks\\x12N\\n\" +\n\t\"\\x0erefresh_tokens\\x18\\b \\x01(\\v2%.memos.store.RefreshTokensUserSettingH\\x00R\\rrefreshTokens\\x12d\\n\" +\n\t\"\\x16personal_access_tokens\\x18\\t \\x01(\\v2,.memos.store.PersonalAccessTokensUserSettingH\\x00R\\x14personalAccessTokens\\\"t\\n\" +\n\t\"\\x03Key\\x12\\x13\\n\" +\n\t\"\\x0fKEY_UNSPECIFIED\\x10\\x00\\x12\\v\\n\" +\n\t\"\\aGENERAL\\x10\\x01\\x12\\r\\n\" +\n\t\"\\tSHORTCUTS\\x10\\x04\\x12\\f\\n\" +\n\t\"\\bWEBHOOKS\\x10\\x05\\x12\\x12\\n\" +\n\t\"\\x0eREFRESH_TOKENS\\x10\\x06\\x12\\x1a\\n\" +\n\t\"\\x16PERSONAL_ACCESS_TOKENS\\x10\\aB\\a\\n\" +\n\t\"\\x05value\\\"k\\n\" +\n\t\"\\x12GeneralUserSetting\\x12\\x16\\n\" +\n\t\"\\x06locale\\x18\\x01 \\x01(\\tR\\x06locale\\x12'\\n\" +\n\t\"\\x0fmemo_visibility\\x18\\x02 \\x01(\\tR\\x0ememoVisibility\\x12\\x14\\n\" +\n\t\"\\x05theme\\x18\\x03 \\x01(\\tR\\x05theme\\\"\\xa4\\x04\\n\" +\n\t\"\\x18RefreshTokensUserSetting\\x12Y\\n\" +\n\t\"\\x0erefresh_tokens\\x18\\x01 \\x03(\\v22.memos.store.RefreshTokensUserSetting.RefreshTokenR\\rrefreshTokens\\x1a\\x94\\x02\\n\" +\n\t\"\\fRefreshToken\\x12\\x19\\n\" +\n\t\"\\btoken_id\\x18\\x01 \\x01(\\tR\\atokenId\\x129\\n\" +\n\t\"\\n\" +\n\t\"expires_at\\x18\\x02 \\x01(\\v2\\x1a.google.protobuf.TimestampR\\texpiresAt\\x129\\n\" +\n\t\"\\n\" +\n\t\"created_at\\x18\\x03 \\x01(\\v2\\x1a.google.protobuf.TimestampR\\tcreatedAt\\x12Q\\n\" +\n\t\"\\vclient_info\\x18\\x04 \\x01(\\v20.memos.store.RefreshTokensUserSetting.ClientInfoR\\n\" +\n\t\"clientInfo\\x12 \\n\" +\n\t\"\\vdescription\\x18\\x05 \\x01(\\tR\\vdescription\\x1a\\x95\\x01\\n\" +\n\t\"\\n\" +\n\t\"ClientInfo\\x12\\x1d\\n\" +\n\t\"\\n\" +\n\t\"user_agent\\x18\\x01 \\x01(\\tR\\tuserAgent\\x12\\x1d\\n\" +\n\t\"\\n\" +\n\t\"ip_address\\x18\\x02 \\x01(\\tR\\tipAddress\\x12\\x1f\\n\" +\n\t\"\\vdevice_type\\x18\\x03 \\x01(\\tR\\n\" +\n\t\"deviceType\\x12\\x0e\\n\" +\n\t\"\\x02os\\x18\\x04 \\x01(\\tR\\x02os\\x12\\x18\\n\" +\n\t\"\\abrowser\\x18\\x05 \\x01(\\tR\\abrowser\\\"\\xa3\\x03\\n\" +\n\t\"\\x1fPersonalAccessTokensUserSetting\\x12X\\n\" +\n\t\"\\x06tokens\\x18\\x01 \\x03(\\v2@.memos.store.PersonalAccessTokensUserSetting.PersonalAccessTokenR\\x06tokens\\x1a\\xa5\\x02\\n\" +\n\t\"\\x13PersonalAccessToken\\x12\\x19\\n\" +\n\t\"\\btoken_id\\x18\\x01 \\x01(\\tR\\atokenId\\x12\\x1d\\n\" +\n\t\"\\n\" +\n\t\"token_hash\\x18\\x02 \\x01(\\tR\\ttokenHash\\x12 \\n\" +\n\t\"\\vdescription\\x18\\x03 \\x01(\\tR\\vdescription\\x129\\n\" +\n\t\"\\n\" +\n\t\"expires_at\\x18\\x04 \\x01(\\v2\\x1a.google.protobuf.TimestampR\\texpiresAt\\x129\\n\" +\n\t\"\\n\" +\n\t\"created_at\\x18\\x05 \\x01(\\v2\\x1a.google.protobuf.TimestampR\\tcreatedAt\\x12<\\n\" +\n\t\"\\flast_used_at\\x18\\x06 \\x01(\\v2\\x1a.google.protobuf.TimestampR\\n\" +\n\t\"lastUsedAt\\\"\\xaa\\x01\\n\" +\n\t\"\\x14ShortcutsUserSetting\\x12H\\n\" +\n\t\"\\tshortcuts\\x18\\x01 \\x03(\\v2*.memos.store.ShortcutsUserSetting.ShortcutR\\tshortcuts\\x1aH\\n\" +\n\t\"\\bShortcut\\x12\\x0e\\n\" +\n\t\"\\x02id\\x18\\x01 \\x01(\\tR\\x02id\\x12\\x14\\n\" +\n\t\"\\x05title\\x18\\x02 \\x01(\\tR\\x05title\\x12\\x16\\n\" +\n\t\"\\x06filter\\x18\\x03 \\x01(\\tR\\x06filter\\\"\\x9e\\x01\\n\" +\n\t\"\\x13WebhooksUserSetting\\x12D\\n\" +\n\t\"\\bwebhooks\\x18\\x01 \\x03(\\v2(.memos.store.WebhooksUserSetting.WebhookR\\bwebhooks\\x1aA\\n\" +\n\t\"\\aWebhook\\x12\\x0e\\n\" +\n\t\"\\x02id\\x18\\x01 \\x01(\\tR\\x02id\\x12\\x14\\n\" +\n\t\"\\x05title\\x18\\x02 \\x01(\\tR\\x05title\\x12\\x10\\n\" +\n\t\"\\x03url\\x18\\x03 \\x01(\\tR\\x03urlB\\x9b\\x01\\n\" +\n\t\"\\x0fcom.memos.storeB\\x10UserSettingProtoP\\x01Z)github.com/usememos/memos/proto/gen/store\\xa2\\x02\\x03MSX\\xaa\\x02\\vMemos.Store\\xca\\x02\\vMemos\\\\Store\\xe2\\x02\\x17Memos\\\\Store\\\\GPBMetadata\\xea\\x02\\fMemos::Storeb\\x06proto3\"\n\nvar (\n\tfile_store_user_setting_proto_rawDescOnce sync.Once\n\tfile_store_user_setting_proto_rawDescData []byte\n)\n\nfunc file_store_user_setting_proto_rawDescGZIP() []byte {\n\tfile_store_user_setting_proto_rawDescOnce.Do(func() {\n\t\tfile_store_user_setting_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_store_user_setting_proto_rawDesc), len(file_store_user_setting_proto_rawDesc)))\n\t})\n\treturn file_store_user_setting_proto_rawDescData\n}\n\nvar file_store_user_setting_proto_enumTypes = make([]protoimpl.EnumInfo, 1)\nvar file_store_user_setting_proto_msgTypes = make([]protoimpl.MessageInfo, 11)\nvar file_store_user_setting_proto_goTypes = []any{\n\t(UserSetting_Key)(0),                                        // 0: memos.store.UserSetting.Key\n\t(*UserSetting)(nil),                                         // 1: memos.store.UserSetting\n\t(*GeneralUserSetting)(nil),                                  // 2: memos.store.GeneralUserSetting\n\t(*RefreshTokensUserSetting)(nil),                            // 3: memos.store.RefreshTokensUserSetting\n\t(*PersonalAccessTokensUserSetting)(nil),                     // 4: memos.store.PersonalAccessTokensUserSetting\n\t(*ShortcutsUserSetting)(nil),                                // 5: memos.store.ShortcutsUserSetting\n\t(*WebhooksUserSetting)(nil),                                 // 6: memos.store.WebhooksUserSetting\n\t(*RefreshTokensUserSetting_RefreshToken)(nil),               // 7: memos.store.RefreshTokensUserSetting.RefreshToken\n\t(*RefreshTokensUserSetting_ClientInfo)(nil),                 // 8: memos.store.RefreshTokensUserSetting.ClientInfo\n\t(*PersonalAccessTokensUserSetting_PersonalAccessToken)(nil), // 9: memos.store.PersonalAccessTokensUserSetting.PersonalAccessToken\n\t(*ShortcutsUserSetting_Shortcut)(nil),                       // 10: memos.store.ShortcutsUserSetting.Shortcut\n\t(*WebhooksUserSetting_Webhook)(nil),                         // 11: memos.store.WebhooksUserSetting.Webhook\n\t(*timestamppb.Timestamp)(nil),                               // 12: google.protobuf.Timestamp\n}\nvar file_store_user_setting_proto_depIdxs = []int32{\n\t0,  // 0: memos.store.UserSetting.key:type_name -> memos.store.UserSetting.Key\n\t2,  // 1: memos.store.UserSetting.general:type_name -> memos.store.GeneralUserSetting\n\t5,  // 2: memos.store.UserSetting.shortcuts:type_name -> memos.store.ShortcutsUserSetting\n\t6,  // 3: memos.store.UserSetting.webhooks:type_name -> memos.store.WebhooksUserSetting\n\t3,  // 4: memos.store.UserSetting.refresh_tokens:type_name -> memos.store.RefreshTokensUserSetting\n\t4,  // 5: memos.store.UserSetting.personal_access_tokens:type_name -> memos.store.PersonalAccessTokensUserSetting\n\t7,  // 6: memos.store.RefreshTokensUserSetting.refresh_tokens:type_name -> memos.store.RefreshTokensUserSetting.RefreshToken\n\t9,  // 7: memos.store.PersonalAccessTokensUserSetting.tokens:type_name -> memos.store.PersonalAccessTokensUserSetting.PersonalAccessToken\n\t10, // 8: memos.store.ShortcutsUserSetting.shortcuts:type_name -> memos.store.ShortcutsUserSetting.Shortcut\n\t11, // 9: memos.store.WebhooksUserSetting.webhooks:type_name -> memos.store.WebhooksUserSetting.Webhook\n\t12, // 10: memos.store.RefreshTokensUserSetting.RefreshToken.expires_at:type_name -> google.protobuf.Timestamp\n\t12, // 11: memos.store.RefreshTokensUserSetting.RefreshToken.created_at:type_name -> google.protobuf.Timestamp\n\t8,  // 12: memos.store.RefreshTokensUserSetting.RefreshToken.client_info:type_name -> memos.store.RefreshTokensUserSetting.ClientInfo\n\t12, // 13: memos.store.PersonalAccessTokensUserSetting.PersonalAccessToken.expires_at:type_name -> google.protobuf.Timestamp\n\t12, // 14: memos.store.PersonalAccessTokensUserSetting.PersonalAccessToken.created_at:type_name -> google.protobuf.Timestamp\n\t12, // 15: memos.store.PersonalAccessTokensUserSetting.PersonalAccessToken.last_used_at:type_name -> google.protobuf.Timestamp\n\t16, // [16:16] is the sub-list for method output_type\n\t16, // [16:16] is the sub-list for method input_type\n\t16, // [16:16] is the sub-list for extension type_name\n\t16, // [16:16] is the sub-list for extension extendee\n\t0,  // [0:16] is the sub-list for field type_name\n}\n\nfunc init() { file_store_user_setting_proto_init() }\nfunc file_store_user_setting_proto_init() {\n\tif File_store_user_setting_proto != nil {\n\t\treturn\n\t}\n\tfile_store_user_setting_proto_msgTypes[0].OneofWrappers = []any{\n\t\t(*UserSetting_General)(nil),\n\t\t(*UserSetting_Shortcuts)(nil),\n\t\t(*UserSetting_Webhooks)(nil),\n\t\t(*UserSetting_RefreshTokens)(nil),\n\t\t(*UserSetting_PersonalAccessTokens)(nil),\n\t}\n\ttype x struct{}\n\tout := protoimpl.TypeBuilder{\n\t\tFile: protoimpl.DescBuilder{\n\t\t\tGoPackagePath: reflect.TypeOf(x{}).PkgPath(),\n\t\t\tRawDescriptor: unsafe.Slice(unsafe.StringData(file_store_user_setting_proto_rawDesc), len(file_store_user_setting_proto_rawDesc)),\n\t\t\tNumEnums:      1,\n\t\t\tNumMessages:   11,\n\t\t\tNumExtensions: 0,\n\t\t\tNumServices:   0,\n\t\t},\n\t\tGoTypes:           file_store_user_setting_proto_goTypes,\n\t\tDependencyIndexes: file_store_user_setting_proto_depIdxs,\n\t\tEnumInfos:         file_store_user_setting_proto_enumTypes,\n\t\tMessageInfos:      file_store_user_setting_proto_msgTypes,\n\t}.Build()\n\tFile_store_user_setting_proto = out.File\n\tfile_store_user_setting_proto_goTypes = nil\n\tfile_store_user_setting_proto_depIdxs = nil\n}\n"
  },
  {
    "path": "proto/store/attachment.proto",
    "content": "syntax = \"proto3\";\n\npackage memos.store;\n\nimport \"google/protobuf/timestamp.proto\";\nimport \"store/instance_setting.proto\";\n\noption go_package = \"gen/store\";\n\nenum AttachmentStorageType {\n  ATTACHMENT_STORAGE_TYPE_UNSPECIFIED = 0;\n  // Attachment is stored locally. AKA, local file system.\n  LOCAL = 1;\n  // Attachment is stored in S3.\n  S3 = 2;\n  // Attachment is stored in an external storage. The reference is a URL.\n  EXTERNAL = 3;\n}\n\nmessage AttachmentPayload {\n  oneof payload {\n    S3Object s3_object = 1;\n  }\n\n  message S3Object {\n    StorageS3Config s3_config = 1;\n    // key is the S3 object key.\n    string key = 2;\n    // last_presigned_time is the last time the object was presigned.\n    // This is used to determine if the presigned URL is still valid.\n    google.protobuf.Timestamp last_presigned_time = 3;\n  }\n}\n"
  },
  {
    "path": "proto/store/idp.proto",
    "content": "syntax = \"proto3\";\n\npackage memos.store;\n\noption go_package = \"gen/store\";\n\nmessage IdentityProvider {\n  int32 id = 1;\n  string name = 2;\n\n  enum Type {\n    TYPE_UNSPECIFIED = 0;\n    OAUTH2 = 1;\n  }\n  Type type = 3;\n  string identifier_filter = 4;\n  IdentityProviderConfig config = 5;\n  string uid = 6;\n}\n\nmessage IdentityProviderConfig {\n  oneof config {\n    OAuth2Config oauth2_config = 1;\n  }\n}\n\nmessage FieldMapping {\n  string identifier = 1;\n  string display_name = 2;\n  string email = 3;\n  string avatar_url = 4;\n}\n\nmessage OAuth2Config {\n  string client_id = 1;\n  string client_secret = 2;\n  string auth_url = 3;\n  string token_url = 4;\n  string user_info_url = 5;\n  repeated string scopes = 6;\n  FieldMapping field_mapping = 7;\n}\n"
  },
  {
    "path": "proto/store/inbox.proto",
    "content": "syntax = \"proto3\";\n\npackage memos.store;\n\noption go_package = \"gen/store\";\n\nmessage InboxMessage {\n  message MemoCommentPayload {\n    int32 memo_id = 1;\n    int32 related_memo_id = 2;\n  }\n\n  // The type of the inbox message.\n  Type type = 1;\n  oneof payload {\n    MemoCommentPayload memo_comment = 2;\n  }\n\n  enum Type {\n    TYPE_UNSPECIFIED = 0;\n    // Memo comment notification.\n    MEMO_COMMENT = 1;\n  }\n}\n"
  },
  {
    "path": "proto/store/instance_setting.proto",
    "content": "syntax = \"proto3\";\n\npackage memos.store;\n\nimport \"google/type/color.proto\";\n\noption go_package = \"gen/store\";\n\nenum InstanceSettingKey {\n  INSTANCE_SETTING_KEY_UNSPECIFIED = 0;\n  // BASIC is the key for basic settings.\n  BASIC = 1;\n  // GENERAL is the key for general settings.\n  GENERAL = 2;\n  // STORAGE is the key for storage settings.\n  STORAGE = 3;\n  // MEMO_RELATED is the key for memo related settings.\n  MEMO_RELATED = 4;\n  // TAGS is the key for tag metadata.\n  TAGS = 5;\n  // NOTIFICATION is the key for notification transport settings.\n  NOTIFICATION = 6;\n}\n\nmessage InstanceSetting {\n  InstanceSettingKey key = 1;\n  oneof value {\n    InstanceBasicSetting basic_setting = 2;\n    InstanceGeneralSetting general_setting = 3;\n    InstanceStorageSetting storage_setting = 4;\n    InstanceMemoRelatedSetting memo_related_setting = 5;\n    InstanceTagsSetting tags_setting = 6;\n    InstanceNotificationSetting notification_setting = 7;\n  }\n}\n\nmessage InstanceBasicSetting {\n  // The secret key for instance. Mainly used for session management.\n  string secret_key = 1;\n  // The current schema version of database.\n  string schema_version = 2;\n}\n\nmessage InstanceGeneralSetting {\n  // disallow_user_registration disallows user registration.\n  bool disallow_user_registration = 2;\n  // disallow_password_auth disallows password authentication.\n  bool disallow_password_auth = 3;\n  // additional_script is the additional script.\n  string additional_script = 4;\n  // additional_style is the additional style.\n  string additional_style = 5;\n  // custom_profile is the custom profile.\n  InstanceCustomProfile custom_profile = 6;\n  // week_start_day_offset is the week start day offset from Sunday.\n  // 0: Sunday, 1: Monday, 2: Tuesday, 3: Wednesday, 4: Thursday, 5: Friday, 6: Saturday\n  // Default is Sunday.\n  int32 week_start_day_offset = 7;\n  // disallow_change_username disallows changing username.\n  bool disallow_change_username = 8;\n  // disallow_change_nickname disallows changing nickname.\n  bool disallow_change_nickname = 9;\n}\n\nmessage InstanceCustomProfile {\n  string title = 1;\n  string description = 2;\n  string logo_url = 3;\n}\n\nmessage InstanceStorageSetting {\n  enum StorageType {\n    STORAGE_TYPE_UNSPECIFIED = 0;\n    // STORAGE_TYPE_DATABASE is the database storage type.\n    DATABASE = 1;\n    // STORAGE_TYPE_LOCAL is the local storage type.\n    LOCAL = 2;\n    // STORAGE_TYPE_S3 is the S3 storage type.\n    S3 = 3;\n  }\n  // storage_type is the storage type.\n  StorageType storage_type = 1;\n  // The template of file path.\n  // e.g. assets/{timestamp}_{filename}\n  string filepath_template = 2;\n  // The max upload size in megabytes.\n  int64 upload_size_limit_mb = 3;\n  // The S3 config.\n  StorageS3Config s3_config = 4;\n}\n\n// Reference: https://developers.cloudflare.com/r2/examples/aws/aws-sdk-go/\nmessage StorageS3Config {\n  string access_key_id = 1;\n  string access_key_secret = 2;\n  string endpoint = 3;\n  string region = 4;\n  string bucket = 5;\n  bool use_path_style = 6;\n}\n\nmessage InstanceMemoRelatedSetting {\n  // display_with_update_time orders and displays memo with update time.\n  bool display_with_update_time = 2;\n  // content_length_limit is the limit of content length. Unit is byte.\n  int32 content_length_limit = 3;\n  // enable_double_click_edit enables editing on double click.\n  bool enable_double_click_edit = 4;\n  // reactions is the list of reactions.\n  repeated string reactions = 7;\n}\n\nmessage InstanceTagMetadata {\n  // Background color for the tag label.\n  google.type.Color background_color = 1;\n}\n\nmessage InstanceTagsSetting {\n  map<string, InstanceTagMetadata> tags = 1;\n}\n\nmessage InstanceNotificationSetting {\n  EmailSetting email = 1;\n\n  message EmailSetting {\n    bool enabled = 1;\n    string smtp_host = 2;\n    int32 smtp_port = 3;\n    string smtp_username = 4;\n    string smtp_password = 5;\n    string from_email = 6;\n    string from_name = 7;\n    string reply_to = 8;\n    bool use_tls = 9;\n    bool use_ssl = 10;\n  }\n}\n"
  },
  {
    "path": "proto/store/memo.proto",
    "content": "syntax = \"proto3\";\n\npackage memos.store;\n\noption go_package = \"gen/store\";\n\nmessage MemoPayload {\n  Property property = 1;\n\n  Location location = 2;\n\n  repeated string tags = 3;\n\n  // The calculated properties from the memo content.\n  message Property {\n    bool has_link = 1;\n    bool has_task_list = 2;\n    bool has_code = 3;\n    bool has_incomplete_tasks = 4;\n    // The title extracted from the first H1 heading, if present.\n    string title = 5;\n  }\n\n  message Location {\n    string placeholder = 1;\n    double latitude = 2;\n    double longitude = 3;\n  }\n}\n"
  },
  {
    "path": "proto/store/user_setting.proto",
    "content": "syntax = \"proto3\";\n\npackage memos.store;\n\nimport \"google/protobuf/timestamp.proto\";\n\noption go_package = \"gen/store\";\n\nmessage UserSetting {\n  enum Key {\n    KEY_UNSPECIFIED = 0;\n    // General user settings.\n    GENERAL = 1;\n    // The shortcuts of the user.\n    SHORTCUTS = 4;\n    // The webhooks of the user.\n    WEBHOOKS = 5;\n    // Refresh tokens for the user.\n    REFRESH_TOKENS = 6;\n    // Personal access tokens for the user.\n    PERSONAL_ACCESS_TOKENS = 7;\n  }\n\n  int32 user_id = 1;\n\n  Key key = 2;\n  oneof value {\n    GeneralUserSetting general = 3;\n    ShortcutsUserSetting shortcuts = 6;\n    WebhooksUserSetting webhooks = 7;\n    RefreshTokensUserSetting refresh_tokens = 8;\n    PersonalAccessTokensUserSetting personal_access_tokens = 9;\n  }\n}\n\nmessage GeneralUserSetting {\n  // The user's locale.\n  string locale = 1;\n  // The user's memo visibility setting.\n  string memo_visibility = 2;\n  // The user's theme preference.\n  // This references a CSS file in the web/public/themes/ directory.\n  string theme = 3;\n}\n\nmessage RefreshTokensUserSetting {\n  message RefreshToken {\n    // Unique identifier (matches 'tid' claim in JWT)\n    string token_id = 1;\n    // When the token expires\n    google.protobuf.Timestamp expires_at = 2;\n    // When the token was created\n    google.protobuf.Timestamp created_at = 3;\n    // Client information for session management UI\n    ClientInfo client_info = 4;\n    // Optional description\n    string description = 5;\n  }\n\n  message ClientInfo {\n    // User agent string of the client.\n    string user_agent = 1;\n    // IP address of the client.\n    string ip_address = 2;\n    // Optional. Device type (e.g., \"mobile\", \"desktop\", \"tablet\").\n    string device_type = 3;\n    // Optional. Operating system (e.g., \"iOS 17.0\", \"Windows 11\").\n    string os = 4;\n    // Optional. Browser name and version (e.g., \"Chrome 119.0\").\n    string browser = 5;\n  }\n\n  repeated RefreshToken refresh_tokens = 1;\n}\n\nmessage PersonalAccessTokensUserSetting {\n  message PersonalAccessToken {\n    // Unique identifier for this token\n    string token_id = 1;\n    // SHA-256 hash of the actual token\n    string token_hash = 2;\n    // User-provided description\n    string description = 3;\n    // When the token expires (null = never)\n    google.protobuf.Timestamp expires_at = 4;\n    // When the token was created\n    google.protobuf.Timestamp created_at = 5;\n    // When the token was last used\n    google.protobuf.Timestamp last_used_at = 6;\n  }\n  repeated PersonalAccessToken tokens = 1;\n}\n\nmessage ShortcutsUserSetting {\n  message Shortcut {\n    string id = 1;\n    string title = 2;\n    string filter = 3;\n  }\n  repeated Shortcut shortcuts = 1;\n}\n\nmessage WebhooksUserSetting {\n  message Webhook {\n    // Unique identifier for the webhook\n    string id = 1;\n    // Descriptive title for the webhook\n    string title = 2;\n    // The webhook URL endpoint\n    string url = 3;\n  }\n  repeated Webhook webhooks = 1;\n}\n"
  },
  {
    "path": "scripts/Dockerfile",
    "content": "FROM --platform=$BUILDPLATFORM golang:1.26.1-alpine AS backend\nWORKDIR /backend-build\n\n# Install build dependencies\nRUN apk add --no-cache git ca-certificates\n\n# Copy go mod files and download dependencies (cached layer)\nCOPY go.mod go.sum ./\nRUN --mount=type=cache,target=/go/pkg/mod \\\n    go mod download\n\n# Copy source code (use .dockerignore to exclude unnecessary files)\nCOPY . .\n\n# Please build frontend first, so that the static files are available.\n# Refer to `pnpm release` in package.json for the build command.\nARG TARGETOS TARGETARCH VERSION COMMIT\nRUN --mount=type=cache,target=/go/pkg/mod \\\n    --mount=type=cache,target=/root/.cache/go-build \\\n    CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH \\\n    go build \\\n      -trimpath \\\n      -ldflags=\"-s -w -extldflags '-static'\" \\\n      -tags netgo,osusergo \\\n      -o memos \\\n      ./cmd/memos\n\n# Use minimal Alpine with security updates\nFROM alpine:3.21 AS monolithic\n\n# Install runtime dependencies and create non-root user in single layer\nRUN apk add --no-cache tzdata ca-certificates su-exec && \\\n    addgroup -g 10001 -S nonroot && \\\n    adduser -u 10001 -S -G nonroot -h /var/opt/memos nonroot && \\\n    mkdir -p /var/opt/memos /usr/local/memos && \\\n    chown -R nonroot:nonroot /var/opt/memos\n\n# Copy binary and entrypoint to /usr/local/memos\nCOPY --from=backend /backend-build/memos /usr/local/memos/memos\nCOPY --from=backend --chmod=755 /backend-build/scripts/entrypoint.sh /usr/local/memos/entrypoint.sh\n\n# Run as root to fix permissions, entrypoint will drop to nonroot\nUSER root\n\n# Set working directory to the writable volume\nWORKDIR /var/opt/memos\n\n# Data directory\nVOLUME /var/opt/memos\n\nENV TZ=\"UTC\" \\\n    MEMOS_PORT=\"5230\"\n\nEXPOSE 5230\n\nENTRYPOINT [\"/usr/local/memos/entrypoint.sh\", \"/usr/local/memos/memos\"]\n"
  },
  {
    "path": "scripts/build.sh",
    "content": "#!/bin/sh\n\nset -e\n\n# Change to repo root\ncd \"$(dirname \"$0\")/../\"\n\nOS=$(uname -s)\n\n# Determine output binary name\ncase \"$OS\" in\n  *CYGWIN*|*MINGW*|*MSYS*)\n    OUTPUT=\"./build/memos.exe\"\n    ;;\n  *)\n    OUTPUT=\"./build/memos\"\n    ;;\nesac\n\necho \"Building for $OS...\"\n\n# Ensure build directories exist and configure a writable Go build cache\nmkdir -p ./build/.gocache ./build/.gomodcache\nexport GOCACHE=\"$(pwd)/build/.gocache\"\nexport GOMODCACHE=\"$(pwd)/build/.gomodcache\"\n\n# Build the executable\ngo build -o \"$OUTPUT\" ./cmd/memos\n\necho \"Build successful!\"\necho \"To run the application, execute the following command:\"\necho \"$OUTPUT\"\n"
  },
  {
    "path": "scripts/compose.yaml",
    "content": "services:\n  memos:\n    image: neosmemo/memos:stable\n    container_name: memos\n    volumes:\n      - ~/.memos/:/var/opt/memos\n    ports:\n      - 5230:5230\n"
  },
  {
    "path": "scripts/entrypoint.sh",
    "content": "#!/usr/bin/env sh\n\n# Fix ownership of data directory for users upgrading from older versions\n# where files were created as root\nMEMOS_UID=${MEMOS_UID:-10001}\nMEMOS_GID=${MEMOS_GID:-10001}\nDATA_DIR=\"/var/opt/memos\"\n\nif [ \"$(id -u)\" = \"0\" ]; then\n    # Running as root, fix permissions and drop to nonroot\n    if [ -d \"$DATA_DIR\" ]; then\n        chown -R \"$MEMOS_UID:$MEMOS_GID\" \"$DATA_DIR\" 2>/dev/null || true\n    fi\n    exec su-exec \"$MEMOS_UID:$MEMOS_GID\" \"$0\" \"$@\"\nfi\n\nfile_env() {\n   var=\"$1\"\n   fileVar=\"${var}_FILE\"\n\n   val_var=\"$(printenv \"$var\")\"\n   val_fileVar=\"$(printenv \"$fileVar\")\"\n\n   if [ -n \"$val_var\" ] && [ -n \"$val_fileVar\" ]; then\n      echo \"error: both $var and $fileVar are set (but are exclusive)\" >&2\n      exit 1\n   fi\n\n   if [ -n \"$val_var\" ]; then\n      val=\"$val_var\"\n   elif [ -n \"$val_fileVar\" ]; then\n      if [ ! -r \"$val_fileVar\" ]; then\n         echo \"error: file '$val_fileVar' does not exist or is not readable\" >&2\n         exit 1\n      fi\n      val=\"$(cat \"$val_fileVar\")\"\n   fi\n\n   export \"$var\"=\"$val\"\n   unset \"$fileVar\"\n}\n\nfile_env \"MEMOS_DSN\"\n\nexec \"$@\"\n"
  },
  {
    "path": "scripts/entrypoint_test.sh",
    "content": "#!/usr/bin/env sh\n\n# Test script for entrypoint.sh file_env function\n# Run: ./scripts/entrypoint_test.sh\n\nset -e\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"$0\")\" && pwd)\"\nTEMP_DIR=$(mktemp -d)\ntrap \"rm -rf $TEMP_DIR\" EXIT\n\n# Colors for output\nRED='\\033[0;31m'\nGREEN='\\033[0;32m'\nNC='\\033[0m' # No Color\n\npass_count=0\nfail_count=0\n\npass() {\n    echo \"${GREEN}PASS${NC}: $1\"\n    pass_count=$((pass_count + 1))\n}\n\nfail() {\n    echo \"${RED}FAIL${NC}: $1\"\n    fail_count=$((fail_count + 1))\n}\n\n# Test 1: Direct env var works\ntest_direct_env_var() {\n    unset MEMOS_DSN MEMOS_DSN_FILE\n    export MEMOS_DSN=\"direct_value\"\n\n    result=$(\"$SCRIPT_DIR/entrypoint.sh\" sh -c 'echo $MEMOS_DSN' 2>&1)\n    if [ \"$result\" = \"direct_value\" ]; then\n        pass \"Direct env var works\"\n    else\n        fail \"Direct env var: expected 'direct_value', got '$result'\"\n    fi\n    unset MEMOS_DSN\n}\n\n# Test 2: File env var works with readable file\ntest_file_env_var_readable() {\n    unset MEMOS_DSN MEMOS_DSN_FILE\n    echo \"file_value\" > \"$TEMP_DIR/dsn_file\"\n    export MEMOS_DSN_FILE=\"$TEMP_DIR/dsn_file\"\n\n    result=$(\"$SCRIPT_DIR/entrypoint.sh\" sh -c 'echo $MEMOS_DSN' 2>&1)\n    if [ \"$result\" = \"file_value\" ]; then\n        pass \"File env var with readable file works\"\n    else\n        fail \"File env var readable: expected 'file_value', got '$result'\"\n    fi\n    unset MEMOS_DSN_FILE\n}\n\n# Test 3: Error when file doesn't exist\ntest_file_env_var_missing() {\n    unset MEMOS_DSN MEMOS_DSN_FILE\n    export MEMOS_DSN_FILE=\"$TEMP_DIR/nonexistent_file\"\n\n    if result=$(\"$SCRIPT_DIR/entrypoint.sh\" sh -c 'echo $MEMOS_DSN' 2>&1); then\n        fail \"Missing file should fail, but succeeded with: $result\"\n    else\n        if echo \"$result\" | grep -q \"does not exist or is not readable\"; then\n            pass \"Missing file returns error\"\n        else\n            fail \"Missing file error message unexpected: $result\"\n        fi\n    fi\n    unset MEMOS_DSN_FILE\n}\n\n# Test 4: Error when file is not readable\ntest_file_env_var_unreadable() {\n    unset MEMOS_DSN MEMOS_DSN_FILE\n    echo \"secret\" > \"$TEMP_DIR/unreadable_file\"\n    chmod 000 \"$TEMP_DIR/unreadable_file\"\n    export MEMOS_DSN_FILE=\"$TEMP_DIR/unreadable_file\"\n\n    if result=$(\"$SCRIPT_DIR/entrypoint.sh\" sh -c 'echo $MEMOS_DSN' 2>&1); then\n        fail \"Unreadable file should fail, but succeeded with: $result\"\n    else\n        if echo \"$result\" | grep -q \"does not exist or is not readable\"; then\n            pass \"Unreadable file returns error\"\n        else\n            fail \"Unreadable file error message unexpected: $result\"\n        fi\n    fi\n    chmod 644 \"$TEMP_DIR/unreadable_file\" 2>/dev/null || true\n    unset MEMOS_DSN_FILE\n}\n\n# Test 5: Error when both var and file are set\ntest_both_set_error() {\n    unset MEMOS_DSN MEMOS_DSN_FILE\n    echo \"file_value\" > \"$TEMP_DIR/dsn_file\"\n    export MEMOS_DSN=\"direct_value\"\n    export MEMOS_DSN_FILE=\"$TEMP_DIR/dsn_file\"\n\n    if result=$(\"$SCRIPT_DIR/entrypoint.sh\" sh -c 'echo $MEMOS_DSN' 2>&1); then\n        fail \"Both set should fail, but succeeded with: $result\"\n    else\n        if echo \"$result\" | grep -q \"are set (but are exclusive)\"; then\n            pass \"Both var and file set returns error\"\n        else\n            fail \"Both set error message unexpected: $result\"\n        fi\n    fi\n    unset MEMOS_DSN MEMOS_DSN_FILE\n}\n\n# Run all tests\necho \"Running entrypoint.sh tests...\"\necho \"================================\"\n\ntest_direct_env_var\ntest_file_env_var_readable\ntest_file_env_var_missing\ntest_file_env_var_unreadable\ntest_both_set_error\n\necho \"================================\"\necho \"Tests completed: ${GREEN}$pass_count passed${NC}, ${RED}$fail_count failed${NC}\"\n\nif [ $fail_count -gt 0 ]; then\n    exit 1\nfi\nexit 0\n"
  },
  {
    "path": "scripts/install.sh",
    "content": "#!/bin/sh\n\nset -eu\n\nREPO=\"${REPO:-usememos/memos}\"\nBIN_NAME=\"memos\"\nVERSION=\"${MEMOS_VERSION:-}\"\nINSTALL_DIR=\"${MEMOS_INSTALL_DIR:-}\"\nSKIP_CHECKSUM=\"${MEMOS_SKIP_CHECKSUM:-0}\"\nQUIET=\"${MEMOS_INSTALL_QUIET:-0}\"\n\nusage() {\n  cat <<'EOF'\nInstall Memos from GitHub Releases.\n\nUsage:\n  install.sh [--version <version>] [--install-dir <dir>] [--repo <owner/name>] [--skip-checksum]\n\nEnvironment:\n  MEMOS_VERSION         Version to install. Accepts \"0.28.1\" or \"v0.28.1\". Defaults to latest release.\n  MEMOS_INSTALL_DIR     Directory to install the binary into.\n  MEMOS_SKIP_CHECKSUM   Set to 1 to skip checksum verification.\n  MEMOS_INSTALL_QUIET   Set to 1 to reduce log output.\n  REPO                  GitHub repository in owner/name form. Defaults to usememos/memos.\n\nExamples:\n  curl -fsSL https://raw.githubusercontent.com/usememos/memos/main/scripts/install.sh | sh\n  curl -fsSL https://raw.githubusercontent.com/usememos/memos/main/scripts/install.sh | sh -s -- --version 0.28.1\nEOF\n}\n\nlog() {\n  if [ \"$QUIET\" = \"1\" ]; then\n    return\n  fi\n  printf '%s\\n' \"$*\"\n}\n\nfail() {\n  printf 'Error: %s\\n' \"$*\" >&2\n  exit 1\n}\n\nneed_cmd() {\n  command -v \"$1\" >/dev/null 2>&1 || fail \"required command not found: $1\"\n}\n\nresolve_latest_version() {\n  latest_tag=\"$(\n    curl -fsSL \\\n      -H \"Accept: application/vnd.github+json\" \\\n      \"https://api.github.com/repos/${REPO}/releases/latest\" | awk -F'\"' '/\"tag_name\":/ { print $4; exit }'\n  )\"\n  [ -n \"$latest_tag\" ] || fail \"failed to resolve latest release tag\"\n  printf '%s\\n' \"${latest_tag#v}\"\n}\n\nnormalize_version() {\n  version=\"$1\"\n  version=\"${version#v}\"\n  [ -n \"$version\" ] || fail \"version cannot be empty\"\n  printf '%s\\n' \"$version\"\n}\n\ndetect_os() {\n  os=\"$(uname -s | tr '[:upper:]' '[:lower:]')\"\n  case \"$os\" in\n    linux)\n      printf 'linux\\n'\n      ;;\n    darwin)\n      printf 'darwin\\n'\n      ;;\n    *)\n      fail \"unsupported operating system: $os\"\n      ;;\n  esac\n}\n\ndetect_arch() {\n  arch=\"$(uname -m)\"\n  case \"$arch\" in\n    x86_64|amd64)\n      printf 'amd64\\n'\n      ;;\n    arm64|aarch64)\n      printf 'arm64\\n'\n      ;;\n    armv7l|armv7)\n      printf 'armv7\\n'\n      ;;\n    *)\n      fail \"unsupported architecture: $arch\"\n      ;;\n  esac\n}\n\nresolve_install_dir() {\n  if [ -n \"$INSTALL_DIR\" ]; then\n    printf '%s\\n' \"$INSTALL_DIR\"\n    return\n  fi\n\n  if [ -w \"/usr/local/bin\" ]; then\n    printf '/usr/local/bin\\n'\n    return\n  fi\n\n  if command -v sudo >/dev/null 2>&1; then\n    printf '/usr/local/bin\\n'\n    return\n  fi\n\n  printf '%s/.local/bin\\n' \"$HOME\"\n}\n\ndownload() {\n  src=\"$1\"\n  dest=\"$2\"\n  if ! curl -fsSL \"$src\" -o \"$dest\"; then\n    fail \"failed to download ${src}\"\n  fi\n}\n\ndownload_optional() {\n  src=\"$1\"\n  dest=\"$2\"\n\n  if curl -fsSL \"$src\" -o \"$dest\" 2>/dev/null; then\n    return 0\n  fi\n\n  rm -f \"$dest\"\n  return 1\n}\n\nverify_checksum() {\n  archive_path=\"$1\"\n  checksum_path=\"$2\"\n\n  if [ \"$SKIP_CHECKSUM\" = \"1\" ]; then\n    log \"Skipping checksum verification\"\n    return\n  fi\n\n  if [ ! -f \"$checksum_path\" ]; then\n    log \"Warning: checksum file not found for this release; skipping verification\"\n    return\n  fi\n\n  archive_name=\"$(basename \"$archive_path\")\"\n  expected_line=\"$(grep \"  ${archive_name}\\$\" \"$checksum_path\" || true)\"\n  [ -n \"$expected_line\" ] || fail \"checksum entry not found for ${archive_name}\"\n\n  if command -v sha256sum >/dev/null 2>&1; then\n    (\n      cd \"$(dirname \"$archive_path\")\"\n      printf '%s\\n' \"$expected_line\" | sha256sum -c -\n    )\n    return\n  fi\n\n  if command -v shasum >/dev/null 2>&1; then\n    expected_sum=\"$(printf '%s' \"$expected_line\" | awk '{print $1}')\"\n    actual_sum=\"$(shasum -a 256 \"$archive_path\" | awk '{print $1}')\"\n    [ \"$expected_sum\" = \"$actual_sum\" ] || fail \"checksum verification failed for ${archive_name}\"\n    return\n  fi\n\n  log \"Warning: sha256sum/shasum not found; skipping checksum verification\"\n}\n\nextract_archive() {\n  archive_path=\"$1\"\n  dest_dir=\"$2\"\n\n  tar -xzf \"$archive_path\" -C \"$dest_dir\"\n}\n\ninstall_binary() {\n  src=\"$1\"\n  dest_dir=\"$2\"\n\n  mkdir -p \"$dest_dir\"\n\n  if [ -w \"$dest_dir\" ]; then\n    install -m 755 \"$src\" \"${dest_dir}/${BIN_NAME}\"\n    return\n  fi\n\n  if command -v sudo >/dev/null 2>&1; then\n    sudo mkdir -p \"$dest_dir\"\n    sudo install -m 755 \"$src\" \"${dest_dir}/${BIN_NAME}\"\n    return\n  fi\n\n  fail \"install directory is not writable: $dest_dir\"\n}\n\nparse_args() {\n  while [ \"$#\" -gt 0 ]; do\n    case \"$1\" in\n      --version)\n        [ \"$#\" -ge 2 ] || fail \"missing value for --version\"\n        VERSION=\"$2\"\n        shift 2\n        ;;\n      --install-dir)\n        [ \"$#\" -ge 2 ] || fail \"missing value for --install-dir\"\n        INSTALL_DIR=\"$2\"\n        shift 2\n        ;;\n      --repo)\n        [ \"$#\" -ge 2 ] || fail \"missing value for --repo\"\n        REPO=\"$2\"\n        shift 2\n        ;;\n      --skip-checksum)\n        SKIP_CHECKSUM=\"1\"\n        shift\n        ;;\n      --quiet)\n        QUIET=\"1\"\n        shift\n        ;;\n      -h|--help)\n        usage\n        exit 0\n        ;;\n      *)\n        fail \"unknown argument: $1\"\n        ;;\n    esac\n  done\n}\n\nmain() {\n  parse_args \"$@\"\n\n  need_cmd curl\n  need_cmd tar\n  need_cmd install\n  need_cmd uname\n  need_cmd grep\n  need_cmd awk\n  need_cmd mktemp\n\n  os=\"$(detect_os)\"\n  arch=\"$(detect_arch)\"\n\n  if [ -z \"$VERSION\" ]; then\n    VERSION=\"$(resolve_latest_version)\"\n  fi\n  VERSION=\"$(normalize_version \"$VERSION\")\"\n\n  install_dir=\"$(resolve_install_dir)\"\n  tag=\"v${VERSION}\"\n\n  asset_suffix=\"${arch}\"\n  if [ \"$arch\" = \"armv7\" ]; then\n    asset_suffix=\"armv7\"\n  fi\n\n  asset_name=\"${BIN_NAME}_${VERSION}_${os}_${asset_suffix}.tar.gz\"\n  checksums_name=\"checksums.txt\"\n  base_url=\"https://github.com/${REPO}/releases/download/${tag}\"\n\n  tmpdir=\"$(mktemp -d)\"\n  trap 'rm -rf \"$tmpdir\"' EXIT INT TERM\n\n  archive_path=\"${tmpdir}/${asset_name}\"\n  checksums_path=\"${tmpdir}/${checksums_name}\"\n  extract_dir=\"${tmpdir}/extract\"\n\n  mkdir -p \"$extract_dir\"\n\n  log \"Installing ${BIN_NAME} ${VERSION} for ${os}/${arch}\"\n  log \"Downloading ${asset_name} from ${REPO}\"\n  download \"${base_url}/${asset_name}\" \"$archive_path\"\n  if ! download_optional \"${base_url}/${checksums_name}\" \"$checksums_path\"; then\n    log \"Warning: ${checksums_name} is not published for ${tag}\"\n  fi\n\n  verify_checksum \"$archive_path\" \"$checksums_path\"\n  extract_archive \"$archive_path\" \"$extract_dir\"\n\n  [ -f \"${extract_dir}/${BIN_NAME}\" ] || fail \"archive did not contain ${BIN_NAME}\"\n  install_binary \"${extract_dir}/${BIN_NAME}\" \"$install_dir\"\n\n  log \"Installed ${BIN_NAME} to ${install_dir}/${BIN_NAME}\"\n  if ! printf '%s' \":$PATH:\" | grep -q \":${install_dir}:\"; then\n    log \"Add ${install_dir} to your PATH to run ${BIN_NAME} directly\"\n  fi\n}\n\nmain \"$@\"\n"
  },
  {
    "path": "server/auth/authenticator.go",
    "content": "package auth\n\nimport (\n\t\"context\"\n\t\"log/slog\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/pkg/errors\"\n\t\"google.golang.org/protobuf/types/known/timestamppb\"\n\n\t\"github.com/usememos/memos/internal/util\"\n\tstorepb \"github.com/usememos/memos/proto/gen/store\"\n\t\"github.com/usememos/memos/store\"\n)\n\n// Authenticator provides shared authentication and authorization logic.\n// Used by gRPC interceptor, Connect interceptor, and file server to ensure\n// consistent authentication behavior across all API endpoints.\n//\n// Authentication methods:\n// - JWT access tokens: Short-lived tokens (15 minutes) for API access\n// - Personal Access Tokens (PAT): Long-lived tokens for programmatic access\n//\n// This struct is safe for concurrent use.\ntype Authenticator struct {\n\tstore  *store.Store\n\tsecret string\n}\n\n// NewAuthenticator creates a new Authenticator instance.\nfunc NewAuthenticator(store *store.Store, secret string) *Authenticator {\n\treturn &Authenticator{\n\t\tstore:  store,\n\t\tsecret: secret,\n\t}\n}\n\n// AuthenticateByAccessTokenV2 validates a short-lived access token.\n// Returns claims without database query (stateless validation).\nfunc (a *Authenticator) AuthenticateByAccessTokenV2(accessToken string) (*UserClaims, error) {\n\tclaims, err := ParseAccessTokenV2(accessToken, []byte(a.secret))\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"invalid access token\")\n\t}\n\n\tuserID, err := util.ConvertStringToInt32(claims.Subject)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"invalid user ID in token\")\n\t}\n\n\treturn &UserClaims{\n\t\tUserID:   userID,\n\t\tUsername: claims.Username,\n\t\tRole:     claims.Role,\n\t\tStatus:   claims.Status,\n\t}, nil\n}\n\n// AuthenticateByRefreshToken validates a refresh token against the database.\nfunc (a *Authenticator) AuthenticateByRefreshToken(ctx context.Context, refreshToken string) (*store.User, string, error) {\n\tclaims, err := ParseRefreshToken(refreshToken, []byte(a.secret))\n\tif err != nil {\n\t\treturn nil, \"\", errors.Wrap(err, \"invalid refresh token\")\n\t}\n\n\tuserID, err := util.ConvertStringToInt32(claims.Subject)\n\tif err != nil {\n\t\treturn nil, \"\", errors.Wrap(err, \"invalid user ID in token\")\n\t}\n\n\t// Check token exists in database (revocation check)\n\ttoken, err := a.store.GetUserRefreshTokenByID(ctx, userID, claims.TokenID)\n\tif err != nil {\n\t\treturn nil, \"\", errors.Wrap(err, \"failed to get refresh token\")\n\t}\n\tif token == nil {\n\t\treturn nil, \"\", errors.New(\"refresh token revoked\")\n\t}\n\n\t// Check token not expired\n\tif token.ExpiresAt != nil && token.ExpiresAt.AsTime().Before(time.Now()) {\n\t\treturn nil, \"\", errors.New(\"refresh token expired\")\n\t}\n\n\t// Get user\n\tuser, err := a.store.GetUser(ctx, &store.FindUser{ID: &userID})\n\tif err != nil {\n\t\treturn nil, \"\", errors.Wrap(err, \"failed to get user\")\n\t}\n\tif user == nil {\n\t\treturn nil, \"\", errors.New(\"user not found\")\n\t}\n\tif user.RowStatus == store.Archived {\n\t\treturn nil, \"\", errors.New(\"user is archived\")\n\t}\n\n\treturn user, claims.TokenID, nil\n}\n\n// AuthenticateByPAT validates a Personal Access Token.\nfunc (a *Authenticator) AuthenticateByPAT(ctx context.Context, token string) (*store.User, *storepb.PersonalAccessTokensUserSetting_PersonalAccessToken, error) {\n\tif !strings.HasPrefix(token, PersonalAccessTokenPrefix) {\n\t\treturn nil, nil, errors.New(\"invalid PAT format\")\n\t}\n\n\ttokenHash := HashPersonalAccessToken(token)\n\tresult, err := a.store.GetUserByPATHash(ctx, tokenHash)\n\tif err != nil {\n\t\treturn nil, nil, errors.Wrap(err, \"invalid PAT\")\n\t}\n\n\t// Check expiry\n\tif result.PAT.ExpiresAt != nil && result.PAT.ExpiresAt.AsTime().Before(time.Now()) {\n\t\treturn nil, nil, errors.New(\"PAT expired\")\n\t}\n\n\t// Check user status\n\tif result.User.RowStatus == store.Archived {\n\t\treturn nil, nil, errors.New(\"user is archived\")\n\t}\n\n\treturn result.User, result.PAT, nil\n}\n\n// AuthResult contains the result of an authentication attempt.\ntype AuthResult struct {\n\tUser        *store.User // Set for PAT authentication\n\tClaims      *UserClaims // Set for Access Token V2 (stateless)\n\tAccessToken string      // Non-empty if authenticated via JWT\n}\n\n// AuthenticateToUser resolves the current request to a *store.User, checking the\n// Authorization header first (access token or PAT), then falling back to the\n// refresh token cookie. Returns (nil, nil) when no credentials are present.\nfunc (a *Authenticator) AuthenticateToUser(ctx context.Context, authHeader, cookieHeader string) (*store.User, error) {\n\t// Try Bearer token first.\n\tif authHeader != \"\" {\n\t\ttoken := ExtractBearerToken(authHeader)\n\t\tif token != \"\" {\n\t\t\tif !strings.HasPrefix(token, PersonalAccessTokenPrefix) {\n\t\t\t\tclaims, err := a.AuthenticateByAccessTokenV2(token)\n\t\t\t\tif err == nil && claims != nil {\n\t\t\t\t\treturn a.store.GetUser(ctx, &store.FindUser{ID: &claims.UserID})\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tuser, _, err := a.AuthenticateByPAT(ctx, token)\n\t\t\t\tif err == nil {\n\t\t\t\t\treturn user, nil\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// Fallback: refresh token cookie.\n\tif cookieHeader != \"\" {\n\t\trefreshToken := ExtractRefreshTokenFromCookie(cookieHeader)\n\t\tif refreshToken != \"\" {\n\t\t\tuser, _, err := a.AuthenticateByRefreshToken(ctx, refreshToken)\n\t\t\treturn user, err\n\t\t}\n\t}\n\n\treturn nil, nil\n}\n\n// Authenticate tries to authenticate using the provided credentials.\n// Priority: 1. Access Token V2, 2. PAT\n// Returns nil if no valid credentials are provided.\nfunc (a *Authenticator) Authenticate(ctx context.Context, authHeader string) *AuthResult {\n\ttoken := ExtractBearerToken(authHeader)\n\n\t// Try Access Token V2 (stateless)\n\tif token != \"\" && !strings.HasPrefix(token, PersonalAccessTokenPrefix) {\n\t\tclaims, err := a.AuthenticateByAccessTokenV2(token)\n\t\tif err == nil && claims != nil {\n\t\t\treturn &AuthResult{\n\t\t\t\tClaims:      claims,\n\t\t\t\tAccessToken: token,\n\t\t\t}\n\t\t}\n\t}\n\n\t// Try PAT\n\tif token != \"\" && strings.HasPrefix(token, PersonalAccessTokenPrefix) {\n\t\tuser, pat, err := a.AuthenticateByPAT(ctx, token)\n\t\tif err == nil && user != nil {\n\t\t\t// Update last used (fire-and-forget with logging)\n\t\t\tgo func() {\n\t\t\t\tif err := a.store.UpdatePATLastUsed(context.Background(), user.ID, pat.TokenId, timestamppb.Now()); err != nil {\n\t\t\t\t\tslog.Warn(\"failed to update PAT last used time\", \"error\", err, \"userID\", user.ID)\n\t\t\t\t}\n\t\t\t}()\n\t\t\treturn &AuthResult{User: user, AccessToken: token}\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "server/auth/context.go",
    "content": "package auth\n\nimport (\n\t\"context\"\n\n\t\"github.com/usememos/memos/store\"\n)\n\n// ContextKey is the key type for context values.\n// Using a custom type prevents collisions with other packages.\ntype ContextKey int\n\nconst (\n\t// UserIDContextKey stores the authenticated user's ID.\n\t// Set for all authenticated requests.\n\t// Use GetUserID(ctx) to retrieve this value.\n\tUserIDContextKey ContextKey = iota\n\n\t// AccessTokenContextKey stores the JWT token for token-based auth.\n\t// Only set when authenticated via Bearer token.\n\tAccessTokenContextKey\n\n\t// UserClaimsContextKey stores the claims from access token.\n\tUserClaimsContextKey\n\n\t// RefreshTokenIDContextKey stores the refresh token ID.\n\tRefreshTokenIDContextKey\n)\n\n// GetUserID retrieves the authenticated user's ID from the context.\n// Returns 0 if no user ID is set (unauthenticated request).\nfunc GetUserID(ctx context.Context) int32 {\n\tif v, ok := ctx.Value(UserIDContextKey).(int32); ok {\n\t\treturn v\n\t}\n\treturn 0\n}\n\n// GetAccessToken retrieves the JWT access token from the context.\n// Returns empty string if not authenticated via bearer token.\nfunc GetAccessToken(ctx context.Context) string {\n\tif v, ok := ctx.Value(AccessTokenContextKey).(string); ok {\n\t\treturn v\n\t}\n\treturn \"\"\n}\n\n// SetUserInContext sets the authenticated user's information in the context.\n// This is a simpler alternative to AuthorizeAndSetContext for cases where\n// authorization is handled separately (e.g., HTTP middleware).\n//\n// Parameters:\n//   - user: The authenticated user\n//   - accessToken: Set if authenticated via JWT token (empty string otherwise)\nfunc SetUserInContext(ctx context.Context, user *store.User, accessToken string) context.Context {\n\tctx = context.WithValue(ctx, UserIDContextKey, user.ID)\n\tif accessToken != \"\" {\n\t\tctx = context.WithValue(ctx, AccessTokenContextKey, accessToken)\n\t}\n\treturn ctx\n}\n\n// UserClaims represents authenticated user info from access token.\ntype UserClaims struct {\n\tUserID   int32\n\tUsername string\n\tRole     string\n\tStatus   string\n}\n\n// GetUserClaims retrieves the user claims from context.\n// Returns nil if not authenticated via access token.\nfunc GetUserClaims(ctx context.Context) *UserClaims {\n\tif v, ok := ctx.Value(UserClaimsContextKey).(*UserClaims); ok {\n\t\treturn v\n\t}\n\treturn nil\n}\n\n// SetUserClaimsInContext sets the user claims in context.\nfunc SetUserClaimsInContext(ctx context.Context, claims *UserClaims) context.Context {\n\treturn context.WithValue(ctx, UserClaimsContextKey, claims)\n}\n\n// ApplyToContext sets the authenticated identity from an AuthResult into the context.\n// This is the canonical way to propagate auth state after a successful Authenticate call.\n// Safe to call with a nil result (no-op).\nfunc ApplyToContext(ctx context.Context, result *AuthResult) context.Context {\n\tif result == nil {\n\t\treturn ctx\n\t}\n\tif result.Claims != nil {\n\t\tctx = SetUserClaimsInContext(ctx, result.Claims)\n\t\tctx = context.WithValue(ctx, UserIDContextKey, result.Claims.UserID)\n\t} else if result.User != nil {\n\t\tctx = SetUserInContext(ctx, result.User, result.AccessToken)\n\t}\n\treturn ctx\n}\n"
  },
  {
    "path": "server/auth/extract.go",
    "content": "package auth\n\nimport (\n\t\"net/http\"\n\t\"strings\"\n)\n\n// ExtractBearerToken extracts the JWT token from an Authorization header value.\n// Expected format: \"Bearer {token}\"\n// Returns empty string if no valid bearer token is found.\nfunc ExtractBearerToken(authHeader string) string {\n\tif authHeader == \"\" {\n\t\treturn \"\"\n\t}\n\tparts := strings.Fields(authHeader)\n\tif len(parts) != 2 || !strings.EqualFold(parts[0], \"bearer\") {\n\t\treturn \"\"\n\t}\n\treturn parts[1]\n}\n\n// ExtractRefreshTokenFromCookie extracts the refresh token from cookie header.\nfunc ExtractRefreshTokenFromCookie(cookieHeader string) string {\n\tif cookieHeader == \"\" {\n\t\treturn \"\"\n\t}\n\treq := &http.Request{Header: http.Header{\"Cookie\": []string{cookieHeader}}}\n\tcookie, err := req.Cookie(RefreshTokenCookieName)\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\treturn cookie.Value\n}\n"
  },
  {
    "path": "server/auth/token.go",
    "content": "// Package auth provides authentication and authorization for the Memos server.\n//\n// This package is used by:\n// - server/router/api/v1: gRPC and Connect API interceptors\n// - server/router/fileserver: HTTP file server authentication\n//\n// Authentication methods supported:\n// - JWT access tokens: Short-lived tokens (15 minutes) for API access\n// - JWT refresh tokens: Long-lived tokens (30 days) for obtaining new access tokens\n// - Personal Access Tokens (PAT): Long-lived tokens for programmatic access\npackage auth\n\nimport (\n\t\"crypto/sha256\"\n\t\"encoding/hex\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/golang-jwt/jwt/v5\"\n\t\"github.com/pkg/errors\"\n\n\t\"github.com/usememos/memos/internal/util\"\n)\n\nconst (\n\t// Issuer is the issuer claim in JWT tokens.\n\t// This identifies tokens as issued by Memos.\n\tIssuer = \"memos\"\n\n\t// KeyID is the key identifier used in JWT header.\n\t// Version \"v1\" allows for future key rotation while maintaining backward compatibility.\n\t// If signing mechanism changes, add \"v2\", \"v3\", etc. and verify both versions.\n\tKeyID = \"v1\"\n\n\t// AccessTokenAudienceName is the audience claim for JWT access tokens.\n\t// This ensures tokens are only used for API access, not other purposes.\n\tAccessTokenAudienceName = \"user.access-token\"\n\n\t// AccessTokenDuration is the lifetime of access tokens (15 minutes).\n\tAccessTokenDuration = 15 * time.Minute\n\n\t// RefreshTokenDuration is the lifetime of refresh tokens (30 days).\n\tRefreshTokenDuration = 30 * 24 * time.Hour\n\n\t// RefreshTokenAudienceName is the audience claim for refresh tokens.\n\tRefreshTokenAudienceName = \"user.refresh-token\"\n\n\t// RefreshTokenCookieName is the cookie name for refresh tokens.\n\tRefreshTokenCookieName = \"memos_refresh\"\n\n\t// PersonalAccessTokenPrefix is the prefix for PAT tokens.\n\tPersonalAccessTokenPrefix = \"memos_pat_\"\n)\n\n// ClaimsMessage represents the claims structure in a JWT token.\n//\n// JWT Claims include:\n// - name: Username (custom claim)\n// - iss: Issuer = \"memos\"\n// - aud: Audience = \"user.access-token\"\n// - sub: Subject = user ID\n// - iat: Issued at time\n// - exp: Expiration time (optional, may be empty for never-expiring tokens).\ntype ClaimsMessage struct {\n\tName string `json:\"name\"` // Username\n\tjwt.RegisteredClaims\n}\n\n// AccessTokenClaims contains claims for short-lived access tokens.\n// These tokens are validated by signature only (stateless).\ntype AccessTokenClaims struct {\n\tType     string `json:\"type\"`     // \"access\"\n\tRole     string `json:\"role\"`     // User role\n\tStatus   string `json:\"status\"`   // User status\n\tUsername string `json:\"username\"` // Username for display\n\tjwt.RegisteredClaims\n}\n\n// RefreshTokenClaims contains claims for long-lived refresh tokens.\n// These tokens are validated against the database for revocation.\ntype RefreshTokenClaims struct {\n\tType    string `json:\"type\"` // \"refresh\"\n\tTokenID string `json:\"tid\"`  // Token ID for revocation lookup\n\tjwt.RegisteredClaims\n}\n\n// GenerateAccessToken generates a JWT access token for a user.\n//\n// Parameters:\n// - username: The user's username (stored in \"name\" claim)\n// - userID: The user's ID (stored in \"sub\" claim)\n// - expirationTime: When the token expires (pass zero time for no expiration)\n// - secret: Server secret used to sign the token\n//\n// Returns a signed JWT string or an error.\nfunc GenerateAccessToken(username string, userID int32, expirationTime time.Time, secret []byte) (string, error) {\n\treturn generateToken(username, userID, AccessTokenAudienceName, expirationTime, secret)\n}\n\n// generateToken generates a JWT token with the given claims.\n//\n// Token structure:\n// Header: {\"alg\": \"HS256\", \"kid\": \"v1\", \"typ\": \"JWT\"}\n// Claims: {\"name\": username, \"iss\": \"memos\", \"aud\": [audience], \"sub\": userID, \"iat\": now, \"exp\": expiry}\n// Signature: HMACSHA256(base64UrlEncode(header) + \".\" + base64UrlEncode(payload), secret).\nfunc generateToken(username string, userID int32, audience string, expirationTime time.Time, secret []byte) (string, error) {\n\tregisteredClaims := jwt.RegisteredClaims{\n\t\tIssuer:   Issuer,\n\t\tAudience: jwt.ClaimStrings{audience},\n\t\tIssuedAt: jwt.NewNumericDate(time.Now()),\n\t\tSubject:  fmt.Sprint(userID),\n\t}\n\tif !expirationTime.IsZero() {\n\t\tregisteredClaims.ExpiresAt = jwt.NewNumericDate(expirationTime)\n\t}\n\n\t// Declare the token with the HS256 algorithm used for signing, and the claims.\n\ttoken := jwt.NewWithClaims(jwt.SigningMethodHS256, &ClaimsMessage{\n\t\tName:             username,\n\t\tRegisteredClaims: registeredClaims,\n\t})\n\ttoken.Header[\"kid\"] = KeyID\n\n\t// Create the JWT string.\n\ttokenString, err := token.SignedString(secret)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn tokenString, nil\n}\n\n// GenerateAccessTokenV2 generates a short-lived access token with user claims.\nfunc GenerateAccessTokenV2(userID int32, username, role, status string, secret []byte) (string, time.Time, error) {\n\texpiresAt := time.Now().Add(AccessTokenDuration)\n\n\tclaims := &AccessTokenClaims{\n\t\tType:     \"access\",\n\t\tRole:     role,\n\t\tStatus:   status,\n\t\tUsername: username,\n\t\tRegisteredClaims: jwt.RegisteredClaims{\n\t\t\tIssuer:    Issuer,\n\t\t\tAudience:  jwt.ClaimStrings{AccessTokenAudienceName},\n\t\t\tSubject:   fmt.Sprint(userID),\n\t\t\tIssuedAt:  jwt.NewNumericDate(time.Now()),\n\t\t\tExpiresAt: jwt.NewNumericDate(expiresAt),\n\t\t},\n\t}\n\n\ttoken := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)\n\ttoken.Header[\"kid\"] = KeyID\n\n\ttokenString, err := token.SignedString(secret)\n\tif err != nil {\n\t\treturn \"\", time.Time{}, err\n\t}\n\n\treturn tokenString, expiresAt, nil\n}\n\n// GenerateRefreshToken generates a long-lived refresh token.\nfunc GenerateRefreshToken(userID int32, tokenID string, secret []byte) (string, time.Time, error) {\n\texpiresAt := time.Now().Add(RefreshTokenDuration)\n\n\tclaims := &RefreshTokenClaims{\n\t\tType:    \"refresh\",\n\t\tTokenID: tokenID,\n\t\tRegisteredClaims: jwt.RegisteredClaims{\n\t\t\tIssuer:    Issuer,\n\t\t\tAudience:  jwt.ClaimStrings{RefreshTokenAudienceName},\n\t\t\tSubject:   fmt.Sprint(userID),\n\t\t\tIssuedAt:  jwt.NewNumericDate(time.Now()),\n\t\t\tExpiresAt: jwt.NewNumericDate(expiresAt),\n\t\t},\n\t}\n\n\ttoken := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)\n\ttoken.Header[\"kid\"] = KeyID\n\n\ttokenString, err := token.SignedString(secret)\n\tif err != nil {\n\t\treturn \"\", time.Time{}, err\n\t}\n\n\treturn tokenString, expiresAt, nil\n}\n\n// GeneratePersonalAccessToken generates a random PAT string.\nfunc GeneratePersonalAccessToken() string {\n\trandomStr, err := util.RandomString(32)\n\tif err != nil {\n\t\t// Fallback to UUID if RandomString fails\n\t\treturn PersonalAccessTokenPrefix + util.GenUUID()\n\t}\n\treturn PersonalAccessTokenPrefix + randomStr\n}\n\n// HashPersonalAccessToken returns SHA-256 hash of a PAT.\nfunc HashPersonalAccessToken(token string) string {\n\thash := sha256.Sum256([]byte(token))\n\treturn hex.EncodeToString(hash[:])\n}\n\n// verifyJWTKeyFunc returns a jwt.Keyfunc that validates the signing method and key ID.\nfunc verifyJWTKeyFunc(secret []byte) jwt.Keyfunc {\n\treturn func(t *jwt.Token) (any, error) {\n\t\tif t.Method.Alg() != jwt.SigningMethodHS256.Name {\n\t\t\treturn nil, errors.Errorf(\"unexpected signing method: %v\", t.Header[\"alg\"])\n\t\t}\n\t\tkid, ok := t.Header[\"kid\"].(string)\n\t\tif !ok || kid != KeyID {\n\t\t\treturn nil, errors.Errorf(\"unexpected kid: %v\", t.Header[\"kid\"])\n\t\t}\n\t\treturn secret, nil\n\t}\n}\n\n// ParseAccessTokenV2 parses and validates a short-lived access token.\nfunc ParseAccessTokenV2(tokenString string, secret []byte) (*AccessTokenClaims, error) {\n\tclaims := &AccessTokenClaims{}\n\t_, err := jwt.ParseWithClaims(tokenString, claims, verifyJWTKeyFunc(secret),\n\t\tjwt.WithIssuer(Issuer),\n\t\tjwt.WithAudience(AccessTokenAudienceName),\n\t)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif claims.Type != \"access\" {\n\t\treturn nil, errors.New(\"invalid token type: expected access token\")\n\t}\n\treturn claims, nil\n}\n\n// ParseRefreshToken parses and validates a refresh token.\nfunc ParseRefreshToken(tokenString string, secret []byte) (*RefreshTokenClaims, error) {\n\tclaims := &RefreshTokenClaims{}\n\t_, err := jwt.ParseWithClaims(tokenString, claims, verifyJWTKeyFunc(secret),\n\t\tjwt.WithIssuer(Issuer),\n\t\tjwt.WithAudience(RefreshTokenAudienceName),\n\t)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif claims.Type != \"refresh\" {\n\t\treturn nil, errors.New(\"invalid token type: expected refresh token\")\n\t}\n\treturn claims, nil\n}\n"
  },
  {
    "path": "server/auth/token_test.go",
    "content": "package auth\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestGenerateAccessTokenV2(t *testing.T) {\n\tsecret := []byte(\"test-secret\")\n\n\tt.Run(\"generates valid access token\", func(t *testing.T) {\n\t\ttoken, expiresAt, err := GenerateAccessTokenV2(1, \"testuser\", \"USER\", \"ACTIVE\", secret)\n\t\trequire.NoError(t, err)\n\t\tassert.NotEmpty(t, token)\n\t\tassert.True(t, expiresAt.After(time.Now()))\n\t\tassert.True(t, expiresAt.Before(time.Now().Add(AccessTokenDuration+time.Minute)))\n\t})\n\n\tt.Run(\"generates different tokens for same user\", func(t *testing.T) {\n\t\ttoken1, _, err := GenerateAccessTokenV2(1, \"testuser\", \"USER\", \"ACTIVE\", secret)\n\t\trequire.NoError(t, err)\n\n\t\ttime.Sleep(2 * time.Second) // Ensure different timestamps (tokens have 1s precision)\n\n\t\ttoken2, _, err := GenerateAccessTokenV2(1, \"testuser\", \"USER\", \"ACTIVE\", secret)\n\t\trequire.NoError(t, err)\n\n\t\tassert.NotEqual(t, token1, token2, \"tokens should be different due to different timestamps\")\n\t})\n}\n\nfunc TestParseAccessTokenV2(t *testing.T) {\n\tsecret := []byte(\"test-secret\")\n\n\tt.Run(\"parses valid access token\", func(t *testing.T) {\n\t\ttoken, _, err := GenerateAccessTokenV2(1, \"testuser\", \"USER\", \"ACTIVE\", secret)\n\t\trequire.NoError(t, err)\n\n\t\tclaims, err := ParseAccessTokenV2(token, secret)\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, \"1\", claims.Subject)\n\t\tassert.Equal(t, \"testuser\", claims.Username)\n\t\tassert.Equal(t, \"USER\", claims.Role)\n\t\tassert.Equal(t, \"ACTIVE\", claims.Status)\n\t\tassert.Equal(t, \"access\", claims.Type)\n\t})\n\n\tt.Run(\"fails with wrong secret\", func(t *testing.T) {\n\t\ttoken, _, err := GenerateAccessTokenV2(1, \"testuser\", \"USER\", \"ACTIVE\", secret)\n\t\trequire.NoError(t, err)\n\n\t\twrongSecret := []byte(\"wrong-secret\")\n\t\t_, err = ParseAccessTokenV2(token, wrongSecret)\n\t\tassert.Error(t, err)\n\t})\n\n\tt.Run(\"fails with invalid token\", func(t *testing.T) {\n\t\t_, err := ParseAccessTokenV2(\"invalid-token\", secret)\n\t\tassert.Error(t, err)\n\t})\n\n\tt.Run(\"fails with refresh token\", func(t *testing.T) {\n\t\t// Generate a refresh token and try to parse it as access token\n\t\t// Should fail because audience mismatch is caught before type check\n\t\trefreshToken, _, err := GenerateRefreshToken(1, \"token-id\", secret)\n\t\trequire.NoError(t, err)\n\n\t\t_, err = ParseAccessTokenV2(refreshToken, secret)\n\t\tassert.Error(t, err)\n\t\tassert.Contains(t, err.Error(), \"invalid audience\")\n\t})\n\n\tt.Run(\"parses token with different roles\", func(t *testing.T) {\n\t\troles := []string{\"USER\", \"ADMIN\"}\n\t\tfor _, role := range roles {\n\t\t\ttoken, _, err := GenerateAccessTokenV2(1, \"testuser\", role, \"ACTIVE\", secret)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tclaims, err := ParseAccessTokenV2(token, secret)\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, role, claims.Role)\n\t\t}\n\t})\n}\n\nfunc TestGenerateRefreshToken(t *testing.T) {\n\tsecret := []byte(\"test-secret\")\n\n\tt.Run(\"generates valid refresh token\", func(t *testing.T) {\n\t\ttoken, expiresAt, err := GenerateRefreshToken(1, \"token-id-123\", secret)\n\t\trequire.NoError(t, err)\n\t\tassert.NotEmpty(t, token)\n\t\tassert.True(t, expiresAt.After(time.Now().Add(29*24*time.Hour)))\n\t})\n\n\tt.Run(\"generates different tokens for different token IDs\", func(t *testing.T) {\n\t\ttoken1, _, err := GenerateRefreshToken(1, \"token-id-1\", secret)\n\t\trequire.NoError(t, err)\n\n\t\ttoken2, _, err := GenerateRefreshToken(1, \"token-id-2\", secret)\n\t\trequire.NoError(t, err)\n\n\t\tassert.NotEqual(t, token1, token2)\n\t})\n}\n\nfunc TestParseRefreshToken(t *testing.T) {\n\tsecret := []byte(\"test-secret\")\n\n\tt.Run(\"parses valid refresh token\", func(t *testing.T) {\n\t\ttoken, _, err := GenerateRefreshToken(1, \"token-id-123\", secret)\n\t\trequire.NoError(t, err)\n\n\t\tclaims, err := ParseRefreshToken(token, secret)\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, \"1\", claims.Subject)\n\t\tassert.Equal(t, \"token-id-123\", claims.TokenID)\n\t\tassert.Equal(t, \"refresh\", claims.Type)\n\t})\n\n\tt.Run(\"fails with wrong secret\", func(t *testing.T) {\n\t\ttoken, _, err := GenerateRefreshToken(1, \"token-id-123\", secret)\n\t\trequire.NoError(t, err)\n\n\t\twrongSecret := []byte(\"wrong-secret\")\n\t\t_, err = ParseRefreshToken(token, wrongSecret)\n\t\tassert.Error(t, err)\n\t})\n\n\tt.Run(\"fails with invalid token\", func(t *testing.T) {\n\t\t_, err := ParseRefreshToken(\"invalid-token\", secret)\n\t\tassert.Error(t, err)\n\t})\n\n\tt.Run(\"fails with access token\", func(t *testing.T) {\n\t\t// Generate an access token and try to parse it as refresh token\n\t\t// Should fail because audience mismatch is caught before type check\n\t\taccessToken, _, err := GenerateAccessTokenV2(1, \"testuser\", \"USER\", \"ACTIVE\", secret)\n\t\trequire.NoError(t, err)\n\n\t\t_, err = ParseRefreshToken(accessToken, secret)\n\t\tassert.Error(t, err)\n\t\tassert.Contains(t, err.Error(), \"invalid audience\")\n\t})\n}\n\nfunc TestGeneratePersonalAccessToken(t *testing.T) {\n\tt.Run(\"generates token with correct prefix\", func(t *testing.T) {\n\t\ttoken := GeneratePersonalAccessToken()\n\t\tassert.NotEmpty(t, token)\n\t\tassert.True(t, len(token) > len(PersonalAccessTokenPrefix))\n\t\tassert.Equal(t, PersonalAccessTokenPrefix, token[:len(PersonalAccessTokenPrefix)])\n\t})\n\n\tt.Run(\"generates unique tokens\", func(t *testing.T) {\n\t\ttoken1 := GeneratePersonalAccessToken()\n\t\ttoken2 := GeneratePersonalAccessToken()\n\t\tassert.NotEqual(t, token1, token2)\n\t})\n\n\tt.Run(\"generates token of sufficient length\", func(t *testing.T) {\n\t\ttoken := GeneratePersonalAccessToken()\n\t\t// Prefix is \"memos_pat_\" (10 chars) + 32 random chars = at least 42 chars\n\t\tassert.True(t, len(token) >= 42, \"token should be at least 42 characters\")\n\t})\n}\n\nfunc TestHashPersonalAccessToken(t *testing.T) {\n\tt.Run(\"generates SHA-256 hash\", func(t *testing.T) {\n\t\ttoken := \"memos_pat_abc123\"\n\t\thash := HashPersonalAccessToken(token)\n\t\tassert.NotEmpty(t, hash)\n\t\tassert.Len(t, hash, 64, \"SHA-256 hex should be 64 characters\")\n\t})\n\n\tt.Run(\"same input produces same hash\", func(t *testing.T) {\n\t\ttoken := \"memos_pat_abc123\"\n\t\thash1 := HashPersonalAccessToken(token)\n\t\thash2 := HashPersonalAccessToken(token)\n\t\tassert.Equal(t, hash1, hash2)\n\t})\n\n\tt.Run(\"different inputs produce different hashes\", func(t *testing.T) {\n\t\ttoken1 := \"memos_pat_abc123\"\n\t\ttoken2 := \"memos_pat_xyz789\"\n\t\thash1 := HashPersonalAccessToken(token1)\n\t\thash2 := HashPersonalAccessToken(token2)\n\t\tassert.NotEqual(t, hash1, hash2)\n\t})\n\n\tt.Run(\"hash is deterministic\", func(t *testing.T) {\n\t\ttoken := GeneratePersonalAccessToken()\n\t\thash1 := HashPersonalAccessToken(token)\n\t\thash2 := HashPersonalAccessToken(token)\n\t\tassert.Equal(t, hash1, hash2)\n\t})\n}\n\nfunc TestAccessTokenV2Integration(t *testing.T) {\n\tsecret := []byte(\"test-secret\")\n\n\tt.Run(\"full lifecycle: generate, parse, validate\", func(t *testing.T) {\n\t\tuserID := int32(42)\n\t\tusername := \"john_doe\"\n\t\trole := \"ADMIN\"\n\t\tstatus := \"ACTIVE\"\n\n\t\t// Generate token\n\t\ttoken, expiresAt, err := GenerateAccessTokenV2(userID, username, role, status, secret)\n\t\trequire.NoError(t, err)\n\t\tassert.NotEmpty(t, token)\n\n\t\t// Parse token\n\t\tclaims, err := ParseAccessTokenV2(token, secret)\n\t\trequire.NoError(t, err)\n\n\t\t// Validate claims\n\t\tassert.Equal(t, \"42\", claims.Subject)\n\t\tassert.Equal(t, username, claims.Username)\n\t\tassert.Equal(t, role, claims.Role)\n\t\tassert.Equal(t, status, claims.Status)\n\t\tassert.Equal(t, \"access\", claims.Type)\n\t\tassert.Equal(t, Issuer, claims.Issuer)\n\t\tassert.NotNil(t, claims.IssuedAt)\n\t\tassert.NotNil(t, claims.ExpiresAt)\n\n\t\t// Validate expiration\n\t\tassert.True(t, claims.ExpiresAt.Equal(expiresAt) || claims.ExpiresAt.Before(expiresAt))\n\t})\n}\n\nfunc TestRefreshTokenIntegration(t *testing.T) {\n\tsecret := []byte(\"test-secret\")\n\n\tt.Run(\"full lifecycle: generate, parse, validate\", func(t *testing.T) {\n\t\tuserID := int32(42)\n\t\ttokenID := \"unique-token-id-456\"\n\n\t\t// Generate token\n\t\ttoken, expiresAt, err := GenerateRefreshToken(userID, tokenID, secret)\n\t\trequire.NoError(t, err)\n\t\tassert.NotEmpty(t, token)\n\n\t\t// Parse token\n\t\tclaims, err := ParseRefreshToken(token, secret)\n\t\trequire.NoError(t, err)\n\n\t\t// Validate claims\n\t\tassert.Equal(t, \"42\", claims.Subject)\n\t\tassert.Equal(t, tokenID, claims.TokenID)\n\t\tassert.Equal(t, \"refresh\", claims.Type)\n\t\tassert.Equal(t, Issuer, claims.Issuer)\n\t\tassert.NotNil(t, claims.IssuedAt)\n\t\tassert.NotNil(t, claims.ExpiresAt)\n\n\t\t// Validate expiration\n\t\tassert.True(t, claims.ExpiresAt.Equal(expiresAt) || claims.ExpiresAt.Before(expiresAt))\n\t})\n}\n\nfunc TestPersonalAccessTokenIntegration(t *testing.T) {\n\tt.Run(\"full lifecycle: generate, hash, verify\", func(t *testing.T) {\n\t\t// Generate token\n\t\ttoken := GeneratePersonalAccessToken()\n\t\tassert.NotEmpty(t, token)\n\t\tassert.True(t, len(token) > len(PersonalAccessTokenPrefix))\n\n\t\t// Hash token\n\t\thash := HashPersonalAccessToken(token)\n\t\tassert.Len(t, hash, 64)\n\n\t\t// Verify same token produces same hash\n\t\thashAgain := HashPersonalAccessToken(token)\n\t\tassert.Equal(t, hash, hashAgain)\n\n\t\t// Verify different token produces different hash\n\t\ttoken2 := GeneratePersonalAccessToken()\n\t\thash2 := HashPersonalAccessToken(token2)\n\t\tassert.NotEqual(t, hash, hash2)\n\t})\n}\n\nfunc TestTokenExpiration(t *testing.T) {\n\tsecret := []byte(\"test-secret\")\n\n\tt.Run(\"access token expires after AccessTokenDuration\", func(t *testing.T) {\n\t\t_, expiresAt, err := GenerateAccessTokenV2(1, \"testuser\", \"USER\", \"ACTIVE\", secret)\n\t\trequire.NoError(t, err)\n\n\t\texpectedExpiry := time.Now().Add(AccessTokenDuration)\n\t\tdelta := expiresAt.Sub(expectedExpiry)\n\t\tassert.True(t, delta < time.Second, \"expiration should be within 1 second of expected\")\n\t})\n\n\tt.Run(\"refresh token expires after RefreshTokenDuration\", func(t *testing.T) {\n\t\t_, expiresAt, err := GenerateRefreshToken(1, \"token-id\", secret)\n\t\trequire.NoError(t, err)\n\n\t\texpectedExpiry := time.Now().Add(RefreshTokenDuration)\n\t\tdelta := expiresAt.Sub(expectedExpiry)\n\t\tassert.True(t, delta < time.Second, \"expiration should be within 1 second of expected\")\n\t})\n}\n"
  },
  {
    "path": "server/router/api/v1/acl_config.go",
    "content": "package v1\n\n// PublicMethods defines API endpoints that don't require authentication.\n// All other endpoints require a valid session or access token.\n//\n// This is the SINGLE SOURCE OF TRUTH for public endpoints.\n// Both Connect interceptor and gRPC-Gateway interceptor use this map.\n//\n// Format: Full gRPC procedure path as returned by req.Spec().Procedure (Connect)\n// or info.FullMethod (gRPC interceptor).\nvar PublicMethods = map[string]struct{}{\n\t// Auth Service - login/token endpoints must be accessible without auth\n\t\"/memos.api.v1.AuthService/SignIn\":       {},\n\t\"/memos.api.v1.AuthService/RefreshToken\": {}, // Token refresh uses cookie, must be accessible when access token expired\n\n\t// Instance Service - needed before login to show instance info\n\t\"/memos.api.v1.InstanceService/GetInstanceProfile\": {},\n\t\"/memos.api.v1.InstanceService/GetInstanceSetting\": {},\n\n\t// User Service - public user profiles and stats\n\t\"/memos.api.v1.UserService/CreateUser\":       {}, // Allow first user registration\n\t\"/memos.api.v1.UserService/GetUser\":          {},\n\t\"/memos.api.v1.UserService/GetUserAvatar\":    {},\n\t\"/memos.api.v1.UserService/GetUserStats\":     {},\n\t\"/memos.api.v1.UserService/ListAllUserStats\": {},\n\t\"/memos.api.v1.UserService/SearchUsers\":      {},\n\n\t// Identity Provider Service - SSO buttons on login page\n\t\"/memos.api.v1.IdentityProviderService/ListIdentityProviders\": {},\n\n\t// Memo Service - public memos (visibility filtering done in service layer)\n\t\"/memos.api.v1.MemoService/GetMemo\":          {},\n\t\"/memos.api.v1.MemoService/ListMemos\":        {},\n\t\"/memos.api.v1.MemoService/ListMemoComments\": {},\n\n\t// Memo sharing - share-token endpoints require no authentication\n\t\"/memos.api.v1.MemoService/GetMemoByShare\": {},\n}\n\n// IsPublicMethod checks if a procedure path is public (no authentication required).\n// Returns true for public methods, false for protected methods.\nfunc IsPublicMethod(procedure string) bool {\n\t_, ok := PublicMethods[procedure]\n\treturn ok\n}\n"
  },
  {
    "path": "server/router/api/v1/acl_config_test.go",
    "content": "package v1\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\n// TestPublicMethodsArePublic verifies that methods in PublicMethods are recognized as public.\nfunc TestPublicMethodsArePublic(t *testing.T) {\n\tpublicMethods := []string{\n\t\t// Auth Service\n\t\t\"/memos.api.v1.AuthService/SignIn\",\n\t\t\"/memos.api.v1.AuthService/RefreshToken\",\n\t\t// Instance Service\n\t\t\"/memos.api.v1.InstanceService/GetInstanceProfile\",\n\t\t\"/memos.api.v1.InstanceService/GetInstanceSetting\",\n\t\t// User Service\n\t\t\"/memos.api.v1.UserService/CreateUser\",\n\t\t\"/memos.api.v1.UserService/GetUser\",\n\t\t\"/memos.api.v1.UserService/GetUserAvatar\",\n\t\t\"/memos.api.v1.UserService/GetUserStats\",\n\t\t\"/memos.api.v1.UserService/ListAllUserStats\",\n\t\t\"/memos.api.v1.UserService/SearchUsers\",\n\t\t// Identity Provider Service\n\t\t\"/memos.api.v1.IdentityProviderService/ListIdentityProviders\",\n\t\t// Memo Service\n\t\t\"/memos.api.v1.MemoService/GetMemo\",\n\t\t\"/memos.api.v1.MemoService/ListMemos\",\n\t}\n\n\tfor _, method := range publicMethods {\n\t\tt.Run(method, func(t *testing.T) {\n\t\t\tassert.True(t, IsPublicMethod(method), \"Expected %s to be public\", method)\n\t\t})\n\t}\n}\n\n// TestProtectedMethodsRequireAuth verifies that non-public methods are recognized as protected.\nfunc TestProtectedMethodsRequireAuth(t *testing.T) {\n\tprotectedMethods := []string{\n\t\t// Auth Service - logout and get current user require auth\n\t\t\"/memos.api.v1.AuthService/SignOut\",\n\t\t\"/memos.api.v1.AuthService/GetCurrentUser\",\n\t\t// Instance Service - admin operations\n\t\t\"/memos.api.v1.InstanceService/UpdateInstanceSetting\",\n\t\t// User Service - modification operations\n\t\t\"/memos.api.v1.UserService/ListUsers\",\n\t\t\"/memos.api.v1.UserService/UpdateUser\",\n\t\t\"/memos.api.v1.UserService/DeleteUser\",\n\t\t// Memo Service - write operations\n\t\t\"/memos.api.v1.MemoService/CreateMemo\",\n\t\t\"/memos.api.v1.MemoService/UpdateMemo\",\n\t\t\"/memos.api.v1.MemoService/DeleteMemo\",\n\t\t// Attachment Service - write operations\n\t\t\"/memos.api.v1.AttachmentService/CreateAttachment\",\n\t\t\"/memos.api.v1.AttachmentService/DeleteAttachment\",\n\t\t// Shortcut Service\n\t\t\"/memos.api.v1.ShortcutService/CreateShortcut\",\n\t\t\"/memos.api.v1.ShortcutService/ListShortcuts\",\n\t\t\"/memos.api.v1.ShortcutService/UpdateShortcut\",\n\t\t\"/memos.api.v1.ShortcutService/DeleteShortcut\",\n\t}\n\n\tfor _, method := range protectedMethods {\n\t\tt.Run(method, func(t *testing.T) {\n\t\t\tassert.False(t, IsPublicMethod(method), \"Expected %s to require auth\", method)\n\t\t})\n\t}\n}\n\n// TestUnknownMethodsRequireAuth verifies that unknown methods default to requiring auth.\nfunc TestUnknownMethodsRequireAuth(t *testing.T) {\n\tunknownMethods := []string{\n\t\t\"/unknown.Service/Method\",\n\t\t\"/memos.api.v1.UnknownService/Method\",\n\t\t\"\",\n\t\t\"invalid\",\n\t}\n\n\tfor _, method := range unknownMethods {\n\t\tt.Run(method, func(t *testing.T) {\n\t\t\tassert.False(t, IsPublicMethod(method), \"Unknown method %q should require auth\", method)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "server/router/api/v1/attachment_exif_test.go",
    "content": "package v1\n\nimport (\n\t\"bytes\"\n\t\"image\"\n\t\"image/color\"\n\t\"image/jpeg\"\n\t\"testing\"\n\n\t\"github.com/disintegration/imaging\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestShouldStripExif(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname     string\n\t\tmimeType string\n\t\texpected bool\n\t}{\n\t\t{\n\t\t\tname:     \"JPEG should strip EXIF\",\n\t\t\tmimeType: \"image/jpeg\",\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"JPG should strip EXIF\",\n\t\t\tmimeType: \"image/jpg\",\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"TIFF should strip EXIF\",\n\t\t\tmimeType: \"image/tiff\",\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"WebP should strip EXIF\",\n\t\t\tmimeType: \"image/webp\",\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"HEIC should strip EXIF\",\n\t\t\tmimeType: \"image/heic\",\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"HEIF should strip EXIF\",\n\t\t\tmimeType: \"image/heif\",\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"PNG should not strip EXIF\",\n\t\t\tmimeType: \"image/png\",\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"GIF should not strip EXIF\",\n\t\t\tmimeType: \"image/gif\",\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"text file should not strip EXIF\",\n\t\t\tmimeType: \"text/plain\",\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"PDF should not strip EXIF\",\n\t\t\tmimeType: \"application/pdf\",\n\t\t\texpected: false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tresult := shouldStripExif(tt.mimeType)\n\t\t\tassert.Equal(t, tt.expected, result)\n\t\t})\n\t}\n}\n\nfunc TestStripImageExif(t *testing.T) {\n\tt.Parallel()\n\n\t// Create a simple test image\n\timg := image.NewRGBA(image.Rect(0, 0, 100, 100))\n\t// Fill with red color\n\tfor y := 0; y < 100; y++ {\n\t\tfor x := 0; x < 100; x++ {\n\t\t\timg.Set(x, y, color.RGBA{R: 255, G: 0, B: 0, A: 255})\n\t\t}\n\t}\n\n\t// Encode as JPEG\n\tvar buf bytes.Buffer\n\terr := jpeg.Encode(&buf, img, &jpeg.Options{Quality: 90})\n\trequire.NoError(t, err)\n\toriginalData := buf.Bytes()\n\n\tt.Run(\"strip JPEG metadata\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tstrippedData, err := stripImageExif(originalData, \"image/jpeg\")\n\t\trequire.NoError(t, err)\n\t\tassert.NotEmpty(t, strippedData)\n\n\t\t// Verify it's still a valid image\n\t\tdecodedImg, err := imaging.Decode(bytes.NewReader(strippedData))\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, 100, decodedImg.Bounds().Dx())\n\t\tassert.Equal(t, 100, decodedImg.Bounds().Dy())\n\t})\n\n\tt.Run(\"strip JPG metadata (alternate extension)\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tstrippedData, err := stripImageExif(originalData, \"image/jpg\")\n\t\trequire.NoError(t, err)\n\t\tassert.NotEmpty(t, strippedData)\n\n\t\t// Verify it's still a valid image\n\t\tdecodedImg, err := imaging.Decode(bytes.NewReader(strippedData))\n\t\trequire.NoError(t, err)\n\t\tassert.NotNil(t, decodedImg)\n\t})\n\n\tt.Run(\"strip PNG metadata\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\t// Encode as PNG first\n\t\tvar pngBuf bytes.Buffer\n\t\terr := imaging.Encode(&pngBuf, img, imaging.PNG)\n\t\trequire.NoError(t, err)\n\n\t\tstrippedData, err := stripImageExif(pngBuf.Bytes(), \"image/png\")\n\t\trequire.NoError(t, err)\n\t\tassert.NotEmpty(t, strippedData)\n\n\t\t// Verify it's still a valid image\n\t\tdecodedImg, err := imaging.Decode(bytes.NewReader(strippedData))\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, 100, decodedImg.Bounds().Dx())\n\t\tassert.Equal(t, 100, decodedImg.Bounds().Dy())\n\t})\n\n\tt.Run(\"handle WebP format by converting to JPEG\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\t// WebP format will be converted to JPEG\n\t\tstrippedData, err := stripImageExif(originalData, \"image/webp\")\n\t\trequire.NoError(t, err)\n\t\tassert.NotEmpty(t, strippedData)\n\n\t\t// Verify it's a valid image\n\t\tdecodedImg, err := imaging.Decode(bytes.NewReader(strippedData))\n\t\trequire.NoError(t, err)\n\t\tassert.NotNil(t, decodedImg)\n\t})\n\n\tt.Run(\"handle HEIC format by converting to JPEG\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tstrippedData, err := stripImageExif(originalData, \"image/heic\")\n\t\trequire.NoError(t, err)\n\t\tassert.NotEmpty(t, strippedData)\n\n\t\t// Verify it's a valid image\n\t\tdecodedImg, err := imaging.Decode(bytes.NewReader(strippedData))\n\t\trequire.NoError(t, err)\n\t\tassert.NotNil(t, decodedImg)\n\t})\n\n\tt.Run(\"return error for invalid image data\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tinvalidData := []byte(\"not an image\")\n\t\t_, err := stripImageExif(invalidData, \"image/jpeg\")\n\t\tassert.Error(t, err)\n\t\tassert.Contains(t, err.Error(), \"failed to decode image\")\n\t})\n\n\tt.Run(\"return error for empty image data\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\temptyData := []byte{}\n\t\t_, err := stripImageExif(emptyData, \"image/jpeg\")\n\t\tassert.Error(t, err)\n\t})\n}\n"
  },
  {
    "path": "server/router/api/v1/attachment_service.go",
    "content": "package v1\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/binary\"\n\t\"fmt\"\n\t\"io\"\n\t\"log/slog\"\n\t\"mime\"\n\t\"net/http\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/disintegration/imaging\"\n\t\"github.com/pkg/errors\"\n\t\"google.golang.org/grpc/codes\"\n\t\"google.golang.org/grpc/status\"\n\t\"google.golang.org/protobuf/types/known/emptypb\"\n\t\"google.golang.org/protobuf/types/known/timestamppb\"\n\n\t\"github.com/usememos/memos/internal/profile\"\n\t\"github.com/usememos/memos/internal/util\"\n\t\"github.com/usememos/memos/plugin/filter\"\n\t\"github.com/usememos/memos/plugin/storage/s3\"\n\tv1pb \"github.com/usememos/memos/proto/gen/api/v1\"\n\tstorepb \"github.com/usememos/memos/proto/gen/store\"\n\t\"github.com/usememos/memos/store\"\n)\n\nconst (\n\t// The upload memory buffer is 32 MiB.\n\t// It should be kept low, so RAM usage doesn't get out of control.\n\t// This is unrelated to maximum upload size limit, which is now set through system setting.\n\tMaxUploadBufferSizeBytes = 32 << 20\n\tMebiByte                 = 1024 * 1024\n\t// ThumbnailCacheFolder is the folder name where the thumbnail images are stored.\n\tThumbnailCacheFolder = \".thumbnail_cache\"\n\n\t// defaultJPEGQuality is the JPEG quality used when re-encoding images for EXIF stripping.\n\t// Quality 95 maintains visual quality while ensuring metadata is removed.\n\tdefaultJPEGQuality = 95\n)\n\nvar SupportedThumbnailMimeTypes = []string{\n\t\"image/png\",\n\t\"image/jpeg\",\n}\n\n// exifCapableImageTypes defines image formats that may contain EXIF metadata.\n// These formats will have their EXIF metadata stripped on upload for privacy.\nvar exifCapableImageTypes = map[string]bool{\n\t\"image/jpeg\": true,\n\t\"image/jpg\":  true,\n\t\"image/tiff\": true,\n\t\"image/webp\": true,\n\t\"image/heic\": true,\n\t\"image/heif\": true,\n}\n\nfunc (s *APIV1Service) CreateAttachment(ctx context.Context, request *v1pb.CreateAttachmentRequest) (*v1pb.Attachment, error) {\n\tuser, err := s.fetchCurrentUser(ctx)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to get current user: %v\", err)\n\t}\n\tif user == nil {\n\t\treturn nil, status.Errorf(codes.Unauthenticated, \"user not authenticated\")\n\t}\n\n\t// Validate required fields\n\tif request.Attachment == nil {\n\t\treturn nil, status.Errorf(codes.InvalidArgument, \"attachment is required\")\n\t}\n\tif request.Attachment.Filename == \"\" {\n\t\treturn nil, status.Errorf(codes.InvalidArgument, \"filename is required\")\n\t}\n\tif !validateFilename(request.Attachment.Filename) {\n\t\treturn nil, status.Errorf(codes.InvalidArgument, \"filename contains invalid characters or format\")\n\t}\n\tif request.Attachment.Type == \"\" {\n\t\text := filepath.Ext(request.Attachment.Filename)\n\t\tmimeType := mime.TypeByExtension(ext)\n\t\tif mimeType == \"\" {\n\t\t\tmimeType = http.DetectContentType(request.Attachment.Content)\n\t\t}\n\t\t// ParseMediaType to strip parameters\n\t\tmediaType, _, err := mime.ParseMediaType(mimeType)\n\t\tif err == nil {\n\t\t\trequest.Attachment.Type = mediaType\n\t\t}\n\t}\n\tif request.Attachment.Type == \"\" {\n\t\trequest.Attachment.Type = \"application/octet-stream\"\n\t}\n\tif !isValidMimeType(request.Attachment.Type) {\n\t\treturn nil, status.Errorf(codes.InvalidArgument, \"invalid MIME type format\")\n\t}\n\n\tattachmentUID, err := ValidateAndGenerateUID(request.AttachmentId)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tcreate := &store.Attachment{\n\t\tUID:       attachmentUID,\n\t\tCreatorID: user.ID,\n\t\tFilename:  request.Attachment.Filename,\n\t\tType:      request.Attachment.Type,\n\t}\n\n\tinstanceStorageSetting, err := s.Store.GetInstanceStorageSetting(ctx)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to get instance storage setting: %v\", err)\n\t}\n\tsize := binary.Size(request.Attachment.Content)\n\tuploadSizeLimit := int(instanceStorageSetting.UploadSizeLimitMb) * MebiByte\n\tif uploadSizeLimit == 0 {\n\t\tuploadSizeLimit = MaxUploadBufferSizeBytes\n\t}\n\tif size > uploadSizeLimit {\n\t\treturn nil, status.Errorf(codes.InvalidArgument, \"file size exceeds the limit\")\n\t}\n\tcreate.Size = int64(size)\n\tcreate.Blob = request.Attachment.Content\n\n\t// Strip EXIF metadata from images for privacy protection.\n\t// This removes sensitive information like GPS location, device details, etc.\n\tif shouldStripExif(create.Type) {\n\t\tif strippedBlob, err := stripImageExif(create.Blob, create.Type); err != nil {\n\t\t\t// Log warning but continue with original image to ensure uploads don't fail.\n\t\t\tslog.Warn(\"failed to strip EXIF metadata from image\",\n\t\t\t\tslog.String(\"type\", create.Type),\n\t\t\t\tslog.String(\"filename\", create.Filename),\n\t\t\t\tslog.String(\"error\", err.Error()))\n\t\t} else {\n\t\t\tcreate.Blob = strippedBlob\n\t\t\tcreate.Size = int64(len(strippedBlob))\n\t\t}\n\t}\n\n\tif err := SaveAttachmentBlob(ctx, s.Profile, s.Store, create); err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to save attachment blob: %v\", err)\n\t}\n\n\tif request.Attachment.Memo != nil {\n\t\tmemoUID, err := ExtractMemoUIDFromName(*request.Attachment.Memo)\n\t\tif err != nil {\n\t\t\treturn nil, status.Errorf(codes.InvalidArgument, \"invalid memo name: %v\", err)\n\t\t}\n\t\tmemo, err := s.Store.GetMemo(ctx, &store.FindMemo{UID: &memoUID})\n\t\tif err != nil {\n\t\t\treturn nil, status.Errorf(codes.Internal, \"failed to find memo: %v\", err)\n\t\t}\n\t\tif memo == nil {\n\t\t\treturn nil, status.Errorf(codes.NotFound, \"memo not found: %s\", *request.Attachment.Memo)\n\t\t}\n\t\tcreate.MemoID = &memo.ID\n\t}\n\tattachment, err := s.Store.CreateAttachment(ctx, create)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to create attachment: %v\", err)\n\t}\n\n\treturn convertAttachmentFromStore(attachment), nil\n}\n\nfunc (s *APIV1Service) ListAttachments(ctx context.Context, request *v1pb.ListAttachmentsRequest) (*v1pb.ListAttachmentsResponse, error) {\n\tuser, err := s.fetchCurrentUser(ctx)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to get current user: %v\", err)\n\t}\n\tif user == nil {\n\t\treturn nil, status.Errorf(codes.Unauthenticated, \"user not authenticated\")\n\t}\n\n\t// Set default page size\n\tpageSize := int(request.PageSize)\n\tif pageSize <= 0 {\n\t\tpageSize = 50\n\t}\n\tif pageSize > 1000 {\n\t\tpageSize = 1000\n\t}\n\n\t// Parse page token for offset\n\toffset := 0\n\tif request.PageToken != \"\" {\n\t\t// Simple implementation: page token is the offset as string\n\t\t// In production, you might want to use encrypted tokens\n\t\tif parsed, err := fmt.Sscanf(request.PageToken, \"%d\", &offset); err != nil || parsed != 1 {\n\t\t\treturn nil, status.Errorf(codes.InvalidArgument, \"invalid page token\")\n\t\t}\n\t}\n\n\tfindAttachment := &store.FindAttachment{\n\t\tCreatorID: &user.ID,\n\t\tLimit:     &pageSize,\n\t\tOffset:    &offset,\n\t}\n\n\t// Parse filter if provided\n\tif request.Filter != \"\" {\n\t\tif err := s.validateAttachmentFilter(ctx, request.Filter); err != nil {\n\t\t\treturn nil, status.Errorf(codes.InvalidArgument, \"invalid filter: %v\", err)\n\t\t}\n\t\tfindAttachment.Filters = append(findAttachment.Filters, request.Filter)\n\t}\n\n\tattachments, err := s.Store.ListAttachments(ctx, findAttachment)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to list attachments: %v\", err)\n\t}\n\n\tresponse := &v1pb.ListAttachmentsResponse{}\n\n\tfor _, attachment := range attachments {\n\t\tresponse.Attachments = append(response.Attachments, convertAttachmentFromStore(attachment))\n\t}\n\n\t// For simplicity, set total size to the number of returned attachments.\n\t// In a full implementation, you'd want a separate count query\n\tresponse.TotalSize = int32(len(response.Attachments))\n\n\t// Set next page token if we got the full page size (indicating there might be more)\n\tif len(attachments) == pageSize {\n\t\tresponse.NextPageToken = fmt.Sprintf(\"%d\", offset+pageSize)\n\t}\n\n\treturn response, nil\n}\n\nfunc (s *APIV1Service) GetAttachment(ctx context.Context, request *v1pb.GetAttachmentRequest) (*v1pb.Attachment, error) {\n\tattachmentUID, err := ExtractAttachmentUIDFromName(request.Name)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.InvalidArgument, \"invalid attachment id: %v\", err)\n\t}\n\tattachment, err := s.Store.GetAttachment(ctx, &store.FindAttachment{UID: &attachmentUID})\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to get attachment: %v\", err)\n\t}\n\tif attachment == nil {\n\t\treturn nil, status.Errorf(codes.NotFound, \"attachment not found\")\n\t}\n\n\t// Check access permission based on linked memo visibility.\n\tif err := s.checkAttachmentAccess(ctx, attachment); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn convertAttachmentFromStore(attachment), nil\n}\n\nfunc (s *APIV1Service) UpdateAttachment(ctx context.Context, request *v1pb.UpdateAttachmentRequest) (*v1pb.Attachment, error) {\n\tattachmentUID, err := ExtractAttachmentUIDFromName(request.Attachment.Name)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.InvalidArgument, \"invalid attachment id: %v\", err)\n\t}\n\tif request.UpdateMask == nil || len(request.UpdateMask.Paths) == 0 {\n\t\treturn nil, status.Errorf(codes.InvalidArgument, \"update mask is required\")\n\t}\n\tuser, err := s.fetchCurrentUser(ctx)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to get current user: %v\", err)\n\t}\n\tif user == nil {\n\t\treturn nil, status.Errorf(codes.Unauthenticated, \"user not authenticated\")\n\t}\n\tattachment, err := s.Store.GetAttachment(ctx, &store.FindAttachment{UID: &attachmentUID})\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to get attachment: %v\", err)\n\t}\n\tif attachment == nil {\n\t\treturn nil, status.Errorf(codes.NotFound, \"attachment not found\")\n\t}\n\t// Only the creator or admin can update the attachment.\n\tif attachment.CreatorID != user.ID && !isSuperUser(user) {\n\t\treturn nil, status.Errorf(codes.PermissionDenied, \"permission denied\")\n\t}\n\n\tcurrentTs := time.Now().Unix()\n\tupdate := &store.UpdateAttachment{\n\t\tID:        attachment.ID,\n\t\tUpdatedTs: &currentTs,\n\t}\n\tfor _, field := range request.UpdateMask.Paths {\n\t\tif field == \"filename\" {\n\t\t\tif !validateFilename(request.Attachment.Filename) {\n\t\t\t\treturn nil, status.Errorf(codes.InvalidArgument, \"filename contains invalid characters or format\")\n\t\t\t}\n\t\t\tupdate.Filename = &request.Attachment.Filename\n\t\t}\n\t}\n\n\tif err := s.Store.UpdateAttachment(ctx, update); err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to update attachment: %v\", err)\n\t}\n\treturn s.GetAttachment(ctx, &v1pb.GetAttachmentRequest{\n\t\tName: request.Attachment.Name,\n\t})\n}\n\nfunc (s *APIV1Service) DeleteAttachment(ctx context.Context, request *v1pb.DeleteAttachmentRequest) (*emptypb.Empty, error) {\n\tattachmentUID, err := ExtractAttachmentUIDFromName(request.Name)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.InvalidArgument, \"invalid attachment id: %v\", err)\n\t}\n\tuser, err := s.fetchCurrentUser(ctx)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to get current user: %v\", err)\n\t}\n\tif user == nil {\n\t\treturn nil, status.Errorf(codes.Unauthenticated, \"user not authenticated\")\n\t}\n\tattachment, err := s.Store.GetAttachment(ctx, &store.FindAttachment{\n\t\tUID:       &attachmentUID,\n\t\tCreatorID: &user.ID,\n\t})\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to find attachment: %v\", err)\n\t}\n\tif attachment == nil {\n\t\treturn nil, status.Errorf(codes.NotFound, \"attachment not found\")\n\t}\n\t// Delete the attachment from the database.\n\tif err := s.Store.DeleteAttachment(ctx, &store.DeleteAttachment{\n\t\tID: attachment.ID,\n\t}); err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to delete attachment: %v\", err)\n\t}\n\treturn &emptypb.Empty{}, nil\n}\n\nfunc convertAttachmentFromStore(attachment *store.Attachment) *v1pb.Attachment {\n\tattachmentMessage := &v1pb.Attachment{\n\t\tName:       fmt.Sprintf(\"%s%s\", AttachmentNamePrefix, attachment.UID),\n\t\tCreateTime: timestamppb.New(time.Unix(attachment.CreatedTs, 0)),\n\t\tFilename:   attachment.Filename,\n\t\tType:       attachment.Type,\n\t\tSize:       attachment.Size,\n\t}\n\tif attachment.MemoUID != nil && *attachment.MemoUID != \"\" {\n\t\tmemoName := fmt.Sprintf(\"%s%s\", MemoNamePrefix, *attachment.MemoUID)\n\t\tattachmentMessage.Memo = &memoName\n\t}\n\tif attachment.StorageType == storepb.AttachmentStorageType_EXTERNAL || attachment.StorageType == storepb.AttachmentStorageType_S3 {\n\t\tattachmentMessage.ExternalLink = attachment.Reference\n\t}\n\n\treturn attachmentMessage\n}\n\n// SaveAttachmentBlob saves the blob of attachment based on the storage config.\nfunc SaveAttachmentBlob(ctx context.Context, profile *profile.Profile, stores *store.Store, create *store.Attachment) error {\n\tinstanceStorageSetting, err := stores.GetInstanceStorageSetting(ctx)\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"Failed to find instance storage setting\")\n\t}\n\n\tif instanceStorageSetting.StorageType == storepb.InstanceStorageSetting_LOCAL {\n\t\tfilepathTemplate := \"assets/{timestamp}_{filename}\"\n\t\tif instanceStorageSetting.FilepathTemplate != \"\" {\n\t\t\tfilepathTemplate = instanceStorageSetting.FilepathTemplate\n\t\t}\n\n\t\tinternalPath := filepathTemplate\n\t\tif !strings.Contains(internalPath, \"{filename}\") {\n\t\t\tinternalPath = filepath.Join(internalPath, \"{filename}\")\n\t\t}\n\t\tinternalPath = replaceFilenameWithPathTemplate(internalPath, create.Filename)\n\t\tinternalPath = filepath.ToSlash(internalPath)\n\n\t\t// Ensure the directory exists.\n\t\tosPath := filepath.FromSlash(internalPath)\n\t\tif !filepath.IsAbs(osPath) {\n\t\t\tosPath = filepath.Join(profile.Data, osPath)\n\t\t}\n\t\tdir := filepath.Dir(osPath)\n\t\tif err = os.MkdirAll(dir, os.ModePerm); err != nil {\n\t\t\treturn errors.Wrap(err, \"Failed to create directory\")\n\t\t}\n\n\t\t// Write the blob to the file.\n\t\tif err := os.WriteFile(osPath, create.Blob, 0644); err != nil {\n\t\t\treturn errors.Wrap(err, \"Failed to write file\")\n\t\t}\n\t\tcreate.Reference = internalPath\n\t\tcreate.Blob = nil\n\t\tcreate.StorageType = storepb.AttachmentStorageType_LOCAL\n\t} else if instanceStorageSetting.StorageType == storepb.InstanceStorageSetting_S3 {\n\t\ts3Config := instanceStorageSetting.S3Config\n\t\tif s3Config == nil {\n\t\t\treturn errors.Errorf(\"No activated external storage found\")\n\t\t}\n\t\ts3Client, err := s3.NewClient(ctx, s3Config)\n\t\tif err != nil {\n\t\t\treturn errors.Wrap(err, \"Failed to create s3 client\")\n\t\t}\n\n\t\tfilepathTemplate := instanceStorageSetting.FilepathTemplate\n\t\tif !strings.Contains(filepathTemplate, \"{filename}\") {\n\t\t\tfilepathTemplate = filepath.Join(filepathTemplate, \"{filename}\")\n\t\t}\n\t\tfilepathTemplate = replaceFilenameWithPathTemplate(filepathTemplate, create.Filename)\n\t\tkey, err := s3Client.UploadObject(ctx, filepathTemplate, create.Type, bytes.NewReader(create.Blob))\n\t\tif err != nil {\n\t\t\treturn errors.Wrap(err, \"Failed to upload via s3 client\")\n\t\t}\n\t\tpresignURL, err := s3Client.PresignGetObject(ctx, key)\n\t\tif err != nil {\n\t\t\treturn errors.Wrap(err, \"Failed to presign via s3 client\")\n\t\t}\n\n\t\tcreate.Reference = presignURL\n\t\tcreate.Blob = nil\n\t\tcreate.StorageType = storepb.AttachmentStorageType_S3\n\t\tcreate.Payload = &storepb.AttachmentPayload{\n\t\t\tPayload: &storepb.AttachmentPayload_S3Object_{\n\t\t\t\tS3Object: &storepb.AttachmentPayload_S3Object{\n\t\t\t\t\tS3Config:          s3Config,\n\t\t\t\t\tKey:               key,\n\t\t\t\t\tLastPresignedTime: timestamppb.New(time.Now()),\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (s *APIV1Service) GetAttachmentBlob(attachment *store.Attachment) ([]byte, error) {\n\t// For local storage, read the file from the local disk.\n\tif attachment.StorageType == storepb.AttachmentStorageType_LOCAL {\n\t\tattachmentPath := filepath.FromSlash(attachment.Reference)\n\t\tif !filepath.IsAbs(attachmentPath) {\n\t\t\tattachmentPath = filepath.Join(s.Profile.Data, attachmentPath)\n\t\t}\n\n\t\tfile, err := os.Open(attachmentPath)\n\t\tif err != nil {\n\t\t\tif os.IsNotExist(err) {\n\t\t\t\treturn nil, errors.Wrap(err, \"file not found\")\n\t\t\t}\n\t\t\treturn nil, errors.Wrap(err, \"failed to open the file\")\n\t\t}\n\t\tdefer file.Close()\n\t\tblob, err := io.ReadAll(file)\n\t\tif err != nil {\n\t\t\treturn nil, errors.Wrap(err, \"failed to read the file\")\n\t\t}\n\t\treturn blob, nil\n\t}\n\t// For S3 storage, download the file from S3.\n\tif attachment.StorageType == storepb.AttachmentStorageType_S3 {\n\t\tif attachment.Payload == nil {\n\t\t\treturn nil, errors.New(\"attachment payload is missing\")\n\t\t}\n\t\ts3Object := attachment.Payload.GetS3Object()\n\t\tif s3Object == nil {\n\t\t\treturn nil, errors.New(\"S3 object payload is missing\")\n\t\t}\n\t\tif s3Object.S3Config == nil {\n\t\t\treturn nil, errors.New(\"S3 config is missing\")\n\t\t}\n\t\tif s3Object.Key == \"\" {\n\t\t\treturn nil, errors.New(\"S3 object key is missing\")\n\t\t}\n\n\t\ts3Client, err := s3.NewClient(context.Background(), s3Object.S3Config)\n\t\tif err != nil {\n\t\t\treturn nil, errors.Wrap(err, \"failed to create S3 client\")\n\t\t}\n\n\t\tblob, err := s3Client.GetObject(context.Background(), s3Object.Key)\n\t\tif err != nil {\n\t\t\treturn nil, errors.Wrap(err, \"failed to get object from S3\")\n\t\t}\n\t\treturn blob, nil\n\t}\n\t// For database storage, return the blob from the database.\n\treturn attachment.Blob, nil\n}\n\nvar fileKeyPattern = regexp.MustCompile(`\\{[a-z]{1,9}\\}`)\n\nfunc replaceFilenameWithPathTemplate(path, filename string) string {\n\tt := time.Now()\n\tpath = fileKeyPattern.ReplaceAllStringFunc(path, func(s string) string {\n\t\tswitch s {\n\t\tcase \"{filename}\":\n\t\t\treturn filename\n\t\tcase \"{timestamp}\":\n\t\t\treturn fmt.Sprintf(\"%d\", t.Unix())\n\t\tcase \"{year}\":\n\t\t\treturn fmt.Sprintf(\"%d\", t.Year())\n\t\tcase \"{month}\":\n\t\t\treturn fmt.Sprintf(\"%02d\", t.Month())\n\t\tcase \"{day}\":\n\t\t\treturn fmt.Sprintf(\"%02d\", t.Day())\n\t\tcase \"{hour}\":\n\t\t\treturn fmt.Sprintf(\"%02d\", t.Hour())\n\t\tcase \"{minute}\":\n\t\t\treturn fmt.Sprintf(\"%02d\", t.Minute())\n\t\tcase \"{second}\":\n\t\t\treturn fmt.Sprintf(\"%02d\", t.Second())\n\t\tcase \"{uuid}\":\n\t\t\treturn util.GenUUID()\n\t\tdefault:\n\t\t\treturn s\n\t\t}\n\t})\n\treturn path\n}\n\nfunc validateFilename(filename string) bool {\n\t// Reject path traversal attempts and make sure no additional directories are created\n\tif !filepath.IsLocal(filename) || strings.ContainsAny(filename, \"/\\\\\") {\n\t\treturn false\n\t}\n\n\t// Reject filenames starting or ending with spaces or periods\n\tif strings.HasPrefix(filename, \" \") || strings.HasSuffix(filename, \" \") ||\n\t\tstrings.HasPrefix(filename, \".\") || strings.HasSuffix(filename, \".\") {\n\t\treturn false\n\t}\n\n\treturn true\n}\n\nfunc isValidMimeType(mimeType string) bool {\n\t// Reject empty or excessively long MIME types\n\tif mimeType == \"\" || len(mimeType) > 255 {\n\t\treturn false\n\t}\n\n\t// MIME type must match the pattern: type/subtype\n\t// Allow common characters in MIME types per RFC 2045\n\tmatched, _ := regexp.MatchString(`^[a-zA-Z0-9][a-zA-Z0-9!#$&^_.+-]{0,126}/[a-zA-Z0-9][a-zA-Z0-9!#$&^_.+-]{0,126}$`, mimeType)\n\treturn matched\n}\n\nfunc (s *APIV1Service) validateAttachmentFilter(ctx context.Context, filterStr string) error {\n\tif filterStr == \"\" {\n\t\treturn errors.New(\"filter cannot be empty\")\n\t}\n\n\tengine, err := filter.DefaultAttachmentEngine()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar dialect filter.DialectName\n\tswitch s.Profile.Driver {\n\tcase \"mysql\":\n\t\tdialect = filter.DialectMySQL\n\tcase \"postgres\":\n\t\tdialect = filter.DialectPostgres\n\tdefault:\n\t\tdialect = filter.DialectSQLite\n\t}\n\n\tif _, err := engine.CompileToStatement(ctx, filterStr, filter.RenderOptions{Dialect: dialect}); err != nil {\n\t\treturn errors.Wrap(err, \"failed to compile filter\")\n\t}\n\treturn nil\n}\n\n// checkAttachmentAccess verifies the user has permission to access the attachment.\n// For unlinked attachments (no memo), only the creator can access.\n// For linked attachments, access follows the memo's visibility rules.\nfunc (s *APIV1Service) checkAttachmentAccess(ctx context.Context, attachment *store.Attachment) error {\n\tuser, _ := s.fetchCurrentUser(ctx)\n\n\t// For unlinked attachments, only the creator can access.\n\tif attachment.MemoID == nil {\n\t\tif user == nil {\n\t\t\treturn status.Errorf(codes.Unauthenticated, \"user not authenticated\")\n\t\t}\n\t\tif attachment.CreatorID != user.ID && !isSuperUser(user) {\n\t\t\treturn status.Errorf(codes.PermissionDenied, \"permission denied\")\n\t\t}\n\t\treturn nil\n\t}\n\n\t// For linked attachments, check memo visibility.\n\tmemo, err := s.Store.GetMemo(ctx, &store.FindMemo{ID: attachment.MemoID})\n\tif err != nil {\n\t\treturn status.Errorf(codes.Internal, \"failed to get memo: %v\", err)\n\t}\n\tif memo == nil {\n\t\treturn status.Errorf(codes.NotFound, \"memo not found\")\n\t}\n\n\tif memo.Visibility == store.Public {\n\t\treturn nil\n\t}\n\tif user == nil {\n\t\treturn status.Errorf(codes.Unauthenticated, \"user not authenticated\")\n\t}\n\tif memo.Visibility == store.Private && memo.CreatorID != user.ID && !isSuperUser(user) {\n\t\treturn status.Errorf(codes.PermissionDenied, \"permission denied\")\n\t}\n\treturn nil\n}\n\n// shouldStripExif checks if the MIME type is an image format that may contain EXIF metadata.\n// Returns true for formats like JPEG, TIFF, WebP, HEIC, and HEIF which commonly contain\n// privacy-sensitive metadata such as GPS coordinates, camera settings, and device information.\nfunc shouldStripExif(mimeType string) bool {\n\treturn exifCapableImageTypes[mimeType]\n}\n\n// stripImageExif removes EXIF metadata from image files by decoding and re-encoding them.\n// This prevents exposure of sensitive metadata such as GPS location, camera details, and timestamps.\n//\n// The function preserves the correct image orientation by applying EXIF orientation tags\n// during decoding before stripping all metadata. Images are re-encoded with high quality\n// to minimize visual degradation.\n//\n// Supported formats:\n//   - JPEG/JPG: Re-encoded as JPEG with quality 95\n//   - PNG: Re-encoded as PNG (lossless)\n//   - TIFF/WebP/HEIC/HEIF: Re-encoded as JPEG with quality 95\n//\n// Returns the cleaned image data without any EXIF metadata, or an error if processing fails.\nfunc stripImageExif(imageData []byte, mimeType string) ([]byte, error) {\n\t// Decode image with automatic EXIF orientation correction.\n\t// This ensures the image displays correctly after metadata removal.\n\timg, err := imaging.Decode(bytes.NewReader(imageData), imaging.AutoOrientation(true))\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"failed to decode image\")\n\t}\n\n\t// Re-encode the image without EXIF metadata.\n\tvar buf bytes.Buffer\n\tvar encodeErr error\n\n\tif mimeType == \"image/png\" {\n\t\t// Preserve PNG format for lossless encoding\n\t\tencodeErr = imaging.Encode(&buf, img, imaging.PNG)\n\t} else {\n\t\t// For JPEG, TIFF, WebP, HEIC, HEIF - re-encode as JPEG.\n\t\t// This ensures EXIF is stripped and provides good compression.\n\t\tencodeErr = imaging.Encode(&buf, img, imaging.JPEG, imaging.JPEGQuality(defaultJPEGQuality))\n\t}\n\n\tif encodeErr != nil {\n\t\treturn nil, errors.Wrap(encodeErr, \"failed to encode image\")\n\t}\n\n\treturn buf.Bytes(), nil\n}\n"
  },
  {
    "path": "server/router/api/v1/auth_service.go",
    "content": "package v1\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"regexp\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/pkg/errors\"\n\t\"golang.org/x/crypto/bcrypt\"\n\t\"google.golang.org/grpc/codes\"\n\t\"google.golang.org/grpc/metadata\"\n\t\"google.golang.org/grpc/status\"\n\t\"google.golang.org/protobuf/types/known/emptypb\"\n\t\"google.golang.org/protobuf/types/known/timestamppb\"\n\n\t\"github.com/usememos/memos/internal/util\"\n\t\"github.com/usememos/memos/plugin/idp\"\n\t\"github.com/usememos/memos/plugin/idp/oauth2\"\n\tv1pb \"github.com/usememos/memos/proto/gen/api/v1\"\n\tstorepb \"github.com/usememos/memos/proto/gen/store\"\n\t\"github.com/usememos/memos/server/auth\"\n\t\"github.com/usememos/memos/store\"\n)\n\nconst (\n\tunmatchedUsernameAndPasswordError = \"unmatched username and password\"\n)\n\n// GetCurrentUser returns the authenticated user's information.\n// Validates the access token and returns user details.\n//\n// Authentication: Required (access token).\n// Returns: User information.\nfunc (s *APIV1Service) GetCurrentUser(ctx context.Context, _ *v1pb.GetCurrentUserRequest) (*v1pb.GetCurrentUserResponse, error) {\n\tuser, err := s.fetchCurrentUser(ctx)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Unauthenticated, \"failed to get current user: %v\", err)\n\t}\n\tif user == nil {\n\t\t// Clear auth cookies\n\t\tif err := s.clearAuthCookies(ctx); err != nil {\n\t\t\treturn nil, status.Errorf(codes.Internal, \"failed to clear auth cookies: %v\", err)\n\t\t}\n\t\treturn nil, status.Errorf(codes.Unauthenticated, \"user not found\")\n\t}\n\n\treturn &v1pb.GetCurrentUserResponse{\n\t\tUser: convertUserFromStore(user),\n\t}, nil\n}\n\n// SignIn authenticates a user with credentials and returns tokens.\n// On success, returns an access token and sets a refresh token cookie.\n//\n// Supports two authentication methods:\n// 1. Password-based authentication (username + password).\n// 2. SSO authentication (OAuth2 authorization code).\n//\n// Authentication: Not required (public endpoint).\n// Returns: User info, access token, and token expiry.\nfunc (s *APIV1Service) SignIn(ctx context.Context, request *v1pb.SignInRequest) (*v1pb.SignInResponse, error) {\n\tvar existingUser *store.User\n\n\t// Authentication Method 1: Password-based authentication\n\tif passwordCredentials := request.GetPasswordCredentials(); passwordCredentials != nil {\n\t\tuser, err := s.Store.GetUser(ctx, &store.FindUser{\n\t\t\tUsername: &passwordCredentials.Username,\n\t\t})\n\t\tif err != nil {\n\t\t\treturn nil, status.Errorf(codes.Internal, \"failed to get user, error: %v\", err)\n\t\t}\n\t\tif user == nil {\n\t\t\treturn nil, status.Errorf(codes.InvalidArgument, unmatchedUsernameAndPasswordError)\n\t\t}\n\t\t// Compare the stored hashed password, with the hashed version of the password that was received.\n\t\tif err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(passwordCredentials.Password)); err != nil {\n\t\t\treturn nil, status.Errorf(codes.InvalidArgument, unmatchedUsernameAndPasswordError)\n\t\t}\n\t\tinstanceGeneralSetting, err := s.Store.GetInstanceGeneralSetting(ctx)\n\t\tif err != nil {\n\t\t\treturn nil, status.Errorf(codes.Internal, \"failed to get instance general setting, error: %v\", err)\n\t\t}\n\t\t// Check if the password auth in is allowed.\n\t\tif instanceGeneralSetting.DisallowPasswordAuth && user.Role == store.RoleUser {\n\t\t\treturn nil, status.Errorf(codes.PermissionDenied, \"password signin is not allowed\")\n\t\t}\n\t\texistingUser = user\n\t} else if ssoCredentials := request.GetSsoCredentials(); ssoCredentials != nil {\n\t\t// Authentication Method 2: SSO (OAuth2) authentication\n\t\tidpUID, err := ExtractIdentityProviderUIDFromName(ssoCredentials.IdpName)\n\t\tif err != nil {\n\t\t\treturn nil, status.Errorf(codes.InvalidArgument, \"invalid identity provider name: %v\", err)\n\t\t}\n\t\tidentityProvider, err := s.Store.GetIdentityProvider(ctx, &store.FindIdentityProvider{\n\t\t\tUID: &idpUID,\n\t\t})\n\t\tif err != nil {\n\t\t\treturn nil, status.Errorf(codes.Internal, \"failed to get identity provider, error: %v\", err)\n\t\t}\n\t\tif identityProvider == nil {\n\t\t\treturn nil, status.Errorf(codes.InvalidArgument, \"identity provider not found\")\n\t\t}\n\n\t\tvar userInfo *idp.IdentityProviderUserInfo\n\t\tif identityProvider.Type == storepb.IdentityProvider_OAUTH2 {\n\t\t\toauth2IdentityProvider, err := oauth2.NewIdentityProvider(identityProvider.Config.GetOauth2Config())\n\t\t\tif err != nil {\n\t\t\t\treturn nil, status.Errorf(codes.Internal, \"failed to create oauth2 identity provider, error: %v\", err)\n\t\t\t}\n\t\t\t// Pass code_verifier for PKCE support (empty string if not provided for backward compatibility)\n\t\t\ttoken, err := oauth2IdentityProvider.ExchangeToken(ctx, ssoCredentials.RedirectUri, ssoCredentials.Code, ssoCredentials.CodeVerifier)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, status.Errorf(codes.Internal, \"failed to exchange token, error: %v\", err)\n\t\t\t}\n\t\t\tuserInfo, err = oauth2IdentityProvider.UserInfo(token)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, status.Errorf(codes.Internal, \"failed to get user info, error: %v\", err)\n\t\t\t}\n\t\t}\n\n\t\tidentifierFilter := identityProvider.IdentifierFilter\n\t\tif identifierFilter != \"\" {\n\t\t\tidentifierFilterRegex, err := regexp.Compile(identifierFilter)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, status.Errorf(codes.Internal, \"failed to compile identifier filter regex, error: %v\", err)\n\t\t\t}\n\t\t\tif !identifierFilterRegex.MatchString(userInfo.Identifier) {\n\t\t\t\treturn nil, status.Errorf(codes.PermissionDenied, \"identifier %s is not allowed\", userInfo.Identifier)\n\t\t\t}\n\t\t}\n\n\t\tuser, err := s.Store.GetUser(ctx, &store.FindUser{\n\t\t\tUsername: &userInfo.Identifier,\n\t\t})\n\t\tif err != nil {\n\t\t\treturn nil, status.Errorf(codes.Internal, \"failed to get user, error: %v\", err)\n\t\t}\n\t\tif user == nil {\n\t\t\t// Check if the user is allowed to sign up.\n\t\t\tinstanceGeneralSetting, err := s.Store.GetInstanceGeneralSetting(ctx)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, status.Errorf(codes.Internal, \"failed to get instance general setting, error: %v\", err)\n\t\t\t}\n\t\t\tif instanceGeneralSetting.DisallowUserRegistration {\n\t\t\t\treturn nil, status.Errorf(codes.PermissionDenied, \"user registration is not allowed\")\n\t\t\t}\n\n\t\t\t// Create a new user with the user info from the identity provider.\n\t\t\tuserCreate := &store.User{\n\t\t\t\tUsername: userInfo.Identifier,\n\t\t\t\t// The new signup user should be normal user by default.\n\t\t\t\tRole:      store.RoleUser,\n\t\t\t\tNickname:  userInfo.DisplayName,\n\t\t\t\tEmail:     userInfo.Email,\n\t\t\t\tAvatarURL: userInfo.AvatarURL,\n\t\t\t}\n\t\t\tpassword, err := util.RandomString(20)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, status.Errorf(codes.Internal, \"failed to generate random password, error: %v\", err)\n\t\t\t}\n\t\t\tpasswordHash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, status.Errorf(codes.Internal, \"failed to generate password hash, error: %v\", err)\n\t\t\t}\n\t\t\tuserCreate.PasswordHash = string(passwordHash)\n\t\t\tuser, err = s.Store.CreateUser(ctx, userCreate)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, status.Errorf(codes.Internal, \"failed to create user, error: %v\", err)\n\t\t\t}\n\t\t}\n\t\texistingUser = user\n\t}\n\n\tif existingUser == nil {\n\t\treturn nil, status.Errorf(codes.InvalidArgument, \"invalid credentials\")\n\t}\n\tif existingUser.RowStatus == store.Archived {\n\t\treturn nil, status.Errorf(codes.PermissionDenied, \"user has been archived with username %s\", existingUser.Username)\n\t}\n\n\taccessToken, accessExpiresAt, err := s.doSignIn(ctx, existingUser)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to sign in: %v\", err)\n\t}\n\n\treturn &v1pb.SignInResponse{\n\t\tUser:                 convertUserFromStore(existingUser),\n\t\tAccessToken:          accessToken,\n\t\tAccessTokenExpiresAt: timestamppb.New(accessExpiresAt),\n\t}, nil\n}\n\n// doSignIn performs the actual sign-in operation by creating a session and setting the cookie.\n//\n// This function:\n// 1. Generates refresh token and access token.\n// 2. Stores refresh token metadata in user_setting.\n// 3. Sets refresh token as HttpOnly cookie.\n// 4. Returns access token and its expiry time.\nfunc (s *APIV1Service) doSignIn(ctx context.Context, user *store.User) (string, time.Time, error) {\n\t// Generate refresh token\n\ttokenID := util.GenUUID()\n\trefreshToken, refreshExpiresAt, err := auth.GenerateRefreshToken(user.ID, tokenID, []byte(s.Secret))\n\tif err != nil {\n\t\treturn \"\", time.Time{}, status.Errorf(codes.Internal, \"failed to generate refresh token: %v\", err)\n\t}\n\n\t// Store refresh token metadata\n\tclientInfo := s.extractClientInfo(ctx)\n\trefreshTokenRecord := &storepb.RefreshTokensUserSetting_RefreshToken{\n\t\tTokenId:    tokenID,\n\t\tExpiresAt:  timestamppb.New(refreshExpiresAt),\n\t\tCreatedAt:  timestamppb.Now(),\n\t\tClientInfo: clientInfo,\n\t}\n\tif err := s.Store.AddUserRefreshToken(ctx, user.ID, refreshTokenRecord); err != nil {\n\t\tslog.Error(\"failed to store refresh token\", \"error\", err)\n\t}\n\n\t// Set refresh token cookie\n\trefreshCookie := s.buildRefreshTokenCookie(ctx, refreshToken, refreshExpiresAt)\n\tif err := SetResponseHeader(ctx, \"Set-Cookie\", refreshCookie); err != nil {\n\t\treturn \"\", time.Time{}, status.Errorf(codes.Internal, \"failed to set refresh token cookie: %v\", err)\n\t}\n\n\t// Generate access token\n\taccessToken, accessExpiresAt, err := auth.GenerateAccessTokenV2(\n\t\tuser.ID,\n\t\tuser.Username,\n\t\tstring(user.Role),\n\t\tstring(user.RowStatus),\n\t\t[]byte(s.Secret),\n\t)\n\tif err != nil {\n\t\treturn \"\", time.Time{}, status.Errorf(codes.Internal, \"failed to generate access token: %v\", err)\n\t}\n\n\treturn accessToken, accessExpiresAt, nil\n}\n\n// SignOut terminates the user's authentication.\n// Revokes the refresh token and clears the authentication cookie.\n//\n// Authentication: Required (access token).\n// Returns: Empty response on success.\nfunc (s *APIV1Service) SignOut(ctx context.Context, _ *v1pb.SignOutRequest) (*emptypb.Empty, error) {\n\t// Get user from access token claims\n\tclaims := auth.GetUserClaims(ctx)\n\tif claims != nil {\n\t\t// Revoke refresh token if we can identify it\n\t\trefreshToken := \"\"\n\t\tif md, ok := metadata.FromIncomingContext(ctx); ok {\n\t\t\tif cookies := md.Get(\"cookie\"); len(cookies) > 0 {\n\t\t\t\trefreshToken = auth.ExtractRefreshTokenFromCookie(cookies[0])\n\t\t\t}\n\t\t}\n\t\tif refreshToken != \"\" {\n\t\t\trefreshClaims, err := auth.ParseRefreshToken(refreshToken, []byte(s.Secret))\n\t\t\tif err == nil {\n\t\t\t\t// Remove refresh token from user_setting by token_id\n\t\t\t\t_ = s.Store.RemoveUserRefreshToken(ctx, claims.UserID, refreshClaims.TokenID)\n\t\t\t}\n\t\t}\n\t}\n\n\t// Clear refresh token cookie\n\tif err := s.clearAuthCookies(ctx); err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to clear auth cookies, error: %v\", err)\n\t}\n\treturn &emptypb.Empty{}, nil\n}\n\n// RefreshToken exchanges a valid refresh token for a new access token.\n//\n// This endpoint implements refresh token rotation with sliding window sessions:\n// 1. Extracts the refresh token from the HttpOnly cookie (memos_refresh)\n// 2. Validates the refresh token against the database (checking expiry and revocation)\n// 3. Rotates the refresh token: generates a new one with fresh 30-day expiry\n// 4. Generates a new short-lived access token (15 minutes)\n// 5. Sets the new refresh token as HttpOnly cookie\n// 6. Returns the new access token and its expiry time\n//\n// Token rotation provides:\n// - Sliding window sessions: active users stay logged in indefinitely\n// - Better security: stolen refresh tokens become invalid after legitimate refresh\n//\n// Authentication: Requires valid refresh token in cookie (public endpoint)\n// Returns: New access token and expiry timestamp.\nfunc (s *APIV1Service) RefreshToken(ctx context.Context, _ *v1pb.RefreshTokenRequest) (*v1pb.RefreshTokenResponse, error) {\n\t// Extract refresh token from cookie\n\trefreshToken := \"\"\n\tif md, ok := metadata.FromIncomingContext(ctx); ok {\n\t\tif cookies := md.Get(\"cookie\"); len(cookies) > 0 {\n\t\t\trefreshToken = auth.ExtractRefreshTokenFromCookie(cookies[0])\n\t\t}\n\t}\n\n\tif refreshToken == \"\" {\n\t\treturn nil, status.Errorf(codes.Unauthenticated, \"refresh token not found\")\n\t}\n\n\t// Validate refresh token and get old token ID for rotation\n\tauthenticator := auth.NewAuthenticator(s.Store, s.Secret)\n\tuser, oldTokenID, err := authenticator.AuthenticateByRefreshToken(ctx, refreshToken)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Unauthenticated, \"invalid refresh token: %v\", err)\n\t}\n\n\t// --- Refresh Token Rotation ---\n\t// Generate new refresh token with fresh 30-day expiry (sliding window)\n\tnewTokenID := util.GenUUID()\n\tnewRefreshToken, newRefreshExpiresAt, err := auth.GenerateRefreshToken(user.ID, newTokenID, []byte(s.Secret))\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to generate refresh token: %v\", err)\n\t}\n\n\t// Store new refresh token (add before remove to handle race conditions)\n\tclientInfo := s.extractClientInfo(ctx)\n\tnewRefreshTokenRecord := &storepb.RefreshTokensUserSetting_RefreshToken{\n\t\tTokenId:    newTokenID,\n\t\tExpiresAt:  timestamppb.New(newRefreshExpiresAt),\n\t\tCreatedAt:  timestamppb.Now(),\n\t\tClientInfo: clientInfo,\n\t}\n\tif err := s.Store.AddUserRefreshToken(ctx, user.ID, newRefreshTokenRecord); err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to store refresh token: %v\", err)\n\t}\n\n\t// Remove old refresh token\n\tif err := s.Store.RemoveUserRefreshToken(ctx, user.ID, oldTokenID); err != nil {\n\t\t// Log but don't fail - old token will expire naturally\n\t\tslog.Warn(\"failed to remove old refresh token\", \"error\", err, \"userID\", user.ID, \"tokenID\", oldTokenID)\n\t}\n\n\t// Set new refresh token cookie\n\tnewRefreshCookie := s.buildRefreshTokenCookie(ctx, newRefreshToken, newRefreshExpiresAt)\n\tif err := SetResponseHeader(ctx, \"Set-Cookie\", newRefreshCookie); err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to set refresh token cookie: %v\", err)\n\t}\n\t// --- End Rotation ---\n\n\t// Generate new access token\n\taccessToken, expiresAt, err := auth.GenerateAccessTokenV2(\n\t\tuser.ID,\n\t\tuser.Username,\n\t\tstring(user.Role),\n\t\tstring(user.RowStatus),\n\t\t[]byte(s.Secret),\n\t)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to generate access token: %v\", err)\n\t}\n\n\treturn &v1pb.RefreshTokenResponse{\n\t\tAccessToken: accessToken,\n\t\tExpiresAt:   timestamppb.New(expiresAt),\n\t}, nil\n}\n\nfunc (s *APIV1Service) clearAuthCookies(ctx context.Context) error {\n\t// Clear refresh token cookie\n\trefreshCookie := s.buildRefreshTokenCookie(ctx, \"\", time.Time{})\n\tif err := SetResponseHeader(ctx, \"Set-Cookie\", refreshCookie); err != nil {\n\t\treturn errors.Wrap(err, \"failed to set refresh cookie\")\n\t}\n\n\treturn nil\n}\n\nfunc (*APIV1Service) buildRefreshTokenCookie(ctx context.Context, refreshToken string, expireTime time.Time) string {\n\tattrs := []string{\n\t\tfmt.Sprintf(\"%s=%s\", auth.RefreshTokenCookieName, refreshToken),\n\t\t\"Path=/\",\n\t\t\"HttpOnly\",\n\t}\n\tif expireTime.IsZero() {\n\t\tattrs = append(attrs, \"Expires=Thu, 01 Jan 1970 00:00:00 GMT\")\n\t} else {\n\t\t// RFC 6265 requires cookie expiration dates to use GMT timezone\n\t\t// Convert to UTC and format with explicit \"GMT\" to ensure browser compatibility\n\t\tattrs = append(attrs, \"Expires=\"+expireTime.UTC().Format(\"Mon, 02 Jan 2006 15:04:05 GMT\"))\n\t}\n\n\t// Try to determine if the request is HTTPS by checking the origin header\n\t// Default to non-HTTPS (Lax SameSite) if metadata is not available\n\tisHTTPS := false\n\tif md, ok := metadata.FromIncomingContext(ctx); ok {\n\t\tfor _, v := range md.Get(\"origin\") {\n\t\t\tif strings.HasPrefix(v, \"https://\") {\n\t\t\t\tisHTTPS = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\tif isHTTPS {\n\t\tattrs = append(attrs, \"SameSite=Lax\", \"Secure\")\n\t} else {\n\t\tattrs = append(attrs, \"SameSite=Lax\")\n\t}\n\treturn strings.Join(attrs, \"; \")\n}\n\nfunc (s *APIV1Service) fetchCurrentUser(ctx context.Context) (*store.User, error) {\n\tuserID := auth.GetUserID(ctx)\n\tif userID == 0 {\n\t\treturn nil, nil\n\t}\n\tuser, err := s.Store.GetUser(ctx, &store.FindUser{\n\t\tID: &userID,\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif user == nil {\n\t\treturn nil, errors.Errorf(\"user %d not found\", userID)\n\t}\n\treturn user, nil\n}\n\n// extractClientInfo extracts comprehensive client information from the request context.\n//\n// This function parses metadata from the gRPC context to extract:\n// - User Agent: Raw user agent string for detailed parsing\n// - IP Address: Client IP from X-Forwarded-For or X-Real-IP headers\n// - Device Type: \"mobile\", \"tablet\", or \"desktop\" (parsed from user agent)\n// - Operating System: OS name and version (e.g., \"iOS 17.1\", \"Windows 10/11\")\n// - Browser: Browser name and version (e.g., \"Chrome 120.0.0.0\")\n//\n// This information enables users to:\n// - See all active sessions with device details\n// - Identify suspicious login attempts\n// - Revoke specific sessions from unknown devices.\nfunc (s *APIV1Service) extractClientInfo(ctx context.Context) *storepb.RefreshTokensUserSetting_ClientInfo {\n\tclientInfo := &storepb.RefreshTokensUserSetting_ClientInfo{}\n\n\t// Extract user agent from metadata if available\n\tif md, ok := metadata.FromIncomingContext(ctx); ok {\n\t\tif userAgents := md.Get(\"user-agent\"); len(userAgents) > 0 {\n\t\t\tuserAgent := userAgents[0]\n\t\t\tclientInfo.UserAgent = userAgent\n\n\t\t\t// Parse user agent to extract device type, OS, browser info\n\t\t\ts.parseUserAgent(userAgent, clientInfo)\n\t\t}\n\t\tif forwardedFor := md.Get(\"x-forwarded-for\"); len(forwardedFor) > 0 {\n\t\t\tipAddress := strings.Split(forwardedFor[0], \",\")[0] // Get the first IP in case of multiple\n\t\t\tipAddress = strings.TrimSpace(ipAddress)\n\t\t\tclientInfo.IpAddress = ipAddress\n\t\t} else if realIP := md.Get(\"x-real-ip\"); len(realIP) > 0 {\n\t\t\tclientInfo.IpAddress = realIP[0]\n\t\t}\n\t}\n\n\treturn clientInfo\n}\n\n// parseUserAgent extracts device type, OS, and browser information from user agent string.\n//\n// Detection logic:\n// - Device Type: Checks for keywords like \"mobile\", \"tablet\", \"ipad\"\n// - OS: Pattern matches for iOS, Android, Windows, macOS, Linux, Chrome OS\n// - Browser: Identifies Edge, Chrome, Firefox, Safari, Opera\n//\n// Note: This is a simplified parser. For production use with high accuracy requirements,\n// consider using a dedicated user agent parsing library.\nfunc (*APIV1Service) parseUserAgent(userAgent string, clientInfo *storepb.RefreshTokensUserSetting_ClientInfo) {\n\tif userAgent == \"\" {\n\t\treturn\n\t}\n\n\tuserAgent = strings.ToLower(userAgent)\n\n\t// Detect device type\n\tif strings.Contains(userAgent, \"ipad\") || strings.Contains(userAgent, \"tablet\") {\n\t\tclientInfo.DeviceType = \"tablet\"\n\t} else if strings.Contains(userAgent, \"mobile\") || strings.Contains(userAgent, \"android\") ||\n\t\tstrings.Contains(userAgent, \"iphone\") || strings.Contains(userAgent, \"ipod\") ||\n\t\tstrings.Contains(userAgent, \"windows phone\") || strings.Contains(userAgent, \"blackberry\") {\n\t\tclientInfo.DeviceType = \"mobile\"\n\t} else {\n\t\tclientInfo.DeviceType = \"desktop\"\n\t}\n\n\t// Detect operating system\n\tif strings.Contains(userAgent, \"iphone os\") || strings.Contains(userAgent, \"cpu os\") {\n\t\t// Extract iOS version\n\t\tif idx := strings.Index(userAgent, \"cpu os \"); idx != -1 {\n\t\t\tversionStart := idx + 7\n\t\t\tversionEnd := strings.Index(userAgent[versionStart:], \" \")\n\t\t\tif versionEnd != -1 {\n\t\t\t\tversion := strings.ReplaceAll(userAgent[versionStart:versionStart+versionEnd], \"_\", \".\")\n\t\t\t\tclientInfo.Os = \"iOS \" + version\n\t\t\t} else {\n\t\t\t\tclientInfo.Os = \"iOS\"\n\t\t\t}\n\t\t} else if idx := strings.Index(userAgent, \"iphone os \"); idx != -1 {\n\t\t\tversionStart := idx + 10\n\t\t\tversionEnd := strings.Index(userAgent[versionStart:], \" \")\n\t\t\tif versionEnd != -1 {\n\t\t\t\tversion := strings.ReplaceAll(userAgent[versionStart:versionStart+versionEnd], \"_\", \".\")\n\t\t\t\tclientInfo.Os = \"iOS \" + version\n\t\t\t} else {\n\t\t\t\tclientInfo.Os = \"iOS\"\n\t\t\t}\n\t\t} else {\n\t\t\tclientInfo.Os = \"iOS\"\n\t\t}\n\t} else if strings.Contains(userAgent, \"android\") {\n\t\t// Extract Android version\n\t\tif idx := strings.Index(userAgent, \"android \"); idx != -1 {\n\t\t\tversionStart := idx + 8\n\t\t\tversionEnd := strings.Index(userAgent[versionStart:], \";\")\n\t\t\tif versionEnd == -1 {\n\t\t\t\tversionEnd = strings.Index(userAgent[versionStart:], \")\")\n\t\t\t}\n\t\t\tif versionEnd != -1 {\n\t\t\t\tversion := userAgent[versionStart : versionStart+versionEnd]\n\t\t\t\tclientInfo.Os = \"Android \" + version\n\t\t\t} else {\n\t\t\t\tclientInfo.Os = \"Android\"\n\t\t\t}\n\t\t} else {\n\t\t\tclientInfo.Os = \"Android\"\n\t\t}\n\t} else if strings.Contains(userAgent, \"windows nt 10.0\") {\n\t\tclientInfo.Os = \"Windows 10/11\"\n\t} else if strings.Contains(userAgent, \"windows nt 6.3\") {\n\t\tclientInfo.Os = \"Windows 8.1\"\n\t} else if strings.Contains(userAgent, \"windows nt 6.1\") {\n\t\tclientInfo.Os = \"Windows 7\"\n\t} else if strings.Contains(userAgent, \"windows\") {\n\t\tclientInfo.Os = \"Windows\"\n\t} else if strings.Contains(userAgent, \"mac os x\") {\n\t\t// Extract macOS version\n\t\tif idx := strings.Index(userAgent, \"mac os x \"); idx != -1 {\n\t\t\tversionStart := idx + 9\n\t\t\tversionEnd := strings.Index(userAgent[versionStart:], \";\")\n\t\t\tif versionEnd == -1 {\n\t\t\t\tversionEnd = strings.Index(userAgent[versionStart:], \")\")\n\t\t\t}\n\t\t\tif versionEnd != -1 {\n\t\t\t\tversion := strings.ReplaceAll(userAgent[versionStart:versionStart+versionEnd], \"_\", \".\")\n\t\t\t\tclientInfo.Os = \"macOS \" + version\n\t\t\t} else {\n\t\t\t\tclientInfo.Os = \"macOS\"\n\t\t\t}\n\t\t} else {\n\t\t\tclientInfo.Os = \"macOS\"\n\t\t}\n\t} else if strings.Contains(userAgent, \"linux\") {\n\t\tclientInfo.Os = \"Linux\"\n\t} else if strings.Contains(userAgent, \"cros\") {\n\t\tclientInfo.Os = \"Chrome OS\"\n\t}\n\n\t// Detect browser\n\tif strings.Contains(userAgent, \"edg/\") {\n\t\t// Extract Edge version\n\t\tif idx := strings.Index(userAgent, \"edg/\"); idx != -1 {\n\t\t\tversionStart := idx + 4\n\t\t\tversionEnd := strings.Index(userAgent[versionStart:], \" \")\n\t\t\tif versionEnd == -1 {\n\t\t\t\tversionEnd = len(userAgent) - versionStart\n\t\t\t}\n\t\t\tversion := userAgent[versionStart : versionStart+versionEnd]\n\t\t\tclientInfo.Browser = \"Edge \" + version\n\t\t} else {\n\t\t\tclientInfo.Browser = \"Edge\"\n\t\t}\n\t} else if strings.Contains(userAgent, \"chrome/\") && !strings.Contains(userAgent, \"edg\") {\n\t\t// Extract Chrome version\n\t\tif idx := strings.Index(userAgent, \"chrome/\"); idx != -1 {\n\t\t\tversionStart := idx + 7\n\t\t\tversionEnd := strings.Index(userAgent[versionStart:], \" \")\n\t\t\tif versionEnd == -1 {\n\t\t\t\tversionEnd = len(userAgent) - versionStart\n\t\t\t}\n\t\t\tversion := userAgent[versionStart : versionStart+versionEnd]\n\t\t\tclientInfo.Browser = \"Chrome \" + version\n\t\t} else {\n\t\t\tclientInfo.Browser = \"Chrome\"\n\t\t}\n\t} else if strings.Contains(userAgent, \"firefox/\") {\n\t\t// Extract Firefox version\n\t\tif idx := strings.Index(userAgent, \"firefox/\"); idx != -1 {\n\t\t\tversionStart := idx + 8\n\t\t\tversionEnd := strings.Index(userAgent[versionStart:], \" \")\n\t\t\tif versionEnd == -1 {\n\t\t\t\tversionEnd = len(userAgent) - versionStart\n\t\t\t}\n\t\t\tversion := userAgent[versionStart : versionStart+versionEnd]\n\t\t\tclientInfo.Browser = \"Firefox \" + version\n\t\t} else {\n\t\t\tclientInfo.Browser = \"Firefox\"\n\t\t}\n\t} else if strings.Contains(userAgent, \"safari/\") && !strings.Contains(userAgent, \"chrome\") && !strings.Contains(userAgent, \"edg\") {\n\t\t// Extract Safari version\n\t\tif idx := strings.Index(userAgent, \"version/\"); idx != -1 {\n\t\t\tversionStart := idx + 8\n\t\t\tversionEnd := strings.Index(userAgent[versionStart:], \" \")\n\t\t\tif versionEnd == -1 {\n\t\t\t\tversionEnd = len(userAgent) - versionStart\n\t\t\t}\n\t\t\tversion := userAgent[versionStart : versionStart+versionEnd]\n\t\t\tclientInfo.Browser = \"Safari \" + version\n\t\t} else {\n\t\t\tclientInfo.Browser = \"Safari\"\n\t\t}\n\t} else if strings.Contains(userAgent, \"opera/\") || strings.Contains(userAgent, \"opr/\") {\n\t\tclientInfo.Browser = \"Opera\"\n\t}\n}\n"
  },
  {
    "path": "server/router/api/v1/auth_service_client_info_test.go",
    "content": "package v1\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"google.golang.org/grpc/metadata\"\n\n\tstorepb \"github.com/usememos/memos/proto/gen/store\"\n)\n\nfunc TestParseUserAgent(t *testing.T) {\n\tservice := &APIV1Service{}\n\n\ttests := []struct {\n\t\tname            string\n\t\tuserAgent       string\n\t\texpectedDevice  string\n\t\texpectedOS      string\n\t\texpectedBrowser string\n\t}{\n\t\t{\n\t\t\tname:            \"Chrome on Windows\",\n\t\t\tuserAgent:       \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36\",\n\t\t\texpectedDevice:  \"desktop\",\n\t\t\texpectedOS:      \"Windows 10/11\",\n\t\t\texpectedBrowser: \"Chrome 119.0.0.0\",\n\t\t},\n\t\t{\n\t\t\tname:            \"Safari on macOS\",\n\t\t\tuserAgent:       \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15\",\n\t\t\texpectedDevice:  \"desktop\",\n\t\t\texpectedOS:      \"macOS 10.15.7\",\n\t\t\texpectedBrowser: \"Safari 17.0\",\n\t\t},\n\t\t{\n\t\t\tname:            \"Chrome on Android Mobile\",\n\t\t\tuserAgent:       \"Mozilla/5.0 (Linux; Android 13; SM-G998B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Mobile Safari/537.36\",\n\t\t\texpectedDevice:  \"mobile\",\n\t\t\texpectedOS:      \"Android 13\",\n\t\t\texpectedBrowser: \"Chrome 119.0.0.0\",\n\t\t},\n\t\t{\n\t\t\tname:            \"Safari on iPhone\",\n\t\t\tuserAgent:       \"Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1\",\n\t\t\texpectedDevice:  \"mobile\",\n\t\t\texpectedOS:      \"iOS 17.0\",\n\t\t\texpectedBrowser: \"Safari 17.0\",\n\t\t},\n\t\t{\n\t\t\tname:            \"Firefox on Windows\",\n\t\t\tuserAgent:       \"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/119.0\",\n\t\t\texpectedDevice:  \"desktop\",\n\t\t\texpectedOS:      \"Windows 10/11\",\n\t\t\texpectedBrowser: \"Firefox 119.0\",\n\t\t},\n\t\t{\n\t\t\tname:            \"Edge on Windows\",\n\t\t\tuserAgent:       \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 Edg/119.0.0.0\",\n\t\t\texpectedDevice:  \"desktop\",\n\t\t\texpectedOS:      \"Windows 10/11\",\n\t\t\texpectedBrowser: \"Edge 119.0.0.0\",\n\t\t},\n\t\t{\n\t\t\tname:            \"iPad Safari\",\n\t\t\tuserAgent:       \"Mozilla/5.0 (iPad; CPU OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1\",\n\t\t\texpectedDevice:  \"tablet\",\n\t\t\texpectedOS:      \"iOS 17.0\",\n\t\t\texpectedBrowser: \"Safari 17.0\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tclientInfo := &storepb.RefreshTokensUserSetting_ClientInfo{}\n\t\t\tservice.parseUserAgent(tt.userAgent, clientInfo)\n\n\t\t\tif clientInfo.DeviceType != tt.expectedDevice {\n\t\t\t\tt.Errorf(\"Expected device type %s, got %s\", tt.expectedDevice, clientInfo.DeviceType)\n\t\t\t}\n\t\t\tif clientInfo.Os != tt.expectedOS {\n\t\t\t\tt.Errorf(\"Expected OS %s, got %s\", tt.expectedOS, clientInfo.Os)\n\t\t\t}\n\t\t\tif clientInfo.Browser != tt.expectedBrowser {\n\t\t\t\tt.Errorf(\"Expected browser %s, got %s\", tt.expectedBrowser, clientInfo.Browser)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestExtractClientInfo(t *testing.T) {\n\tservice := &APIV1Service{}\n\n\t// Test with metadata containing user agent and IP\n\tmd := metadata.New(map[string]string{\n\t\t\"user-agent\":      \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36\",\n\t\t\"x-forwarded-for\": \"203.0.113.1, 198.51.100.1\",\n\t\t\"x-real-ip\":       \"203.0.113.1\",\n\t})\n\n\tctx := metadata.NewIncomingContext(context.Background(), md)\n\n\tclientInfo := service.extractClientInfo(ctx)\n\n\tif clientInfo.UserAgent == \"\" {\n\t\tt.Error(\"Expected user agent to be set\")\n\t}\n\tif clientInfo.IpAddress != \"203.0.113.1\" {\n\t\tt.Errorf(\"Expected IP address to be 203.0.113.1, got %s\", clientInfo.IpAddress)\n\t}\n\tif clientInfo.DeviceType != \"desktop\" {\n\t\tt.Errorf(\"Expected device type to be desktop, got %s\", clientInfo.DeviceType)\n\t}\n\tif clientInfo.Os != \"Windows 10/11\" {\n\t\tt.Errorf(\"Expected OS to be Windows 10/11, got %s\", clientInfo.Os)\n\t}\n\tif clientInfo.Browser != \"Chrome 119.0.0.0\" {\n\t\tt.Errorf(\"Expected browser to be Chrome 119.0.0.0, got %s\", clientInfo.Browser)\n\t}\n}\n\n// TestClientInfoExamples demonstrates the enhanced client info extraction with various user agents.\nfunc TestClientInfoExamples(t *testing.T) {\n\tservice := &APIV1Service{}\n\n\texamples := []struct {\n\t\tdescription string\n\t\tuserAgent   string\n\t}{\n\t\t{\n\t\t\tdescription: \"Modern Chrome on Windows 11\",\n\t\t\tuserAgent:   \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"Safari on iPhone 15 Pro\",\n\t\t\tuserAgent:   \"Mozilla/5.0 (iPhone; CPU iPhone OS 17_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Mobile/15E148 Safari/604.1\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"Chrome on Samsung Galaxy\",\n\t\t\tuserAgent:   \"Mozilla/5.0 (Linux; Android 14; SM-S918B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"Firefox on Ubuntu\",\n\t\t\tuserAgent:   \"Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/120.0\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"Edge on Windows 10\",\n\t\t\tuserAgent:   \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"Safari on iPad Air\",\n\t\t\tuserAgent:   \"Mozilla/5.0 (iPad; CPU OS 17_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Mobile/15E148 Safari/604.1\",\n\t\t},\n\t}\n\n\tfor _, example := range examples {\n\t\tt.Run(example.description, func(t *testing.T) {\n\t\t\tclientInfo := &storepb.RefreshTokensUserSetting_ClientInfo{}\n\t\t\tservice.parseUserAgent(example.userAgent, clientInfo)\n\n\t\t\tt.Logf(\"User Agent: %s\", example.userAgent)\n\t\t\tt.Logf(\"Device Type: %s\", clientInfo.DeviceType)\n\t\t\tt.Logf(\"Operating System: %s\", clientInfo.Os)\n\t\t\tt.Logf(\"Browser: %s\", clientInfo.Browser)\n\t\t\tt.Log(\"---\")\n\n\t\t\t// Ensure all fields are populated\n\t\t\tif clientInfo.DeviceType == \"\" {\n\t\t\t\tt.Error(\"Device type should not be empty\")\n\t\t\t}\n\t\t\tif clientInfo.Os == \"\" {\n\t\t\t\tt.Error(\"OS should not be empty\")\n\t\t\t}\n\t\t\tif clientInfo.Browser == \"\" {\n\t\t\t\tt.Error(\"Browser should not be empty\")\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "server/router/api/v1/common.go",
    "content": "package v1\n\nimport (\n\t\"encoding/base64\"\n\n\t\"github.com/pkg/errors\"\n\t\"google.golang.org/protobuf/proto\"\n\n\tv1pb \"github.com/usememos/memos/proto/gen/api/v1\"\n\t\"github.com/usememos/memos/store\"\n)\n\nconst (\n\t// DefaultPageSize is the default page size for requests.\n\tDefaultPageSize = 10\n\t// MaxPageSize is the maximum page size for requests.\n\tMaxPageSize = 1000\n)\n\nfunc convertStateFromStore(rowStatus store.RowStatus) v1pb.State {\n\tswitch rowStatus {\n\tcase store.Normal:\n\t\treturn v1pb.State_NORMAL\n\tcase store.Archived:\n\t\treturn v1pb.State_ARCHIVED\n\tdefault:\n\t\treturn v1pb.State_STATE_UNSPECIFIED\n\t}\n}\n\nfunc convertStateToStore(state v1pb.State) store.RowStatus {\n\tswitch state {\n\tcase v1pb.State_ARCHIVED:\n\t\treturn store.Archived\n\tdefault:\n\t\treturn store.Normal\n\t}\n}\n\nfunc getPageToken(limit int, offset int) (string, error) {\n\treturn marshalPageToken(&v1pb.PageToken{\n\t\tLimit:  int32(limit),\n\t\tOffset: int32(offset),\n\t})\n}\n\nfunc marshalPageToken(pageToken *v1pb.PageToken) (string, error) {\n\tb, err := proto.Marshal(pageToken)\n\tif err != nil {\n\t\treturn \"\", errors.Wrapf(err, \"failed to marshal page token\")\n\t}\n\treturn base64.StdEncoding.EncodeToString(b), nil\n}\n\nfunc unmarshalPageToken(s string, pageToken *v1pb.PageToken) error {\n\tb, err := base64.StdEncoding.DecodeString(s)\n\tif err != nil {\n\t\treturn errors.Wrapf(err, \"failed to decode page token\")\n\t}\n\tif err := proto.Unmarshal(b, pageToken); err != nil {\n\t\treturn errors.Wrapf(err, \"failed to unmarshal page token\")\n\t}\n\treturn nil\n}\n\nfunc isSuperUser(user *store.User) bool {\n\treturn user.Role == store.RoleAdmin\n}\n"
  },
  {
    "path": "server/router/api/v1/connect_handler.go",
    "content": "package v1\n\nimport (\n\t\"net/http\"\n\n\t\"connectrpc.com/connect\"\n\t\"google.golang.org/grpc/codes\"\n\t\"google.golang.org/grpc/status\"\n\n\t\"github.com/usememos/memos/proto/gen/api/v1/apiv1connect\"\n)\n\n// ConnectServiceHandler wraps APIV1Service to implement Connect handler interfaces.\n// It adapts the existing gRPC service implementations to work with Connect's\n// request/response wrapper types.\n//\n// This wrapper pattern allows us to:\n// - Reuse existing gRPC service implementations\n// - Support both native gRPC and Connect protocols\n// - Maintain a single source of truth for business logic.\ntype ConnectServiceHandler struct {\n\t*APIV1Service\n}\n\n// NewConnectServiceHandler creates a new Connect service handler.\nfunc NewConnectServiceHandler(svc *APIV1Service) *ConnectServiceHandler {\n\treturn &ConnectServiceHandler{APIV1Service: svc}\n}\n\n// RegisterConnectHandlers registers all Connect service handlers on the given mux.\nfunc (s *ConnectServiceHandler) RegisterConnectHandlers(mux *http.ServeMux, opts ...connect.HandlerOption) {\n\t// Register all service handlers\n\thandlers := []struct {\n\t\tpath    string\n\t\thandler http.Handler\n\t}{\n\t\twrap(apiv1connect.NewInstanceServiceHandler(s, opts...)),\n\t\twrap(apiv1connect.NewAuthServiceHandler(s, opts...)),\n\t\twrap(apiv1connect.NewUserServiceHandler(s, opts...)),\n\t\twrap(apiv1connect.NewMemoServiceHandler(s, opts...)),\n\t\twrap(apiv1connect.NewAttachmentServiceHandler(s, opts...)),\n\t\twrap(apiv1connect.NewShortcutServiceHandler(s, opts...)),\n\t\twrap(apiv1connect.NewIdentityProviderServiceHandler(s, opts...)),\n\t}\n\n\tfor _, h := range handlers {\n\t\tmux.Handle(h.path, h.handler)\n\t}\n}\n\n// wrap converts (path, handler) return value to a struct for cleaner iteration.\nfunc wrap(path string, handler http.Handler) struct {\n\tpath    string\n\thandler http.Handler\n} {\n\treturn struct {\n\t\tpath    string\n\t\thandler http.Handler\n\t}{path, handler}\n}\n\n// convertGRPCError converts gRPC status errors to Connect errors.\n// This preserves the error code semantics between the two protocols.\nfunc convertGRPCError(err error) error {\n\tif err == nil {\n\t\treturn nil\n\t}\n\tif st, ok := status.FromError(err); ok {\n\t\treturn connect.NewError(grpcCodeToConnectCode(st.Code()), err)\n\t}\n\treturn connect.NewError(connect.CodeInternal, err)\n}\n\n// grpcCodeToConnectCode converts gRPC status codes to Connect error codes.\n// gRPC and Connect use the same error code semantics, so this is a direct cast.\n// See: https://connectrpc.com/docs/protocol/#error-codes\nfunc grpcCodeToConnectCode(code codes.Code) connect.Code {\n\treturn connect.Code(code)\n}\n"
  },
  {
    "path": "server/router/api/v1/connect_interceptors.go",
    "content": "package v1\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"reflect\"\n\t\"runtime/debug\"\n\n\t\"connectrpc.com/connect\"\n\tpkgerrors \"github.com/pkg/errors\"\n\t\"google.golang.org/grpc/metadata\"\n\n\t\"github.com/usememos/memos/server/auth\"\n\t\"github.com/usememos/memos/store\"\n)\n\n// MetadataInterceptor converts Connect HTTP headers to gRPC metadata.\n//\n// This ensures service methods can use metadata.FromIncomingContext() to access\n// headers like User-Agent, X-Forwarded-For, etc., regardless of whether the\n// request came via Connect RPC or gRPC-Gateway.\ntype MetadataInterceptor struct{}\n\n// NewMetadataInterceptor creates a new metadata interceptor.\nfunc NewMetadataInterceptor() *MetadataInterceptor {\n\treturn &MetadataInterceptor{}\n}\n\nfunc (*MetadataInterceptor) WrapUnary(next connect.UnaryFunc) connect.UnaryFunc {\n\treturn func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) {\n\t\t// Convert HTTP headers to gRPC metadata\n\t\theader := req.Header()\n\t\tmd := metadata.MD{}\n\n\t\t// Copy important headers for client info extraction\n\t\tif ua := header.Get(\"User-Agent\"); ua != \"\" {\n\t\t\tmd.Set(\"user-agent\", ua)\n\t\t}\n\t\tif xff := header.Get(\"X-Forwarded-For\"); xff != \"\" {\n\t\t\tmd.Set(\"x-forwarded-for\", xff)\n\t\t}\n\t\tif xri := header.Get(\"X-Real-Ip\"); xri != \"\" {\n\t\t\tmd.Set(\"x-real-ip\", xri)\n\t\t}\n\t\t// Forward Cookie header for authentication methods that need it (e.g., RefreshToken)\n\t\tif cookie := header.Get(\"Cookie\"); cookie != \"\" {\n\t\t\tmd.Set(\"cookie\", cookie)\n\t\t}\n\n\t\t// Set metadata in context so services can use metadata.FromIncomingContext()\n\t\tctx = metadata.NewIncomingContext(ctx, md)\n\n\t\t// Execute the request\n\t\tresp, err := next(ctx, req)\n\n\t\t// Prevent browser caching of API responses to avoid stale data issues\n\t\t// See: https://github.com/usememos/memos/issues/5470\n\t\tif !isNilAnyResponse(resp) && resp.Header() != nil {\n\t\t\tresp.Header().Set(\"Cache-Control\", \"no-cache, no-store, must-revalidate\")\n\t\t\tresp.Header().Set(\"Pragma\", \"no-cache\")\n\t\t\tresp.Header().Set(\"Expires\", \"0\")\n\t\t}\n\n\t\treturn resp, err\n\t}\n}\n\nfunc isNilAnyResponse(resp connect.AnyResponse) bool {\n\tif resp == nil {\n\t\treturn true\n\t}\n\tval := reflect.ValueOf(resp)\n\treturn val.Kind() == reflect.Ptr && val.IsNil()\n}\n\nfunc (*MetadataInterceptor) WrapStreamingClient(next connect.StreamingClientFunc) connect.StreamingClientFunc {\n\treturn next\n}\n\nfunc (*MetadataInterceptor) WrapStreamingHandler(next connect.StreamingHandlerFunc) connect.StreamingHandlerFunc {\n\treturn next\n}\n\n// LoggingInterceptor logs Connect RPC requests with appropriate log levels.\n//\n// Log levels:\n// - INFO: Successful requests and expected client errors (not found, permission denied, etc.)\n// - ERROR: Server errors (internal, unavailable, etc.)\ntype LoggingInterceptor struct {\n\tlogStacktrace bool\n}\n\n// NewLoggingInterceptor creates a new logging interceptor.\nfunc NewLoggingInterceptor(logStacktrace bool) *LoggingInterceptor {\n\treturn &LoggingInterceptor{logStacktrace: logStacktrace}\n}\n\nfunc (in *LoggingInterceptor) WrapUnary(next connect.UnaryFunc) connect.UnaryFunc {\n\treturn func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) {\n\t\tresp, err := next(ctx, req)\n\t\tin.log(req.Spec().Procedure, err)\n\t\treturn resp, err\n\t}\n}\n\nfunc (*LoggingInterceptor) WrapStreamingClient(next connect.StreamingClientFunc) connect.StreamingClientFunc {\n\treturn next // No-op for server-side interceptor\n}\n\nfunc (*LoggingInterceptor) WrapStreamingHandler(next connect.StreamingHandlerFunc) connect.StreamingHandlerFunc {\n\treturn next // Streaming not used in this service\n}\n\nfunc (in *LoggingInterceptor) log(procedure string, err error) {\n\tlevel, msg := in.classifyError(err)\n\tattrs := []slog.Attr{slog.String(\"method\", procedure)}\n\tif err != nil {\n\t\tattrs = append(attrs, slog.String(\"error\", err.Error()))\n\t\tif in.logStacktrace {\n\t\t\tattrs = append(attrs, slog.String(\"stacktrace\", fmt.Sprintf(\"%+v\", err)))\n\t\t}\n\t}\n\tslog.LogAttrs(context.Background(), level, msg, attrs...)\n}\n\nfunc (*LoggingInterceptor) classifyError(err error) (slog.Level, string) {\n\tif err == nil {\n\t\treturn slog.LevelInfo, \"OK\"\n\t}\n\n\tvar connectErr *connect.Error\n\tif !pkgerrors.As(err, &connectErr) {\n\t\treturn slog.LevelError, \"unknown error\"\n\t}\n\n\t// Client errors (expected, log at INFO)\n\tswitch connectErr.Code() {\n\tcase connect.CodeCanceled,\n\t\tconnect.CodeInvalidArgument,\n\t\tconnect.CodeNotFound,\n\t\tconnect.CodeAlreadyExists,\n\t\tconnect.CodePermissionDenied,\n\t\tconnect.CodeUnauthenticated,\n\t\tconnect.CodeResourceExhausted,\n\t\tconnect.CodeFailedPrecondition,\n\t\tconnect.CodeAborted,\n\t\tconnect.CodeOutOfRange:\n\t\treturn slog.LevelInfo, \"client error\"\n\tdefault:\n\t\t// Server errors\n\t\treturn slog.LevelError, \"server error\"\n\t}\n}\n\n// RecoveryInterceptor recovers from panics in Connect handlers and returns an internal error.\ntype RecoveryInterceptor struct {\n\tlogStacktrace bool\n}\n\n// NewRecoveryInterceptor creates a new recovery interceptor.\nfunc NewRecoveryInterceptor(logStacktrace bool) *RecoveryInterceptor {\n\treturn &RecoveryInterceptor{logStacktrace: logStacktrace}\n}\n\nfunc (in *RecoveryInterceptor) WrapUnary(next connect.UnaryFunc) connect.UnaryFunc {\n\treturn func(ctx context.Context, req connect.AnyRequest) (resp connect.AnyResponse, err error) {\n\t\tdefer func() {\n\t\t\tif r := recover(); r != nil {\n\t\t\t\tin.logPanic(req.Spec().Procedure, r)\n\t\t\t\terr = connect.NewError(connect.CodeInternal, pkgerrors.New(\"internal server error\"))\n\t\t\t}\n\t\t}()\n\t\treturn next(ctx, req)\n\t}\n}\n\nfunc (*RecoveryInterceptor) WrapStreamingClient(next connect.StreamingClientFunc) connect.StreamingClientFunc {\n\treturn next\n}\n\nfunc (*RecoveryInterceptor) WrapStreamingHandler(next connect.StreamingHandlerFunc) connect.StreamingHandlerFunc {\n\treturn next\n}\n\nfunc (in *RecoveryInterceptor) logPanic(procedure string, panicValue any) {\n\tattrs := []slog.Attr{\n\t\tslog.String(\"method\", procedure),\n\t\tslog.Any(\"panic\", panicValue),\n\t}\n\tif in.logStacktrace {\n\t\tattrs = append(attrs, slog.String(\"stacktrace\", string(debug.Stack())))\n\t}\n\tslog.LogAttrs(context.Background(), slog.LevelError, \"panic recovered in Connect handler\", attrs...)\n}\n\n// AuthInterceptor handles authentication for Connect handlers.\n//\n// It enforces authentication for all endpoints except those listed in PublicMethods.\n// Role-based authorization (admin checks) remains in the service layer.\ntype AuthInterceptor struct {\n\tauthenticator *auth.Authenticator\n}\n\n// NewAuthInterceptor creates a new auth interceptor.\nfunc NewAuthInterceptor(store *store.Store, secret string) *AuthInterceptor {\n\treturn &AuthInterceptor{\n\t\tauthenticator: auth.NewAuthenticator(store, secret),\n\t}\n}\n\nfunc (in *AuthInterceptor) WrapUnary(next connect.UnaryFunc) connect.UnaryFunc {\n\treturn func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) {\n\t\theader := req.Header()\n\t\tauthHeader := header.Get(\"Authorization\")\n\n\t\tresult := in.authenticator.Authenticate(ctx, authHeader)\n\n\t\t// Enforce authentication for non-public methods\n\t\tif result == nil && !IsPublicMethod(req.Spec().Procedure) {\n\t\t\treturn nil, connect.NewError(connect.CodeUnauthenticated, errors.New(\"authentication required\"))\n\t\t}\n\n\t\tctx = auth.ApplyToContext(ctx, result)\n\n\t\treturn next(ctx, req)\n\t}\n}\n\nfunc (*AuthInterceptor) WrapStreamingClient(next connect.StreamingClientFunc) connect.StreamingClientFunc {\n\treturn next\n}\n\nfunc (*AuthInterceptor) WrapStreamingHandler(next connect.StreamingHandlerFunc) connect.StreamingHandlerFunc {\n\treturn next\n}\n"
  },
  {
    "path": "server/router/api/v1/connect_services.go",
    "content": "package v1\n\nimport (\n\t\"context\"\n\n\t\"connectrpc.com/connect\"\n\t\"google.golang.org/protobuf/types/known/emptypb\"\n\n\tv1pb \"github.com/usememos/memos/proto/gen/api/v1\"\n)\n\n// This file contains all Connect service handler method implementations.\n// Each method delegates to the underlying gRPC service implementation,\n// converting between Connect and gRPC request/response types.\n\n// InstanceService\n\nfunc (s *ConnectServiceHandler) GetInstanceProfile(ctx context.Context, req *connect.Request[v1pb.GetInstanceProfileRequest]) (*connect.Response[v1pb.InstanceProfile], error) {\n\tresp, err := s.APIV1Service.GetInstanceProfile(ctx, req.Msg)\n\tif err != nil {\n\t\treturn nil, convertGRPCError(err)\n\t}\n\treturn connect.NewResponse(resp), nil\n}\n\nfunc (s *ConnectServiceHandler) GetInstanceSetting(ctx context.Context, req *connect.Request[v1pb.GetInstanceSettingRequest]) (*connect.Response[v1pb.InstanceSetting], error) {\n\tresp, err := s.APIV1Service.GetInstanceSetting(ctx, req.Msg)\n\tif err != nil {\n\t\treturn nil, convertGRPCError(err)\n\t}\n\treturn connect.NewResponse(resp), nil\n}\n\nfunc (s *ConnectServiceHandler) UpdateInstanceSetting(ctx context.Context, req *connect.Request[v1pb.UpdateInstanceSettingRequest]) (*connect.Response[v1pb.InstanceSetting], error) {\n\tresp, err := s.APIV1Service.UpdateInstanceSetting(ctx, req.Msg)\n\tif err != nil {\n\t\treturn nil, convertGRPCError(err)\n\t}\n\treturn connect.NewResponse(resp), nil\n}\n\n// AuthService\n//\n// Auth service methods need special handling for response headers (cookies).\n// We use connectWithHeaderCarrier helper to inject a header carrier into the context,\n// which allows the service to set headers in a protocol-agnostic way.\n\nfunc (s *ConnectServiceHandler) GetCurrentUser(ctx context.Context, req *connect.Request[v1pb.GetCurrentUserRequest]) (*connect.Response[v1pb.GetCurrentUserResponse], error) {\n\treturn connectWithHeaderCarrier(ctx, func(ctx context.Context) (*v1pb.GetCurrentUserResponse, error) {\n\t\treturn s.APIV1Service.GetCurrentUser(ctx, req.Msg)\n\t})\n}\n\nfunc (s *ConnectServiceHandler) SignIn(ctx context.Context, req *connect.Request[v1pb.SignInRequest]) (*connect.Response[v1pb.SignInResponse], error) {\n\treturn connectWithHeaderCarrier(ctx, func(ctx context.Context) (*v1pb.SignInResponse, error) {\n\t\treturn s.APIV1Service.SignIn(ctx, req.Msg)\n\t})\n}\n\nfunc (s *ConnectServiceHandler) SignOut(ctx context.Context, req *connect.Request[v1pb.SignOutRequest]) (*connect.Response[emptypb.Empty], error) {\n\treturn connectWithHeaderCarrier(ctx, func(ctx context.Context) (*emptypb.Empty, error) {\n\t\treturn s.APIV1Service.SignOut(ctx, req.Msg)\n\t})\n}\n\nfunc (s *ConnectServiceHandler) RefreshToken(ctx context.Context, req *connect.Request[v1pb.RefreshTokenRequest]) (*connect.Response[v1pb.RefreshTokenResponse], error) {\n\treturn connectWithHeaderCarrier(ctx, func(ctx context.Context) (*v1pb.RefreshTokenResponse, error) {\n\t\treturn s.APIV1Service.RefreshToken(ctx, req.Msg)\n\t})\n}\n\n// UserService\n\nfunc (s *ConnectServiceHandler) ListUsers(ctx context.Context, req *connect.Request[v1pb.ListUsersRequest]) (*connect.Response[v1pb.ListUsersResponse], error) {\n\tresp, err := s.APIV1Service.ListUsers(ctx, req.Msg)\n\tif err != nil {\n\t\treturn nil, convertGRPCError(err)\n\t}\n\treturn connect.NewResponse(resp), nil\n}\n\nfunc (s *ConnectServiceHandler) GetUser(ctx context.Context, req *connect.Request[v1pb.GetUserRequest]) (*connect.Response[v1pb.User], error) {\n\tresp, err := s.APIV1Service.GetUser(ctx, req.Msg)\n\tif err != nil {\n\t\treturn nil, convertGRPCError(err)\n\t}\n\treturn connect.NewResponse(resp), nil\n}\n\nfunc (s *ConnectServiceHandler) CreateUser(ctx context.Context, req *connect.Request[v1pb.CreateUserRequest]) (*connect.Response[v1pb.User], error) {\n\tresp, err := s.APIV1Service.CreateUser(ctx, req.Msg)\n\tif err != nil {\n\t\treturn nil, convertGRPCError(err)\n\t}\n\treturn connect.NewResponse(resp), nil\n}\n\nfunc (s *ConnectServiceHandler) UpdateUser(ctx context.Context, req *connect.Request[v1pb.UpdateUserRequest]) (*connect.Response[v1pb.User], error) {\n\tresp, err := s.APIV1Service.UpdateUser(ctx, req.Msg)\n\tif err != nil {\n\t\treturn nil, convertGRPCError(err)\n\t}\n\treturn connect.NewResponse(resp), nil\n}\n\nfunc (s *ConnectServiceHandler) DeleteUser(ctx context.Context, req *connect.Request[v1pb.DeleteUserRequest]) (*connect.Response[emptypb.Empty], error) {\n\tresp, err := s.APIV1Service.DeleteUser(ctx, req.Msg)\n\tif err != nil {\n\t\treturn nil, convertGRPCError(err)\n\t}\n\treturn connect.NewResponse(resp), nil\n}\n\nfunc (s *ConnectServiceHandler) ListAllUserStats(ctx context.Context, req *connect.Request[v1pb.ListAllUserStatsRequest]) (*connect.Response[v1pb.ListAllUserStatsResponse], error) {\n\tresp, err := s.APIV1Service.ListAllUserStats(ctx, req.Msg)\n\tif err != nil {\n\t\treturn nil, convertGRPCError(err)\n\t}\n\treturn connect.NewResponse(resp), nil\n}\n\nfunc (s *ConnectServiceHandler) GetUserStats(ctx context.Context, req *connect.Request[v1pb.GetUserStatsRequest]) (*connect.Response[v1pb.UserStats], error) {\n\tresp, err := s.APIV1Service.GetUserStats(ctx, req.Msg)\n\tif err != nil {\n\t\treturn nil, convertGRPCError(err)\n\t}\n\treturn connect.NewResponse(resp), nil\n}\n\nfunc (s *ConnectServiceHandler) GetUserSetting(ctx context.Context, req *connect.Request[v1pb.GetUserSettingRequest]) (*connect.Response[v1pb.UserSetting], error) {\n\tresp, err := s.APIV1Service.GetUserSetting(ctx, req.Msg)\n\tif err != nil {\n\t\treturn nil, convertGRPCError(err)\n\t}\n\treturn connect.NewResponse(resp), nil\n}\n\nfunc (s *ConnectServiceHandler) UpdateUserSetting(ctx context.Context, req *connect.Request[v1pb.UpdateUserSettingRequest]) (*connect.Response[v1pb.UserSetting], error) {\n\tresp, err := s.APIV1Service.UpdateUserSetting(ctx, req.Msg)\n\tif err != nil {\n\t\treturn nil, convertGRPCError(err)\n\t}\n\treturn connect.NewResponse(resp), nil\n}\n\nfunc (s *ConnectServiceHandler) ListUserSettings(ctx context.Context, req *connect.Request[v1pb.ListUserSettingsRequest]) (*connect.Response[v1pb.ListUserSettingsResponse], error) {\n\tresp, err := s.APIV1Service.ListUserSettings(ctx, req.Msg)\n\tif err != nil {\n\t\treturn nil, convertGRPCError(err)\n\t}\n\treturn connect.NewResponse(resp), nil\n}\n\nfunc (s *ConnectServiceHandler) ListPersonalAccessTokens(ctx context.Context, req *connect.Request[v1pb.ListPersonalAccessTokensRequest]) (*connect.Response[v1pb.ListPersonalAccessTokensResponse], error) {\n\tresp, err := s.APIV1Service.ListPersonalAccessTokens(ctx, req.Msg)\n\tif err != nil {\n\t\treturn nil, convertGRPCError(err)\n\t}\n\treturn connect.NewResponse(resp), nil\n}\n\nfunc (s *ConnectServiceHandler) CreatePersonalAccessToken(ctx context.Context, req *connect.Request[v1pb.CreatePersonalAccessTokenRequest]) (*connect.Response[v1pb.CreatePersonalAccessTokenResponse], error) {\n\tresp, err := s.APIV1Service.CreatePersonalAccessToken(ctx, req.Msg)\n\tif err != nil {\n\t\treturn nil, convertGRPCError(err)\n\t}\n\treturn connect.NewResponse(resp), nil\n}\n\nfunc (s *ConnectServiceHandler) DeletePersonalAccessToken(ctx context.Context, req *connect.Request[v1pb.DeletePersonalAccessTokenRequest]) (*connect.Response[emptypb.Empty], error) {\n\tresp, err := s.APIV1Service.DeletePersonalAccessToken(ctx, req.Msg)\n\tif err != nil {\n\t\treturn nil, convertGRPCError(err)\n\t}\n\treturn connect.NewResponse(resp), nil\n}\n\nfunc (s *ConnectServiceHandler) ListUserWebhooks(ctx context.Context, req *connect.Request[v1pb.ListUserWebhooksRequest]) (*connect.Response[v1pb.ListUserWebhooksResponse], error) {\n\tresp, err := s.APIV1Service.ListUserWebhooks(ctx, req.Msg)\n\tif err != nil {\n\t\treturn nil, convertGRPCError(err)\n\t}\n\treturn connect.NewResponse(resp), nil\n}\n\nfunc (s *ConnectServiceHandler) CreateUserWebhook(ctx context.Context, req *connect.Request[v1pb.CreateUserWebhookRequest]) (*connect.Response[v1pb.UserWebhook], error) {\n\tresp, err := s.APIV1Service.CreateUserWebhook(ctx, req.Msg)\n\tif err != nil {\n\t\treturn nil, convertGRPCError(err)\n\t}\n\treturn connect.NewResponse(resp), nil\n}\n\nfunc (s *ConnectServiceHandler) UpdateUserWebhook(ctx context.Context, req *connect.Request[v1pb.UpdateUserWebhookRequest]) (*connect.Response[v1pb.UserWebhook], error) {\n\tresp, err := s.APIV1Service.UpdateUserWebhook(ctx, req.Msg)\n\tif err != nil {\n\t\treturn nil, convertGRPCError(err)\n\t}\n\treturn connect.NewResponse(resp), nil\n}\n\nfunc (s *ConnectServiceHandler) DeleteUserWebhook(ctx context.Context, req *connect.Request[v1pb.DeleteUserWebhookRequest]) (*connect.Response[emptypb.Empty], error) {\n\tresp, err := s.APIV1Service.DeleteUserWebhook(ctx, req.Msg)\n\tif err != nil {\n\t\treturn nil, convertGRPCError(err)\n\t}\n\treturn connect.NewResponse(resp), nil\n}\n\nfunc (s *ConnectServiceHandler) ListUserNotifications(ctx context.Context, req *connect.Request[v1pb.ListUserNotificationsRequest]) (*connect.Response[v1pb.ListUserNotificationsResponse], error) {\n\tresp, err := s.APIV1Service.ListUserNotifications(ctx, req.Msg)\n\tif err != nil {\n\t\treturn nil, convertGRPCError(err)\n\t}\n\treturn connect.NewResponse(resp), nil\n}\n\nfunc (s *ConnectServiceHandler) UpdateUserNotification(ctx context.Context, req *connect.Request[v1pb.UpdateUserNotificationRequest]) (*connect.Response[v1pb.UserNotification], error) {\n\tresp, err := s.APIV1Service.UpdateUserNotification(ctx, req.Msg)\n\tif err != nil {\n\t\treturn nil, convertGRPCError(err)\n\t}\n\treturn connect.NewResponse(resp), nil\n}\n\nfunc (s *ConnectServiceHandler) DeleteUserNotification(ctx context.Context, req *connect.Request[v1pb.DeleteUserNotificationRequest]) (*connect.Response[emptypb.Empty], error) {\n\tresp, err := s.APIV1Service.DeleteUserNotification(ctx, req.Msg)\n\tif err != nil {\n\t\treturn nil, convertGRPCError(err)\n\t}\n\treturn connect.NewResponse(resp), nil\n}\n\n// MemoService\n\nfunc (s *ConnectServiceHandler) CreateMemo(ctx context.Context, req *connect.Request[v1pb.CreateMemoRequest]) (*connect.Response[v1pb.Memo], error) {\n\tresp, err := s.APIV1Service.CreateMemo(ctx, req.Msg)\n\tif err != nil {\n\t\treturn nil, convertGRPCError(err)\n\t}\n\treturn connect.NewResponse(resp), nil\n}\n\nfunc (s *ConnectServiceHandler) ListMemos(ctx context.Context, req *connect.Request[v1pb.ListMemosRequest]) (*connect.Response[v1pb.ListMemosResponse], error) {\n\tresp, err := s.APIV1Service.ListMemos(ctx, req.Msg)\n\tif err != nil {\n\t\treturn nil, convertGRPCError(err)\n\t}\n\treturn connect.NewResponse(resp), nil\n}\n\nfunc (s *ConnectServiceHandler) GetMemo(ctx context.Context, req *connect.Request[v1pb.GetMemoRequest]) (*connect.Response[v1pb.Memo], error) {\n\tresp, err := s.APIV1Service.GetMemo(ctx, req.Msg)\n\tif err != nil {\n\t\treturn nil, convertGRPCError(err)\n\t}\n\treturn connect.NewResponse(resp), nil\n}\n\nfunc (s *ConnectServiceHandler) UpdateMemo(ctx context.Context, req *connect.Request[v1pb.UpdateMemoRequest]) (*connect.Response[v1pb.Memo], error) {\n\tresp, err := s.APIV1Service.UpdateMemo(ctx, req.Msg)\n\tif err != nil {\n\t\treturn nil, convertGRPCError(err)\n\t}\n\treturn connect.NewResponse(resp), nil\n}\n\nfunc (s *ConnectServiceHandler) DeleteMemo(ctx context.Context, req *connect.Request[v1pb.DeleteMemoRequest]) (*connect.Response[emptypb.Empty], error) {\n\tresp, err := s.APIV1Service.DeleteMemo(ctx, req.Msg)\n\tif err != nil {\n\t\treturn nil, convertGRPCError(err)\n\t}\n\treturn connect.NewResponse(resp), nil\n}\n\nfunc (s *ConnectServiceHandler) SetMemoAttachments(ctx context.Context, req *connect.Request[v1pb.SetMemoAttachmentsRequest]) (*connect.Response[emptypb.Empty], error) {\n\tresp, err := s.APIV1Service.SetMemoAttachments(ctx, req.Msg)\n\tif err != nil {\n\t\treturn nil, convertGRPCError(err)\n\t}\n\treturn connect.NewResponse(resp), nil\n}\n\nfunc (s *ConnectServiceHandler) ListMemoAttachments(ctx context.Context, req *connect.Request[v1pb.ListMemoAttachmentsRequest]) (*connect.Response[v1pb.ListMemoAttachmentsResponse], error) {\n\tresp, err := s.APIV1Service.ListMemoAttachments(ctx, req.Msg)\n\tif err != nil {\n\t\treturn nil, convertGRPCError(err)\n\t}\n\treturn connect.NewResponse(resp), nil\n}\n\nfunc (s *ConnectServiceHandler) SetMemoRelations(ctx context.Context, req *connect.Request[v1pb.SetMemoRelationsRequest]) (*connect.Response[emptypb.Empty], error) {\n\tresp, err := s.APIV1Service.SetMemoRelations(ctx, req.Msg)\n\tif err != nil {\n\t\treturn nil, convertGRPCError(err)\n\t}\n\treturn connect.NewResponse(resp), nil\n}\n\nfunc (s *ConnectServiceHandler) ListMemoRelations(ctx context.Context, req *connect.Request[v1pb.ListMemoRelationsRequest]) (*connect.Response[v1pb.ListMemoRelationsResponse], error) {\n\tresp, err := s.APIV1Service.ListMemoRelations(ctx, req.Msg)\n\tif err != nil {\n\t\treturn nil, convertGRPCError(err)\n\t}\n\treturn connect.NewResponse(resp), nil\n}\n\nfunc (s *ConnectServiceHandler) CreateMemoComment(ctx context.Context, req *connect.Request[v1pb.CreateMemoCommentRequest]) (*connect.Response[v1pb.Memo], error) {\n\tresp, err := s.APIV1Service.CreateMemoComment(ctx, req.Msg)\n\tif err != nil {\n\t\treturn nil, convertGRPCError(err)\n\t}\n\treturn connect.NewResponse(resp), nil\n}\n\nfunc (s *ConnectServiceHandler) ListMemoComments(ctx context.Context, req *connect.Request[v1pb.ListMemoCommentsRequest]) (*connect.Response[v1pb.ListMemoCommentsResponse], error) {\n\tresp, err := s.APIV1Service.ListMemoComments(ctx, req.Msg)\n\tif err != nil {\n\t\treturn nil, convertGRPCError(err)\n\t}\n\treturn connect.NewResponse(resp), nil\n}\n\nfunc (s *ConnectServiceHandler) ListMemoReactions(ctx context.Context, req *connect.Request[v1pb.ListMemoReactionsRequest]) (*connect.Response[v1pb.ListMemoReactionsResponse], error) {\n\tresp, err := s.APIV1Service.ListMemoReactions(ctx, req.Msg)\n\tif err != nil {\n\t\treturn nil, convertGRPCError(err)\n\t}\n\treturn connect.NewResponse(resp), nil\n}\n\nfunc (s *ConnectServiceHandler) UpsertMemoReaction(ctx context.Context, req *connect.Request[v1pb.UpsertMemoReactionRequest]) (*connect.Response[v1pb.Reaction], error) {\n\tresp, err := s.APIV1Service.UpsertMemoReaction(ctx, req.Msg)\n\tif err != nil {\n\t\treturn nil, convertGRPCError(err)\n\t}\n\treturn connect.NewResponse(resp), nil\n}\n\nfunc (s *ConnectServiceHandler) DeleteMemoReaction(ctx context.Context, req *connect.Request[v1pb.DeleteMemoReactionRequest]) (*connect.Response[emptypb.Empty], error) {\n\tresp, err := s.APIV1Service.DeleteMemoReaction(ctx, req.Msg)\n\tif err != nil {\n\t\treturn nil, convertGRPCError(err)\n\t}\n\treturn connect.NewResponse(resp), nil\n}\n\nfunc (s *ConnectServiceHandler) CreateMemoShare(ctx context.Context, req *connect.Request[v1pb.CreateMemoShareRequest]) (*connect.Response[v1pb.MemoShare], error) {\n\tresp, err := s.APIV1Service.CreateMemoShare(ctx, req.Msg)\n\tif err != nil {\n\t\treturn nil, convertGRPCError(err)\n\t}\n\treturn connect.NewResponse(resp), nil\n}\n\nfunc (s *ConnectServiceHandler) ListMemoShares(ctx context.Context, req *connect.Request[v1pb.ListMemoSharesRequest]) (*connect.Response[v1pb.ListMemoSharesResponse], error) {\n\tresp, err := s.APIV1Service.ListMemoShares(ctx, req.Msg)\n\tif err != nil {\n\t\treturn nil, convertGRPCError(err)\n\t}\n\treturn connect.NewResponse(resp), nil\n}\n\nfunc (s *ConnectServiceHandler) DeleteMemoShare(ctx context.Context, req *connect.Request[v1pb.DeleteMemoShareRequest]) (*connect.Response[emptypb.Empty], error) {\n\tresp, err := s.APIV1Service.DeleteMemoShare(ctx, req.Msg)\n\tif err != nil {\n\t\treturn nil, convertGRPCError(err)\n\t}\n\treturn connect.NewResponse(resp), nil\n}\n\nfunc (s *ConnectServiceHandler) GetMemoByShare(ctx context.Context, req *connect.Request[v1pb.GetMemoByShareRequest]) (*connect.Response[v1pb.Memo], error) {\n\tresp, err := s.APIV1Service.GetMemoByShare(ctx, req.Msg)\n\tif err != nil {\n\t\treturn nil, convertGRPCError(err)\n\t}\n\treturn connect.NewResponse(resp), nil\n}\n\n// AttachmentService\n\nfunc (s *ConnectServiceHandler) CreateAttachment(ctx context.Context, req *connect.Request[v1pb.CreateAttachmentRequest]) (*connect.Response[v1pb.Attachment], error) {\n\tresp, err := s.APIV1Service.CreateAttachment(ctx, req.Msg)\n\tif err != nil {\n\t\treturn nil, convertGRPCError(err)\n\t}\n\treturn connect.NewResponse(resp), nil\n}\n\nfunc (s *ConnectServiceHandler) ListAttachments(ctx context.Context, req *connect.Request[v1pb.ListAttachmentsRequest]) (*connect.Response[v1pb.ListAttachmentsResponse], error) {\n\tresp, err := s.APIV1Service.ListAttachments(ctx, req.Msg)\n\tif err != nil {\n\t\treturn nil, convertGRPCError(err)\n\t}\n\treturn connect.NewResponse(resp), nil\n}\n\nfunc (s *ConnectServiceHandler) GetAttachment(ctx context.Context, req *connect.Request[v1pb.GetAttachmentRequest]) (*connect.Response[v1pb.Attachment], error) {\n\tresp, err := s.APIV1Service.GetAttachment(ctx, req.Msg)\n\tif err != nil {\n\t\treturn nil, convertGRPCError(err)\n\t}\n\treturn connect.NewResponse(resp), nil\n}\n\nfunc (s *ConnectServiceHandler) UpdateAttachment(ctx context.Context, req *connect.Request[v1pb.UpdateAttachmentRequest]) (*connect.Response[v1pb.Attachment], error) {\n\tresp, err := s.APIV1Service.UpdateAttachment(ctx, req.Msg)\n\tif err != nil {\n\t\treturn nil, convertGRPCError(err)\n\t}\n\treturn connect.NewResponse(resp), nil\n}\n\nfunc (s *ConnectServiceHandler) DeleteAttachment(ctx context.Context, req *connect.Request[v1pb.DeleteAttachmentRequest]) (*connect.Response[emptypb.Empty], error) {\n\tresp, err := s.APIV1Service.DeleteAttachment(ctx, req.Msg)\n\tif err != nil {\n\t\treturn nil, convertGRPCError(err)\n\t}\n\treturn connect.NewResponse(resp), nil\n}\n\n// ShortcutService\n\nfunc (s *ConnectServiceHandler) ListShortcuts(ctx context.Context, req *connect.Request[v1pb.ListShortcutsRequest]) (*connect.Response[v1pb.ListShortcutsResponse], error) {\n\tresp, err := s.APIV1Service.ListShortcuts(ctx, req.Msg)\n\tif err != nil {\n\t\treturn nil, convertGRPCError(err)\n\t}\n\treturn connect.NewResponse(resp), nil\n}\n\nfunc (s *ConnectServiceHandler) GetShortcut(ctx context.Context, req *connect.Request[v1pb.GetShortcutRequest]) (*connect.Response[v1pb.Shortcut], error) {\n\tresp, err := s.APIV1Service.GetShortcut(ctx, req.Msg)\n\tif err != nil {\n\t\treturn nil, convertGRPCError(err)\n\t}\n\treturn connect.NewResponse(resp), nil\n}\n\nfunc (s *ConnectServiceHandler) CreateShortcut(ctx context.Context, req *connect.Request[v1pb.CreateShortcutRequest]) (*connect.Response[v1pb.Shortcut], error) {\n\tresp, err := s.APIV1Service.CreateShortcut(ctx, req.Msg)\n\tif err != nil {\n\t\treturn nil, convertGRPCError(err)\n\t}\n\treturn connect.NewResponse(resp), nil\n}\n\nfunc (s *ConnectServiceHandler) UpdateShortcut(ctx context.Context, req *connect.Request[v1pb.UpdateShortcutRequest]) (*connect.Response[v1pb.Shortcut], error) {\n\tresp, err := s.APIV1Service.UpdateShortcut(ctx, req.Msg)\n\tif err != nil {\n\t\treturn nil, convertGRPCError(err)\n\t}\n\treturn connect.NewResponse(resp), nil\n}\n\nfunc (s *ConnectServiceHandler) DeleteShortcut(ctx context.Context, req *connect.Request[v1pb.DeleteShortcutRequest]) (*connect.Response[emptypb.Empty], error) {\n\tresp, err := s.APIV1Service.DeleteShortcut(ctx, req.Msg)\n\tif err != nil {\n\t\treturn nil, convertGRPCError(err)\n\t}\n\treturn connect.NewResponse(resp), nil\n}\n\n// IdentityProviderService\n\nfunc (s *ConnectServiceHandler) ListIdentityProviders(ctx context.Context, req *connect.Request[v1pb.ListIdentityProvidersRequest]) (*connect.Response[v1pb.ListIdentityProvidersResponse], error) {\n\tresp, err := s.APIV1Service.ListIdentityProviders(ctx, req.Msg)\n\tif err != nil {\n\t\treturn nil, convertGRPCError(err)\n\t}\n\treturn connect.NewResponse(resp), nil\n}\n\nfunc (s *ConnectServiceHandler) GetIdentityProvider(ctx context.Context, req *connect.Request[v1pb.GetIdentityProviderRequest]) (*connect.Response[v1pb.IdentityProvider], error) {\n\tresp, err := s.APIV1Service.GetIdentityProvider(ctx, req.Msg)\n\tif err != nil {\n\t\treturn nil, convertGRPCError(err)\n\t}\n\treturn connect.NewResponse(resp), nil\n}\n\nfunc (s *ConnectServiceHandler) CreateIdentityProvider(ctx context.Context, req *connect.Request[v1pb.CreateIdentityProviderRequest]) (*connect.Response[v1pb.IdentityProvider], error) {\n\tresp, err := s.APIV1Service.CreateIdentityProvider(ctx, req.Msg)\n\tif err != nil {\n\t\treturn nil, convertGRPCError(err)\n\t}\n\treturn connect.NewResponse(resp), nil\n}\n\nfunc (s *ConnectServiceHandler) UpdateIdentityProvider(ctx context.Context, req *connect.Request[v1pb.UpdateIdentityProviderRequest]) (*connect.Response[v1pb.IdentityProvider], error) {\n\tresp, err := s.APIV1Service.UpdateIdentityProvider(ctx, req.Msg)\n\tif err != nil {\n\t\treturn nil, convertGRPCError(err)\n\t}\n\treturn connect.NewResponse(resp), nil\n}\n\nfunc (s *ConnectServiceHandler) DeleteIdentityProvider(ctx context.Context, req *connect.Request[v1pb.DeleteIdentityProviderRequest]) (*connect.Response[emptypb.Empty], error) {\n\tresp, err := s.APIV1Service.DeleteIdentityProvider(ctx, req.Msg)\n\tif err != nil {\n\t\treturn nil, convertGRPCError(err)\n\t}\n\treturn connect.NewResponse(resp), nil\n}\n"
  },
  {
    "path": "server/router/api/v1/header_carrier.go",
    "content": "package v1\n\nimport (\n\t\"context\"\n\n\t\"connectrpc.com/connect\"\n\t\"google.golang.org/grpc\"\n\t\"google.golang.org/grpc/metadata\"\n)\n\n// headerCarrierKey is the context key for storing headers to be set in the response.\ntype headerCarrierKey struct{}\n\n// HeaderCarrier stores headers that need to be set in the response.\n//\n// Problem: The codebase supports two protocols simultaneously:\n//   - Native gRPC: Uses grpc.SetHeader() to set response headers\n//   - Connect-RPC: Uses connect.Response.Header().Set() to set response headers\n//\n// Solution: HeaderCarrier provides a protocol-agnostic way to set headers.\n//   - Service methods call SetResponseHeader() regardless of protocol\n//   - For gRPC requests: SetResponseHeader uses grpc.SetHeader directly\n//   - For Connect requests: SetResponseHeader stores headers in HeaderCarrier\n//   - Connect wrappers extract headers from HeaderCarrier and apply to response\n//\n// This allows service methods to work with both protocols without knowing which one is being used.\ntype HeaderCarrier struct {\n\theaders map[string]string\n}\n\n// newHeaderCarrier creates a new header carrier.\nfunc newHeaderCarrier() *HeaderCarrier {\n\treturn &HeaderCarrier{\n\t\theaders: make(map[string]string),\n\t}\n}\n\n// Set adds a header to the carrier.\nfunc (h *HeaderCarrier) Set(key, value string) {\n\th.headers[key] = value\n}\n\n// Get retrieves a header from the carrier.\nfunc (h *HeaderCarrier) Get(key string) string {\n\treturn h.headers[key]\n}\n\n// All returns all headers.\nfunc (h *HeaderCarrier) All() map[string]string {\n\treturn h.headers\n}\n\n// WithHeaderCarrier adds a header carrier to the context.\nfunc WithHeaderCarrier(ctx context.Context) context.Context {\n\treturn context.WithValue(ctx, headerCarrierKey{}, newHeaderCarrier())\n}\n\n// GetHeaderCarrier retrieves the header carrier from the context.\n// Returns nil if no carrier is present.\nfunc GetHeaderCarrier(ctx context.Context) *HeaderCarrier {\n\tif carrier, ok := ctx.Value(headerCarrierKey{}).(*HeaderCarrier); ok {\n\t\treturn carrier\n\t}\n\treturn nil\n}\n\n// SetResponseHeader sets a header in the response.\n//\n// This function works for both gRPC and Connect protocols:\n//   - For gRPC: Uses grpc.SetHeader to set headers in gRPC metadata\n//   - For Connect: Stores in HeaderCarrier for Connect wrapper to apply later\n//\n// The protocol is automatically detected based on whether a HeaderCarrier\n// exists in the context (injected by Connect wrappers).\nfunc SetResponseHeader(ctx context.Context, key, value string) error {\n\t// Try Connect first (check if we have a header carrier)\n\tif carrier := GetHeaderCarrier(ctx); carrier != nil {\n\t\tcarrier.Set(key, value)\n\t\treturn nil\n\t}\n\n\t// Fall back to gRPC\n\treturn grpc.SetHeader(ctx, metadata.New(map[string]string{\n\t\tkey: value,\n\t}))\n}\n\n// connectWithHeaderCarrier is a helper for Connect service wrappers that need to set response headers.\n//\n// It injects a HeaderCarrier into the context, calls the service method,\n// and applies any headers from the carrier to the Connect response.\n//\n// The generic parameter T is the non-pointer protobuf message type (e.g., v1pb.CreateSessionResponse),\n// while fn returns *T (the pointer type) as is standard for protobuf messages.\n//\n// Usage in Connect wrappers:\n//\n//\tfunc (s *ConnectServiceHandler) CreateSession(ctx context.Context, req *connect.Request[v1pb.CreateSessionRequest]) (*connect.Response[v1pb.CreateSessionResponse], error) {\n//\t    return connectWithHeaderCarrier(ctx, func(ctx context.Context) (*v1pb.CreateSessionResponse, error) {\n//\t        return s.APIV1Service.CreateSession(ctx, req.Msg)\n//\t    })\n//\t}\nfunc connectWithHeaderCarrier[T any](ctx context.Context, fn func(context.Context) (*T, error)) (*connect.Response[T], error) {\n\t// Inject header carrier for Connect protocol\n\tctx = WithHeaderCarrier(ctx)\n\n\t// Call the service method\n\tresp, err := fn(ctx)\n\tif err != nil {\n\t\treturn nil, convertGRPCError(err)\n\t}\n\n\t// Create Connect response\n\tconnectResp := connect.NewResponse(resp)\n\n\t// Apply any headers set via the header carrier\n\tif carrier := GetHeaderCarrier(ctx); carrier != nil {\n\t\tfor key, value := range carrier.All() {\n\t\t\tconnectResp.Header().Set(key, value)\n\t\t}\n\t}\n\n\treturn connectResp, nil\n}\n"
  },
  {
    "path": "server/router/api/v1/health_service.go",
    "content": "package v1\n\nimport (\n\t\"context\"\n\n\t\"google.golang.org/grpc/codes\"\n\t\"google.golang.org/grpc/health/grpc_health_v1\"\n\t\"google.golang.org/grpc/status\"\n)\n\nfunc (s *APIV1Service) Check(ctx context.Context,\n\t_ *grpc_health_v1.HealthCheckRequest) (*grpc_health_v1.HealthCheckResponse, error) {\n\t// Check if database is initialized by verifying instance basic setting exists\n\tinstanceBasicSetting, err := s.Store.GetInstanceBasicSetting(ctx)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Unavailable, \"database not initialized: %v\", err)\n\t}\n\n\t// Verify schema version is set (empty means database not properly initialized)\n\tif instanceBasicSetting.SchemaVersion == \"\" {\n\t\treturn nil, status.Errorf(codes.Unavailable, \"schema version not set\")\n\t}\n\n\treturn &grpc_health_v1.HealthCheckResponse{Status: grpc_health_v1.HealthCheckResponse_SERVING}, nil\n}\n"
  },
  {
    "path": "server/router/api/v1/idp_service.go",
    "content": "package v1\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"google.golang.org/grpc/codes\"\n\t\"google.golang.org/grpc/status\"\n\t\"google.golang.org/protobuf/types/known/emptypb\"\n\n\tv1pb \"github.com/usememos/memos/proto/gen/api/v1\"\n\tstorepb \"github.com/usememos/memos/proto/gen/store\"\n\t\"github.com/usememos/memos/store\"\n)\n\nfunc (s *APIV1Service) CreateIdentityProvider(ctx context.Context, request *v1pb.CreateIdentityProviderRequest) (*v1pb.IdentityProvider, error) {\n\tcurrentUser, err := s.fetchCurrentUser(ctx)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to get user: %v\", err)\n\t}\n\tif currentUser == nil {\n\t\treturn nil, status.Errorf(codes.Unauthenticated, \"user not authenticated\")\n\t}\n\tif currentUser.Role != store.RoleAdmin {\n\t\treturn nil, status.Errorf(codes.PermissionDenied, \"permission denied\")\n\t}\n\n\tidpUID, err := ValidateAndGenerateUID(request.IdentityProviderId)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tstoreIdp := convertIdentityProviderToStore(request.IdentityProvider)\n\tstoreIdp.Uid = idpUID\n\n\tidentityProvider, err := s.Store.CreateIdentityProvider(ctx, storeIdp)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to create identity provider, error: %+v\", err)\n\t}\n\treturn convertIdentityProviderFromStore(identityProvider), nil\n}\n\nfunc (s *APIV1Service) ListIdentityProviders(ctx context.Context, _ *v1pb.ListIdentityProvidersRequest) (*v1pb.ListIdentityProvidersResponse, error) {\n\tidentityProviders, err := s.Store.ListIdentityProviders(ctx, &store.FindIdentityProvider{})\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to list identity providers, error: %+v\", err)\n\t}\n\n\tresponse := &v1pb.ListIdentityProvidersResponse{\n\t\tIdentityProviders: []*v1pb.IdentityProvider{},\n\t}\n\n\t// Default to lowest-privilege role, update later based on real role\n\tcurrentUserRole := store.RoleUser\n\tcurrentUser, err := s.fetchCurrentUser(ctx)\n\tif err == nil && currentUser != nil {\n\t\tcurrentUserRole = currentUser.Role\n\t}\n\n\tfor _, identityProvider := range identityProviders {\n\t\tidentityProviderConverted := convertIdentityProviderFromStore(identityProvider)\n\t\tresponse.IdentityProviders = append(response.IdentityProviders, redactIdentityProviderResponse(identityProviderConverted, currentUserRole))\n\t}\n\treturn response, nil\n}\n\nfunc (s *APIV1Service) GetIdentityProvider(ctx context.Context, request *v1pb.GetIdentityProviderRequest) (*v1pb.IdentityProvider, error) {\n\tuid, err := ExtractIdentityProviderUIDFromName(request.Name)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.InvalidArgument, \"invalid identity provider name: %v\", err)\n\t}\n\tidentityProvider, err := s.Store.GetIdentityProvider(ctx, &store.FindIdentityProvider{\n\t\tUID: &uid,\n\t})\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to get identity provider, error: %+v\", err)\n\t}\n\tif identityProvider == nil {\n\t\treturn nil, status.Errorf(codes.NotFound, \"identity provider not found\")\n\t}\n\n\t// Default to lowest-privilege role, update later based on real role\n\tcurrentUserRole := store.RoleUser\n\tcurrentUser, err := s.fetchCurrentUser(ctx)\n\tif err == nil && currentUser != nil {\n\t\tcurrentUserRole = currentUser.Role\n\t}\n\n\tidentityProviderConverted := convertIdentityProviderFromStore(identityProvider)\n\treturn redactIdentityProviderResponse(identityProviderConverted, currentUserRole), nil\n}\n\nfunc (s *APIV1Service) UpdateIdentityProvider(ctx context.Context, request *v1pb.UpdateIdentityProviderRequest) (*v1pb.IdentityProvider, error) {\n\tcurrentUser, err := s.fetchCurrentUser(ctx)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to get user: %v\", err)\n\t}\n\tif currentUser == nil {\n\t\treturn nil, status.Errorf(codes.Unauthenticated, \"user not authenticated\")\n\t}\n\tif currentUser.Role != store.RoleAdmin {\n\t\treturn nil, status.Errorf(codes.PermissionDenied, \"permission denied\")\n\t}\n\n\tif request.UpdateMask == nil || len(request.UpdateMask.Paths) == 0 {\n\t\treturn nil, status.Errorf(codes.InvalidArgument, \"update_mask is required\")\n\t}\n\n\tuid, err := ExtractIdentityProviderUIDFromName(request.IdentityProvider.Name)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.InvalidArgument, \"invalid identity provider name: %v\", err)\n\t}\n\n\t// Look up the IdP by UID to get the internal ID for update.\n\texisting, err := s.Store.GetIdentityProvider(ctx, &store.FindIdentityProvider{UID: &uid})\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to get identity provider, error: %+v\", err)\n\t}\n\tif existing == nil {\n\t\treturn nil, status.Errorf(codes.NotFound, \"identity provider not found\")\n\t}\n\n\tupdate := &store.UpdateIdentityProviderV1{\n\t\tID:   existing.Id,\n\t\tType: storepb.IdentityProvider_Type(storepb.IdentityProvider_Type_value[request.IdentityProvider.Type.String()]),\n\t}\n\tfor _, field := range request.UpdateMask.Paths {\n\t\tswitch field {\n\t\tcase \"title\":\n\t\t\tupdate.Name = &request.IdentityProvider.Title\n\t\tcase \"identifier_filter\":\n\t\t\tupdate.IdentifierFilter = &request.IdentityProvider.IdentifierFilter\n\t\tcase \"config\":\n\t\t\tupdate.Config = convertIdentityProviderConfigToStore(request.IdentityProvider.Type, request.IdentityProvider.Config)\n\t\tdefault:\n\t\t\t// Ignore unsupported fields\n\t\t}\n\t}\n\n\tidentityProvider, err := s.Store.UpdateIdentityProvider(ctx, update)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to update identity provider, error: %+v\", err)\n\t}\n\treturn convertIdentityProviderFromStore(identityProvider), nil\n}\n\nfunc (s *APIV1Service) DeleteIdentityProvider(ctx context.Context, request *v1pb.DeleteIdentityProviderRequest) (*emptypb.Empty, error) {\n\tcurrentUser, err := s.fetchCurrentUser(ctx)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to get user: %v\", err)\n\t}\n\tif currentUser == nil {\n\t\treturn nil, status.Errorf(codes.Unauthenticated, \"user not authenticated\")\n\t}\n\tif currentUser.Role != store.RoleAdmin {\n\t\treturn nil, status.Errorf(codes.PermissionDenied, \"permission denied\")\n\t}\n\n\tuid, err := ExtractIdentityProviderUIDFromName(request.Name)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.InvalidArgument, \"invalid identity provider name: %v\", err)\n\t}\n\n\t// Look up the IdP by UID to get the internal ID for deletion.\n\tidentityProvider, err := s.Store.GetIdentityProvider(ctx, &store.FindIdentityProvider{UID: &uid})\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to check identity provider existence: %v\", err)\n\t}\n\tif identityProvider == nil {\n\t\treturn nil, status.Errorf(codes.NotFound, \"identity provider not found\")\n\t}\n\n\tif err := s.Store.DeleteIdentityProvider(ctx, &store.DeleteIdentityProvider{ID: identityProvider.Id}); err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to delete identity provider, error: %+v\", err)\n\t}\n\treturn &emptypb.Empty{}, nil\n}\n\nfunc convertIdentityProviderFromStore(identityProvider *storepb.IdentityProvider) *v1pb.IdentityProvider {\n\ttemp := &v1pb.IdentityProvider{\n\t\tName:             fmt.Sprintf(\"%s%s\", IdentityProviderNamePrefix, identityProvider.Uid),\n\t\tTitle:            identityProvider.Name,\n\t\tIdentifierFilter: identityProvider.IdentifierFilter,\n\t\tType:             v1pb.IdentityProvider_Type(v1pb.IdentityProvider_Type_value[identityProvider.Type.String()]),\n\t}\n\tif identityProvider.Type == storepb.IdentityProvider_OAUTH2 {\n\t\toauth2Config := identityProvider.Config.GetOauth2Config()\n\t\ttemp.Config = &v1pb.IdentityProviderConfig{\n\t\t\tConfig: &v1pb.IdentityProviderConfig_Oauth2Config{\n\t\t\t\tOauth2Config: &v1pb.OAuth2Config{\n\t\t\t\t\tClientId:     oauth2Config.ClientId,\n\t\t\t\t\tClientSecret: oauth2Config.ClientSecret,\n\t\t\t\t\tAuthUrl:      oauth2Config.AuthUrl,\n\t\t\t\t\tTokenUrl:     oauth2Config.TokenUrl,\n\t\t\t\t\tUserInfoUrl:  oauth2Config.UserInfoUrl,\n\t\t\t\t\tScopes:       oauth2Config.Scopes,\n\t\t\t\t\tFieldMapping: &v1pb.FieldMapping{\n\t\t\t\t\t\tIdentifier:  oauth2Config.FieldMapping.Identifier,\n\t\t\t\t\t\tDisplayName: oauth2Config.FieldMapping.DisplayName,\n\t\t\t\t\t\tEmail:       oauth2Config.FieldMapping.Email,\n\t\t\t\t\t\tAvatarUrl:   oauth2Config.FieldMapping.AvatarUrl,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\t}\n\treturn temp\n}\n\nfunc convertIdentityProviderToStore(identityProvider *v1pb.IdentityProvider) *storepb.IdentityProvider {\n\ttemp := &storepb.IdentityProvider{\n\t\tName:             identityProvider.Title,\n\t\tIdentifierFilter: identityProvider.IdentifierFilter,\n\t\tType:             storepb.IdentityProvider_Type(storepb.IdentityProvider_Type_value[identityProvider.Type.String()]),\n\t\tConfig:           convertIdentityProviderConfigToStore(identityProvider.Type, identityProvider.Config),\n\t}\n\treturn temp\n}\n\nfunc convertIdentityProviderConfigToStore(identityProviderType v1pb.IdentityProvider_Type, config *v1pb.IdentityProviderConfig) *storepb.IdentityProviderConfig {\n\tif identityProviderType == v1pb.IdentityProvider_OAUTH2 {\n\t\toauth2Config := config.GetOauth2Config()\n\t\treturn &storepb.IdentityProviderConfig{\n\t\t\tConfig: &storepb.IdentityProviderConfig_Oauth2Config{\n\t\t\t\tOauth2Config: &storepb.OAuth2Config{\n\t\t\t\t\tClientId:     oauth2Config.ClientId,\n\t\t\t\t\tClientSecret: oauth2Config.ClientSecret,\n\t\t\t\t\tAuthUrl:      oauth2Config.AuthUrl,\n\t\t\t\t\tTokenUrl:     oauth2Config.TokenUrl,\n\t\t\t\t\tUserInfoUrl:  oauth2Config.UserInfoUrl,\n\t\t\t\t\tScopes:       oauth2Config.Scopes,\n\t\t\t\t\tFieldMapping: &storepb.FieldMapping{\n\t\t\t\t\t\tIdentifier:  oauth2Config.FieldMapping.Identifier,\n\t\t\t\t\t\tDisplayName: oauth2Config.FieldMapping.DisplayName,\n\t\t\t\t\t\tEmail:       oauth2Config.FieldMapping.Email,\n\t\t\t\t\t\tAvatarUrl:   oauth2Config.FieldMapping.AvatarUrl,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc redactIdentityProviderResponse(identityProvider *v1pb.IdentityProvider, userRole store.Role) *v1pb.IdentityProvider {\n\tif userRole != store.RoleAdmin {\n\t\tif identityProvider.Type == v1pb.IdentityProvider_OAUTH2 {\n\t\t\tidentityProvider.Config.GetOauth2Config().ClientSecret = \"\"\n\t\t}\n\t}\n\n\treturn identityProvider\n}\n"
  },
  {
    "path": "server/router/api/v1/instance_service.go",
    "content": "package v1\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"math\"\n\t\"strings\"\n\n\t\"github.com/pkg/errors\"\n\tcolorpb \"google.golang.org/genproto/googleapis/type/color\"\n\t\"google.golang.org/grpc/codes\"\n\t\"google.golang.org/grpc/status\"\n\n\tv1pb \"github.com/usememos/memos/proto/gen/api/v1\"\n\tstorepb \"github.com/usememos/memos/proto/gen/store\"\n\t\"github.com/usememos/memos/store\"\n)\n\n// GetInstanceProfile returns the instance profile.\nfunc (s *APIV1Service) GetInstanceProfile(ctx context.Context, _ *v1pb.GetInstanceProfileRequest) (*v1pb.InstanceProfile, error) {\n\tadmin, err := s.GetInstanceAdmin(ctx)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to get instance admin: %v\", err)\n\t}\n\n\tinstanceProfile := &v1pb.InstanceProfile{\n\t\tVersion:     s.Profile.Version,\n\t\tDemo:        s.Profile.Demo,\n\t\tInstanceUrl: s.Profile.InstanceURL,\n\t\tAdmin:       admin, // nil when not initialized\n\t}\n\treturn instanceProfile, nil\n}\n\nfunc (s *APIV1Service) GetInstanceSetting(ctx context.Context, request *v1pb.GetInstanceSettingRequest) (*v1pb.InstanceSetting, error) {\n\tinstanceSettingKeyString, err := ExtractInstanceSettingKeyFromName(request.Name)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.InvalidArgument, \"invalid instance setting name: %v\", err)\n\t}\n\n\tinstanceSettingKey := storepb.InstanceSettingKey(storepb.InstanceSettingKey_value[instanceSettingKeyString])\n\t// Get instance setting from store with default value.\n\tswitch instanceSettingKey {\n\tcase storepb.InstanceSettingKey_BASIC:\n\t\t_, err = s.Store.GetInstanceBasicSetting(ctx)\n\tcase storepb.InstanceSettingKey_GENERAL:\n\t\t_, err = s.Store.GetInstanceGeneralSetting(ctx)\n\tcase storepb.InstanceSettingKey_MEMO_RELATED:\n\t\t_, err = s.Store.GetInstanceMemoRelatedSetting(ctx)\n\tcase storepb.InstanceSettingKey_STORAGE:\n\t\t_, err = s.Store.GetInstanceStorageSetting(ctx)\n\tcase storepb.InstanceSettingKey_TAGS:\n\t\t_, err = s.Store.GetInstanceTagsSetting(ctx)\n\tcase storepb.InstanceSettingKey_NOTIFICATION:\n\t\t_, err = s.Store.GetInstanceNotificationSetting(ctx)\n\tdefault:\n\t\treturn nil, status.Errorf(codes.InvalidArgument, \"unsupported instance setting key: %v\", instanceSettingKey)\n\t}\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to get instance setting: %v\", err)\n\t}\n\n\tinstanceSetting, err := s.Store.GetInstanceSetting(ctx, &store.FindInstanceSetting{\n\t\tName: instanceSettingKey.String(),\n\t})\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to get instance setting: %v\", err)\n\t}\n\tif instanceSetting == nil {\n\t\treturn nil, status.Errorf(codes.NotFound, \"instance setting not found\")\n\t}\n\n\t// For storage setting, only admin can get it.\n\tif instanceSetting.Key == storepb.InstanceSettingKey_STORAGE {\n\t\tuser, err := s.fetchCurrentUser(ctx)\n\t\tif err != nil {\n\t\t\treturn nil, status.Errorf(codes.Internal, \"failed to get current user: %v\", err)\n\t\t}\n\t\tif user == nil {\n\t\t\treturn nil, status.Errorf(codes.Unauthenticated, \"user not authenticated\")\n\t\t}\n\t\tif user.Role != store.RoleAdmin {\n\t\t\treturn nil, status.Errorf(codes.PermissionDenied, \"permission denied\")\n\t\t}\n\t}\n\n\treturn convertInstanceSettingFromStore(instanceSetting), nil\n}\n\nfunc (s *APIV1Service) UpdateInstanceSetting(ctx context.Context, request *v1pb.UpdateInstanceSettingRequest) (*v1pb.InstanceSetting, error) {\n\tuser, err := s.fetchCurrentUser(ctx)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to get current user: %v\", err)\n\t}\n\tif user == nil {\n\t\treturn nil, status.Errorf(codes.Unauthenticated, \"user not authenticated\")\n\t}\n\tif user.Role != store.RoleAdmin {\n\t\treturn nil, status.Errorf(codes.PermissionDenied, \"permission denied\")\n\t}\n\n\t// TODO: Apply update_mask if specified\n\t_ = request.UpdateMask\n\n\tif err := validateInstanceSetting(request.Setting); err != nil {\n\t\treturn nil, status.Errorf(codes.InvalidArgument, \"invalid instance setting: %v\", err)\n\t}\n\n\tupdateSetting := convertInstanceSettingToStore(request.Setting)\n\tinstanceSetting, err := s.Store.UpsertInstanceSetting(ctx, updateSetting)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to upsert instance setting: %v\", err)\n\t}\n\n\treturn convertInstanceSettingFromStore(instanceSetting), nil\n}\n\nfunc convertInstanceSettingFromStore(setting *storepb.InstanceSetting) *v1pb.InstanceSetting {\n\tinstanceSetting := &v1pb.InstanceSetting{\n\t\tName: fmt.Sprintf(\"instance/settings/%s\", setting.Key.String()),\n\t}\n\tswitch setting.Value.(type) {\n\tcase *storepb.InstanceSetting_GeneralSetting:\n\t\tinstanceSetting.Value = &v1pb.InstanceSetting_GeneralSetting_{\n\t\t\tGeneralSetting: convertInstanceGeneralSettingFromStore(setting.GetGeneralSetting()),\n\t\t}\n\tcase *storepb.InstanceSetting_StorageSetting:\n\t\tinstanceSetting.Value = &v1pb.InstanceSetting_StorageSetting_{\n\t\t\tStorageSetting: convertInstanceStorageSettingFromStore(setting.GetStorageSetting()),\n\t\t}\n\tcase *storepb.InstanceSetting_MemoRelatedSetting:\n\t\tinstanceSetting.Value = &v1pb.InstanceSetting_MemoRelatedSetting_{\n\t\t\tMemoRelatedSetting: convertInstanceMemoRelatedSettingFromStore(setting.GetMemoRelatedSetting()),\n\t\t}\n\tcase *storepb.InstanceSetting_TagsSetting:\n\t\tinstanceSetting.Value = &v1pb.InstanceSetting_TagsSetting_{\n\t\t\tTagsSetting: convertInstanceTagsSettingFromStore(setting.GetTagsSetting()),\n\t\t}\n\tcase *storepb.InstanceSetting_NotificationSetting:\n\t\tinstanceSetting.Value = &v1pb.InstanceSetting_NotificationSetting_{\n\t\t\tNotificationSetting: convertInstanceNotificationSettingFromStore(setting.GetNotificationSetting()),\n\t\t}\n\tdefault:\n\t\t// Leave Value unset for unsupported setting variants.\n\t}\n\treturn instanceSetting\n}\n\nfunc convertInstanceSettingToStore(setting *v1pb.InstanceSetting) *storepb.InstanceSetting {\n\tsettingKeyString, _ := ExtractInstanceSettingKeyFromName(setting.Name)\n\tinstanceSetting := &storepb.InstanceSetting{\n\t\tKey: storepb.InstanceSettingKey(storepb.InstanceSettingKey_value[settingKeyString]),\n\t\tValue: &storepb.InstanceSetting_GeneralSetting{\n\t\t\tGeneralSetting: convertInstanceGeneralSettingToStore(setting.GetGeneralSetting()),\n\t\t},\n\t}\n\tswitch instanceSetting.Key {\n\tcase storepb.InstanceSettingKey_GENERAL:\n\t\tinstanceSetting.Value = &storepb.InstanceSetting_GeneralSetting{\n\t\t\tGeneralSetting: convertInstanceGeneralSettingToStore(setting.GetGeneralSetting()),\n\t\t}\n\tcase storepb.InstanceSettingKey_STORAGE:\n\t\tinstanceSetting.Value = &storepb.InstanceSetting_StorageSetting{\n\t\t\tStorageSetting: convertInstanceStorageSettingToStore(setting.GetStorageSetting()),\n\t\t}\n\tcase storepb.InstanceSettingKey_MEMO_RELATED:\n\t\tinstanceSetting.Value = &storepb.InstanceSetting_MemoRelatedSetting{\n\t\t\tMemoRelatedSetting: convertInstanceMemoRelatedSettingToStore(setting.GetMemoRelatedSetting()),\n\t\t}\n\tcase storepb.InstanceSettingKey_TAGS:\n\t\tinstanceSetting.Value = &storepb.InstanceSetting_TagsSetting{\n\t\t\tTagsSetting: convertInstanceTagsSettingToStore(setting.GetTagsSetting()),\n\t\t}\n\tcase storepb.InstanceSettingKey_NOTIFICATION:\n\t\tinstanceSetting.Value = &storepb.InstanceSetting_NotificationSetting{\n\t\t\tNotificationSetting: convertInstanceNotificationSettingToStore(setting.GetNotificationSetting()),\n\t\t}\n\tdefault:\n\t\t// Keep the default GeneralSetting value\n\t}\n\treturn instanceSetting\n}\n\nfunc convertInstanceGeneralSettingFromStore(setting *storepb.InstanceGeneralSetting) *v1pb.InstanceSetting_GeneralSetting {\n\tif setting == nil {\n\t\treturn nil\n\t}\n\n\tgeneralSetting := &v1pb.InstanceSetting_GeneralSetting{\n\t\tDisallowUserRegistration: setting.DisallowUserRegistration,\n\t\tDisallowPasswordAuth:     setting.DisallowPasswordAuth,\n\t\tAdditionalScript:         setting.AdditionalScript,\n\t\tAdditionalStyle:          setting.AdditionalStyle,\n\t\tWeekStartDayOffset:       setting.WeekStartDayOffset,\n\t\tDisallowChangeUsername:   setting.DisallowChangeUsername,\n\t\tDisallowChangeNickname:   setting.DisallowChangeNickname,\n\t}\n\tif setting.CustomProfile != nil {\n\t\tgeneralSetting.CustomProfile = &v1pb.InstanceSetting_GeneralSetting_CustomProfile{\n\t\t\tTitle:       setting.CustomProfile.Title,\n\t\t\tDescription: setting.CustomProfile.Description,\n\t\t\tLogoUrl:     setting.CustomProfile.LogoUrl,\n\t\t}\n\t}\n\treturn generalSetting\n}\n\nfunc convertInstanceGeneralSettingToStore(setting *v1pb.InstanceSetting_GeneralSetting) *storepb.InstanceGeneralSetting {\n\tif setting == nil {\n\t\treturn nil\n\t}\n\tgeneralSetting := &storepb.InstanceGeneralSetting{\n\t\tDisallowUserRegistration: setting.DisallowUserRegistration,\n\t\tDisallowPasswordAuth:     setting.DisallowPasswordAuth,\n\t\tAdditionalScript:         setting.AdditionalScript,\n\t\tAdditionalStyle:          setting.AdditionalStyle,\n\t\tWeekStartDayOffset:       setting.WeekStartDayOffset,\n\t\tDisallowChangeUsername:   setting.DisallowChangeUsername,\n\t\tDisallowChangeNickname:   setting.DisallowChangeNickname,\n\t}\n\tif setting.CustomProfile != nil {\n\t\tgeneralSetting.CustomProfile = &storepb.InstanceCustomProfile{\n\t\t\tTitle:       setting.CustomProfile.Title,\n\t\t\tDescription: setting.CustomProfile.Description,\n\t\t\tLogoUrl:     setting.CustomProfile.LogoUrl,\n\t\t}\n\t}\n\treturn generalSetting\n}\n\nfunc convertInstanceStorageSettingFromStore(settingpb *storepb.InstanceStorageSetting) *v1pb.InstanceSetting_StorageSetting {\n\tif settingpb == nil {\n\t\treturn nil\n\t}\n\tsetting := &v1pb.InstanceSetting_StorageSetting{\n\t\tStorageType:       v1pb.InstanceSetting_StorageSetting_StorageType(settingpb.StorageType),\n\t\tFilepathTemplate:  settingpb.FilepathTemplate,\n\t\tUploadSizeLimitMb: settingpb.UploadSizeLimitMb,\n\t}\n\tif settingpb.S3Config != nil {\n\t\tsetting.S3Config = &v1pb.InstanceSetting_StorageSetting_S3Config{\n\t\t\tAccessKeyId:     settingpb.S3Config.AccessKeyId,\n\t\t\tAccessKeySecret: settingpb.S3Config.AccessKeySecret,\n\t\t\tEndpoint:        settingpb.S3Config.Endpoint,\n\t\t\tRegion:          settingpb.S3Config.Region,\n\t\t\tBucket:          settingpb.S3Config.Bucket,\n\t\t\tUsePathStyle:    settingpb.S3Config.UsePathStyle,\n\t\t}\n\t}\n\treturn setting\n}\n\nfunc convertInstanceStorageSettingToStore(setting *v1pb.InstanceSetting_StorageSetting) *storepb.InstanceStorageSetting {\n\tif setting == nil {\n\t\treturn nil\n\t}\n\tsettingpb := &storepb.InstanceStorageSetting{\n\t\tStorageType:       storepb.InstanceStorageSetting_StorageType(setting.StorageType),\n\t\tFilepathTemplate:  setting.FilepathTemplate,\n\t\tUploadSizeLimitMb: setting.UploadSizeLimitMb,\n\t}\n\tif setting.S3Config != nil {\n\t\tsettingpb.S3Config = &storepb.StorageS3Config{\n\t\t\tAccessKeyId:     setting.S3Config.AccessKeyId,\n\t\t\tAccessKeySecret: setting.S3Config.AccessKeySecret,\n\t\t\tEndpoint:        setting.S3Config.Endpoint,\n\t\t\tRegion:          setting.S3Config.Region,\n\t\t\tBucket:          setting.S3Config.Bucket,\n\t\t\tUsePathStyle:    setting.S3Config.UsePathStyle,\n\t\t}\n\t}\n\treturn settingpb\n}\n\nfunc convertInstanceMemoRelatedSettingFromStore(setting *storepb.InstanceMemoRelatedSetting) *v1pb.InstanceSetting_MemoRelatedSetting {\n\tif setting == nil {\n\t\treturn nil\n\t}\n\treturn &v1pb.InstanceSetting_MemoRelatedSetting{\n\t\tDisplayWithUpdateTime: setting.DisplayWithUpdateTime,\n\t\tContentLengthLimit:    setting.ContentLengthLimit,\n\t\tEnableDoubleClickEdit: setting.EnableDoubleClickEdit,\n\t\tReactions:             setting.Reactions,\n\t}\n}\n\nfunc convertInstanceMemoRelatedSettingToStore(setting *v1pb.InstanceSetting_MemoRelatedSetting) *storepb.InstanceMemoRelatedSetting {\n\tif setting == nil {\n\t\treturn nil\n\t}\n\treturn &storepb.InstanceMemoRelatedSetting{\n\t\tDisplayWithUpdateTime: setting.DisplayWithUpdateTime,\n\t\tContentLengthLimit:    setting.ContentLengthLimit,\n\t\tEnableDoubleClickEdit: setting.EnableDoubleClickEdit,\n\t\tReactions:             setting.Reactions,\n\t}\n}\n\nfunc convertInstanceTagsSettingFromStore(setting *storepb.InstanceTagsSetting) *v1pb.InstanceSetting_TagsSetting {\n\tif setting == nil {\n\t\treturn nil\n\t}\n\ttags := make(map[string]*v1pb.InstanceSetting_TagMetadata, len(setting.Tags))\n\tfor tag, metadata := range setting.Tags {\n\t\ttags[tag] = &v1pb.InstanceSetting_TagMetadata{\n\t\t\tBackgroundColor: metadata.GetBackgroundColor(),\n\t\t}\n\t}\n\treturn &v1pb.InstanceSetting_TagsSetting{\n\t\tTags: tags,\n\t}\n}\n\nfunc convertInstanceTagsSettingToStore(setting *v1pb.InstanceSetting_TagsSetting) *storepb.InstanceTagsSetting {\n\tif setting == nil {\n\t\treturn nil\n\t}\n\ttags := make(map[string]*storepb.InstanceTagMetadata, len(setting.Tags))\n\tfor tag, metadata := range setting.Tags {\n\t\ttags[tag] = &storepb.InstanceTagMetadata{\n\t\t\tBackgroundColor: metadata.GetBackgroundColor(),\n\t\t}\n\t}\n\treturn &storepb.InstanceTagsSetting{\n\t\tTags: tags,\n\t}\n}\n\nfunc convertInstanceNotificationSettingFromStore(setting *storepb.InstanceNotificationSetting) *v1pb.InstanceSetting_NotificationSetting {\n\tif setting == nil {\n\t\treturn nil\n\t}\n\n\tnotificationSetting := &v1pb.InstanceSetting_NotificationSetting{}\n\tif setting.Email != nil {\n\t\tnotificationSetting.Email = &v1pb.InstanceSetting_NotificationSetting_EmailSetting{\n\t\t\tEnabled:      setting.Email.Enabled,\n\t\t\tSmtpHost:     setting.Email.SmtpHost,\n\t\t\tSmtpPort:     setting.Email.SmtpPort,\n\t\t\tSmtpUsername: setting.Email.SmtpUsername,\n\t\t\tSmtpPassword: setting.Email.SmtpPassword,\n\t\t\tFromEmail:    setting.Email.FromEmail,\n\t\t\tFromName:     setting.Email.FromName,\n\t\t\tReplyTo:      setting.Email.ReplyTo,\n\t\t\tUseTls:       setting.Email.UseTls,\n\t\t\tUseSsl:       setting.Email.UseSsl,\n\t\t}\n\t}\n\treturn notificationSetting\n}\n\nfunc convertInstanceNotificationSettingToStore(setting *v1pb.InstanceSetting_NotificationSetting) *storepb.InstanceNotificationSetting {\n\tif setting == nil {\n\t\treturn nil\n\t}\n\n\tnotificationSetting := &storepb.InstanceNotificationSetting{}\n\tif setting.Email != nil {\n\t\tnotificationSetting.Email = &storepb.InstanceNotificationSetting_EmailSetting{\n\t\t\tEnabled:      setting.Email.Enabled,\n\t\t\tSmtpHost:     setting.Email.SmtpHost,\n\t\t\tSmtpPort:     setting.Email.SmtpPort,\n\t\t\tSmtpUsername: setting.Email.SmtpUsername,\n\t\t\tSmtpPassword: setting.Email.SmtpPassword,\n\t\t\tFromEmail:    setting.Email.FromEmail,\n\t\t\tFromName:     setting.Email.FromName,\n\t\t\tReplyTo:      setting.Email.ReplyTo,\n\t\t\tUseTls:       setting.Email.UseTls,\n\t\t\tUseSsl:       setting.Email.UseSsl,\n\t\t}\n\t}\n\treturn notificationSetting\n}\n\nfunc validateInstanceSetting(setting *v1pb.InstanceSetting) error {\n\tkey, err := ExtractInstanceSettingKeyFromName(setting.Name)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif key != storepb.InstanceSettingKey_TAGS.String() {\n\t\treturn nil\n\t}\n\treturn validateInstanceTagsSetting(setting.GetTagsSetting())\n}\n\nfunc validateInstanceTagsSetting(setting *v1pb.InstanceSetting_TagsSetting) error {\n\tif setting == nil {\n\t\treturn errors.New(\"tags setting is required\")\n\t}\n\tfor tag, metadata := range setting.Tags {\n\t\tif strings.TrimSpace(tag) == \"\" {\n\t\t\treturn errors.New(\"tag key cannot be empty\")\n\t\t}\n\t\tif metadata == nil {\n\t\t\treturn errors.Errorf(\"tag metadata is required for %q\", tag)\n\t\t}\n\t\tif metadata.GetBackgroundColor() == nil {\n\t\t\treturn errors.Errorf(\"background_color is required for %q\", tag)\n\t\t}\n\t\tif err := validateInstanceColor(metadata.GetBackgroundColor()); err != nil {\n\t\t\treturn errors.Wrapf(err, \"background_color for %q\", tag)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc validateInstanceColor(color *colorpb.Color) error {\n\tif err := validateInstanceColorComponent(\"red\", color.GetRed()); err != nil {\n\t\treturn err\n\t}\n\tif err := validateInstanceColorComponent(\"green\", color.GetGreen()); err != nil {\n\t\treturn err\n\t}\n\tif err := validateInstanceColorComponent(\"blue\", color.GetBlue()); err != nil {\n\t\treturn err\n\t}\n\tif alpha := color.GetAlpha(); alpha != nil {\n\t\tif err := validateInstanceColorComponent(\"alpha\", alpha.GetValue()); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc validateInstanceColorComponent(name string, value float32) error {\n\tif math.IsNaN(float64(value)) || math.IsInf(float64(value), 0) {\n\t\treturn errors.Errorf(\"%s must be a finite number\", name)\n\t}\n\tif value < 0 || value > 1 {\n\t\treturn errors.Errorf(\"%s must be between 0 and 1\", name)\n\t}\n\treturn nil\n}\n\nfunc (s *APIV1Service) GetInstanceAdmin(ctx context.Context) (*v1pb.User, error) {\n\tadminUserType := store.RoleAdmin\n\tuser, err := s.Store.GetUser(ctx, &store.FindUser{\n\t\tRole: &adminUserType,\n\t})\n\tif err != nil {\n\t\treturn nil, errors.Wrapf(err, \"failed to find admin\")\n\t}\n\tif user == nil {\n\t\treturn nil, nil\n\t}\n\n\treturn convertUserFromStore(user), nil\n}\n"
  },
  {
    "path": "server/router/api/v1/memo_attachment_service.go",
    "content": "package v1\n\nimport (\n\t\"context\"\n\t\"slices\"\n\t\"time\"\n\n\t\"google.golang.org/grpc/codes\"\n\t\"google.golang.org/grpc/status\"\n\t\"google.golang.org/protobuf/types/known/emptypb\"\n\n\tv1pb \"github.com/usememos/memos/proto/gen/api/v1\"\n\t\"github.com/usememos/memos/store\"\n)\n\nfunc (s *APIV1Service) SetMemoAttachments(ctx context.Context, request *v1pb.SetMemoAttachmentsRequest) (*emptypb.Empty, error) {\n\tuser, err := s.fetchCurrentUser(ctx)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to get current user: %v\", err)\n\t}\n\tif user == nil {\n\t\treturn nil, status.Errorf(codes.Unauthenticated, \"user not authenticated\")\n\t}\n\tmemoUID, err := ExtractMemoUIDFromName(request.Name)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.InvalidArgument, \"invalid memo name: %v\", err)\n\t}\n\tmemo, err := s.Store.GetMemo(ctx, &store.FindMemo{UID: &memoUID})\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to get memo\")\n\t}\n\tif memo == nil {\n\t\treturn nil, status.Errorf(codes.NotFound, \"memo not found\")\n\t}\n\tif memo.CreatorID != user.ID && !isSuperUser(user) {\n\t\treturn nil, status.Errorf(codes.PermissionDenied, \"permission denied\")\n\t}\n\tattachments, err := s.Store.ListAttachments(ctx, &store.FindAttachment{\n\t\tMemoID: &memo.ID,\n\t})\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to list attachments\")\n\t}\n\n\t// Delete attachments that are not in the request.\n\tfor _, attachment := range attachments {\n\t\tfound := false\n\t\tfor _, requestAttachment := range request.Attachments {\n\t\t\trequestAttachmentUID, err := ExtractAttachmentUIDFromName(requestAttachment.Name)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, status.Errorf(codes.InvalidArgument, \"invalid attachment name: %v\", err)\n\t\t\t}\n\t\t\tif attachment.UID == requestAttachmentUID {\n\t\t\t\tfound = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif !found {\n\t\t\tif err = s.Store.DeleteAttachment(ctx, &store.DeleteAttachment{\n\t\t\t\tID:     int32(attachment.ID),\n\t\t\t\tMemoID: &memo.ID,\n\t\t\t}); err != nil {\n\t\t\t\treturn nil, status.Errorf(codes.Internal, \"failed to delete attachment\")\n\t\t\t}\n\t\t}\n\t}\n\n\tslices.Reverse(request.Attachments)\n\t// Update attachments' memo_id in the request.\n\tfor index, attachment := range request.Attachments {\n\t\tattachmentUID, err := ExtractAttachmentUIDFromName(attachment.Name)\n\t\tif err != nil {\n\t\t\treturn nil, status.Errorf(codes.InvalidArgument, \"invalid attachment name: %v\", err)\n\t\t}\n\t\ttempAttachment, err := s.Store.GetAttachment(ctx, &store.FindAttachment{UID: &attachmentUID})\n\t\tif err != nil {\n\t\t\treturn nil, status.Errorf(codes.Internal, \"failed to get attachment: %v\", err)\n\t\t}\n\t\tif tempAttachment == nil {\n\t\t\treturn nil, status.Errorf(codes.NotFound, \"attachment not found: %s\", attachmentUID)\n\t\t}\n\t\tupdatedTs := time.Now().Unix() + int64(index)\n\t\tif err := s.Store.UpdateAttachment(ctx, &store.UpdateAttachment{\n\t\t\tID:        tempAttachment.ID,\n\t\t\tMemoID:    &memo.ID,\n\t\t\tUpdatedTs: &updatedTs,\n\t\t}); err != nil {\n\t\t\treturn nil, status.Errorf(codes.Internal, \"failed to update attachment: %v\", err)\n\t\t}\n\t}\n\n\treturn &emptypb.Empty{}, nil\n}\n\nfunc (s *APIV1Service) ListMemoAttachments(ctx context.Context, request *v1pb.ListMemoAttachmentsRequest) (*v1pb.ListMemoAttachmentsResponse, error) {\n\tmemoUID, err := ExtractMemoUIDFromName(request.Name)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.InvalidArgument, \"invalid memo name: %v\", err)\n\t}\n\tmemo, err := s.Store.GetMemo(ctx, &store.FindMemo{UID: &memoUID})\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to get memo: %v\", err)\n\t}\n\tif memo == nil {\n\t\treturn nil, status.Errorf(codes.NotFound, \"memo not found\")\n\t}\n\n\t// Check memo visibility.\n\tif memo.Visibility != store.Public {\n\t\tuser, err := s.fetchCurrentUser(ctx)\n\t\tif err != nil {\n\t\t\treturn nil, status.Errorf(codes.Internal, \"failed to get user: %v\", err)\n\t\t}\n\t\tif user == nil {\n\t\t\treturn nil, status.Errorf(codes.Unauthenticated, \"user not authenticated\")\n\t\t}\n\t\tif memo.Visibility == store.Private && memo.CreatorID != user.ID && !isSuperUser(user) {\n\t\t\treturn nil, status.Errorf(codes.PermissionDenied, \"permission denied\")\n\t\t}\n\t}\n\n\tattachments, err := s.Store.ListAttachments(ctx, &store.FindAttachment{\n\t\tMemoID: &memo.ID,\n\t})\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to list attachments: %v\", err)\n\t}\n\n\tresponse := &v1pb.ListMemoAttachmentsResponse{\n\t\tAttachments: []*v1pb.Attachment{},\n\t}\n\tfor _, attachment := range attachments {\n\t\tresponse.Attachments = append(response.Attachments, convertAttachmentFromStore(attachment))\n\t}\n\treturn response, nil\n}\n"
  },
  {
    "path": "server/router/api/v1/memo_relation_service.go",
    "content": "package v1\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/pkg/errors\"\n\t\"google.golang.org/grpc/codes\"\n\t\"google.golang.org/grpc/status\"\n\t\"google.golang.org/protobuf/types/known/emptypb\"\n\n\tv1pb \"github.com/usememos/memos/proto/gen/api/v1\"\n\t\"github.com/usememos/memos/store\"\n)\n\nfunc (s *APIV1Service) SetMemoRelations(ctx context.Context, request *v1pb.SetMemoRelationsRequest) (*emptypb.Empty, error) {\n\tuser, err := s.fetchCurrentUser(ctx)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to get current user: %v\", err)\n\t}\n\tif user == nil {\n\t\treturn nil, status.Errorf(codes.Unauthenticated, \"user not authenticated\")\n\t}\n\tmemoUID, err := ExtractMemoUIDFromName(request.Name)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.InvalidArgument, \"invalid memo name: %v\", err)\n\t}\n\tmemo, err := s.Store.GetMemo(ctx, &store.FindMemo{UID: &memoUID})\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to get memo\")\n\t}\n\tif memo == nil {\n\t\treturn nil, status.Errorf(codes.NotFound, \"memo not found\")\n\t}\n\tif memo.CreatorID != user.ID && !isSuperUser(user) {\n\t\treturn nil, status.Errorf(codes.PermissionDenied, \"permission denied\")\n\t}\n\treferenceType := store.MemoRelationReference\n\t// Delete all reference relations first.\n\tif err := s.Store.DeleteMemoRelation(ctx, &store.DeleteMemoRelation{\n\t\tMemoID: &memo.ID,\n\t\tType:   &referenceType,\n\t}); err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to delete memo relation\")\n\t}\n\n\tfor _, relation := range request.Relations {\n\t\t// Ignore reflexive relations.\n\t\tif request.Name == relation.RelatedMemo.Name {\n\t\t\tcontinue\n\t\t}\n\t\t// Ignore comment relations as there's no need to update a comment's relation.\n\t\t// Inserting/Deleting a comment is handled elsewhere.\n\t\tif relation.Type == v1pb.MemoRelation_COMMENT {\n\t\t\tcontinue\n\t\t}\n\t\trelatedMemoUID, err := ExtractMemoUIDFromName(relation.RelatedMemo.Name)\n\t\tif err != nil {\n\t\t\treturn nil, status.Errorf(codes.InvalidArgument, \"invalid related memo name: %v\", err)\n\t\t}\n\t\trelatedMemo, err := s.Store.GetMemo(ctx, &store.FindMemo{UID: &relatedMemoUID})\n\t\tif err != nil {\n\t\t\treturn nil, status.Errorf(codes.Internal, \"failed to get related memo\")\n\t\t}\n\t\tif _, err := s.Store.UpsertMemoRelation(ctx, &store.MemoRelation{\n\t\t\tMemoID:        memo.ID,\n\t\t\tRelatedMemoID: relatedMemo.ID,\n\t\t\tType:          convertMemoRelationTypeToStore(relation.Type),\n\t\t}); err != nil {\n\t\t\treturn nil, status.Errorf(codes.Internal, \"failed to upsert memo relation\")\n\t\t}\n\t}\n\n\treturn &emptypb.Empty{}, nil\n}\n\nfunc (s *APIV1Service) ListMemoRelations(ctx context.Context, request *v1pb.ListMemoRelationsRequest) (*v1pb.ListMemoRelationsResponse, error) {\n\tmemoUID, err := ExtractMemoUIDFromName(request.Name)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.InvalidArgument, \"invalid memo name: %v\", err)\n\t}\n\tmemo, err := s.Store.GetMemo(ctx, &store.FindMemo{UID: &memoUID})\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to get memo\")\n\t}\n\n\tcurrentUser, err := s.fetchCurrentUser(ctx)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to get user\")\n\t}\n\tvar memoFilter string\n\tif currentUser == nil {\n\t\tmemoFilter = `visibility == \"PUBLIC\"`\n\t} else {\n\t\tmemoFilter = fmt.Sprintf(`creator_id == %d || visibility in [\"PUBLIC\", \"PROTECTED\"]`, currentUser.ID)\n\t}\n\trelationList := []*v1pb.MemoRelation{}\n\ttempList, err := s.Store.ListMemoRelations(ctx, &store.FindMemoRelation{\n\t\tMemoID:     &memo.ID,\n\t\tMemoFilter: &memoFilter,\n\t})\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to list memo relations: %v\", err)\n\t}\n\tfor _, raw := range tempList {\n\t\trelation, err := s.convertMemoRelationFromStore(ctx, raw)\n\t\tif err != nil {\n\t\t\treturn nil, status.Errorf(codes.Internal, \"failed to convert memo relation\")\n\t\t}\n\t\trelationList = append(relationList, relation)\n\t}\n\ttempList, err = s.Store.ListMemoRelations(ctx, &store.FindMemoRelation{\n\t\tRelatedMemoID: &memo.ID,\n\t\tMemoFilter:    &memoFilter,\n\t})\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to list related memo relations: %v\", err)\n\t}\n\tfor _, raw := range tempList {\n\t\trelation, err := s.convertMemoRelationFromStore(ctx, raw)\n\t\tif err != nil {\n\t\t\treturn nil, status.Errorf(codes.Internal, \"failed to convert memo relation\")\n\t\t}\n\t\trelationList = append(relationList, relation)\n\t}\n\n\tresponse := &v1pb.ListMemoRelationsResponse{\n\t\tRelations: relationList,\n\t}\n\treturn response, nil\n}\n\nfunc (s *APIV1Service) convertMemoRelationFromStore(ctx context.Context, memoRelation *store.MemoRelation) (*v1pb.MemoRelation, error) {\n\tmemo, err := s.Store.GetMemo(ctx, &store.FindMemo{ID: &memoRelation.MemoID})\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to get memo: %v\", err)\n\t}\n\tmemoSnippet, err := s.getMemoContentSnippet(memo.Content)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"failed to get memo content snippet\")\n\t}\n\trelatedMemo, err := s.Store.GetMemo(ctx, &store.FindMemo{ID: &memoRelation.RelatedMemoID})\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to get related memo: %v\", err)\n\t}\n\trelatedMemoSnippet, err := s.getMemoContentSnippet(relatedMemo.Content)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"failed to get related memo content snippet\")\n\t}\n\treturn &v1pb.MemoRelation{\n\t\tMemo: &v1pb.MemoRelation_Memo{\n\t\t\tName:    fmt.Sprintf(\"%s%s\", MemoNamePrefix, memo.UID),\n\t\t\tSnippet: memoSnippet,\n\t\t},\n\t\tRelatedMemo: &v1pb.MemoRelation_Memo{\n\t\t\tName:    fmt.Sprintf(\"%s%s\", MemoNamePrefix, relatedMemo.UID),\n\t\t\tSnippet: relatedMemoSnippet,\n\t\t},\n\t\tType: convertMemoRelationTypeFromStore(memoRelation.Type),\n\t}, nil\n}\n\nfunc convertMemoRelationTypeFromStore(relationType store.MemoRelationType) v1pb.MemoRelation_Type {\n\tswitch relationType {\n\tcase store.MemoRelationReference:\n\t\treturn v1pb.MemoRelation_REFERENCE\n\tcase store.MemoRelationComment:\n\t\treturn v1pb.MemoRelation_COMMENT\n\tdefault:\n\t\treturn v1pb.MemoRelation_TYPE_UNSPECIFIED\n\t}\n}\n\nfunc convertMemoRelationTypeToStore(relationType v1pb.MemoRelation_Type) store.MemoRelationType {\n\tswitch relationType {\n\tcase v1pb.MemoRelation_COMMENT:\n\t\treturn store.MemoRelationComment\n\tdefault:\n\t\treturn store.MemoRelationReference\n\t}\n}\n"
  },
  {
    "path": "server/router/api/v1/memo_service.go",
    "content": "package v1\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/pkg/errors\"\n\t\"google.golang.org/grpc/codes\"\n\t\"google.golang.org/grpc/status\"\n\t\"google.golang.org/protobuf/types/known/emptypb\"\n\n\t\"github.com/usememos/memos/plugin/webhook\"\n\tv1pb \"github.com/usememos/memos/proto/gen/api/v1\"\n\tstorepb \"github.com/usememos/memos/proto/gen/store\"\n\t\"github.com/usememos/memos/server/runner/memopayload\"\n\t\"github.com/usememos/memos/store\"\n)\n\nfunc (s *APIV1Service) CreateMemo(ctx context.Context, request *v1pb.CreateMemoRequest) (*v1pb.Memo, error) {\n\tuser, err := s.fetchCurrentUser(ctx)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to get user\")\n\t}\n\tif user == nil {\n\t\treturn nil, status.Errorf(codes.Unauthenticated, \"user not authenticated\")\n\t}\n\n\tmemoUID, err := ValidateAndGenerateUID(request.MemoId)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tcreate := &store.Memo{\n\t\tUID:        memoUID,\n\t\tCreatorID:  user.ID,\n\t\tContent:    request.Memo.Content,\n\t\tVisibility: convertVisibilityToStore(request.Memo.Visibility),\n\t}\n\n\tinstanceMemoRelatedSetting, err := s.Store.GetInstanceMemoRelatedSetting(ctx)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to get instance memo related setting\")\n\t}\n\n\t// Handle display_time first: if provided, use it to set the appropriate timestamp\n\t// based on the instance setting (similar to UpdateMemo logic)\n\t// Note: explicit create_time/update_time below will override this if provided\n\tif request.Memo.DisplayTime != nil && request.Memo.DisplayTime.IsValid() {\n\t\tdisplayTs := request.Memo.DisplayTime.AsTime().Unix()\n\t\tif instanceMemoRelatedSetting.DisplayWithUpdateTime {\n\t\t\tcreate.UpdatedTs = displayTs\n\t\t} else {\n\t\t\tcreate.CreatedTs = displayTs\n\t\t}\n\t}\n\n\t// Set custom timestamps if provided in the request\n\t// These take precedence over display_time\n\tif request.Memo.CreateTime != nil && request.Memo.CreateTime.IsValid() {\n\t\tcreatedTs := request.Memo.CreateTime.AsTime().Unix()\n\t\tcreate.CreatedTs = createdTs\n\t}\n\tif request.Memo.UpdateTime != nil && request.Memo.UpdateTime.IsValid() {\n\t\tupdatedTs := request.Memo.UpdateTime.AsTime().Unix()\n\t\tcreate.UpdatedTs = updatedTs\n\t}\n\n\tcontentLengthLimit, err := s.getContentLengthLimit(ctx)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to get content length limit\")\n\t}\n\tif len(create.Content) > contentLengthLimit {\n\t\treturn nil, status.Errorf(codes.InvalidArgument, \"content too long (max %d characters)\", contentLengthLimit)\n\t}\n\tif err := memopayload.RebuildMemoPayload(create, s.MarkdownService); err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to rebuild memo payload: %v\", err)\n\t}\n\tif request.Memo.Location != nil {\n\t\tcreate.Payload.Location = convertLocationToStore(request.Memo.Location)\n\t}\n\n\tmemo, err := s.Store.CreateMemo(ctx, create)\n\tif err != nil {\n\t\t// Check for unique constraint violation (AIP-133 compliance)\n\t\terrMsg := err.Error()\n\t\tif strings.Contains(errMsg, \"UNIQUE constraint failed\") ||\n\t\t\tstrings.Contains(errMsg, \"duplicate key\") ||\n\t\t\tstrings.Contains(errMsg, \"Duplicate entry\") {\n\t\t\treturn nil, status.Errorf(codes.AlreadyExists, \"memo with ID %q already exists\", memoUID)\n\t\t}\n\t\treturn nil, err\n\t}\n\n\tattachments := []*store.Attachment{}\n\n\tif len(request.Memo.Attachments) > 0 {\n\t\t_, err := s.SetMemoAttachments(ctx, &v1pb.SetMemoAttachmentsRequest{\n\t\t\tName:        fmt.Sprintf(\"%s%s\", MemoNamePrefix, memo.UID),\n\t\t\tAttachments: request.Memo.Attachments,\n\t\t})\n\t\tif err != nil {\n\t\t\treturn nil, errors.Wrap(err, \"failed to set memo attachments\")\n\t\t}\n\n\t\ta, err := s.Store.ListAttachments(ctx, &store.FindAttachment{\n\t\t\tMemoID: &memo.ID,\n\t\t})\n\t\tif err != nil {\n\t\t\treturn nil, errors.Wrap(err, \"failed to get memo attachments\")\n\t\t}\n\t\tattachments = a\n\t}\n\tif len(request.Memo.Relations) > 0 {\n\t\t_, err := s.SetMemoRelations(ctx, &v1pb.SetMemoRelationsRequest{\n\t\t\tName:      fmt.Sprintf(\"%s%s\", MemoNamePrefix, memo.UID),\n\t\t\tRelations: request.Memo.Relations,\n\t\t})\n\t\tif err != nil {\n\t\t\treturn nil, errors.Wrap(err, \"failed to set memo relations\")\n\t\t}\n\t}\n\n\trelations, err := s.loadMemoRelations(ctx, memo)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"failed to load memo relations\")\n\t}\n\tmemoMessage, err := s.convertMemoFromStore(ctx, memo, nil, attachments, relations)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"failed to convert memo\")\n\t}\n\t// Try to dispatch webhook when memo is created.\n\tif err := s.DispatchMemoCreatedWebhook(ctx, memoMessage); err != nil {\n\t\tslog.Warn(\"Failed to dispatch memo created webhook\", slog.Any(\"err\", err))\n\t}\n\n\t// Broadcast live refresh event.\n\ts.SSEHub.Broadcast(&SSEEvent{\n\t\tType: SSEEventMemoCreated,\n\t\tName: memoMessage.Name,\n\t})\n\n\treturn memoMessage, nil\n}\n\nfunc (s *APIV1Service) ListMemos(ctx context.Context, request *v1pb.ListMemosRequest) (*v1pb.ListMemosResponse, error) {\n\tmemoFind := &store.FindMemo{\n\t\t// Exclude comments by default.\n\t\tExcludeComments: true,\n\t}\n\tcurrentUser, err := s.fetchCurrentUser(ctx)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to get user\")\n\t}\n\n\tif request.State == v1pb.State_ARCHIVED {\n\t\tstate := store.Archived\n\t\tmemoFind.RowStatus = &state\n\t\t// Archived memos are only visible to their creator.\n\t\tif currentUser == nil {\n\t\t\treturn &v1pb.ListMemosResponse{}, nil\n\t\t}\n\t\tmemoFind.CreatorID = &currentUser.ID\n\t} else {\n\t\tstate := store.Normal\n\t\tmemoFind.RowStatus = &state\n\t}\n\n\t// Parse order_by field (replaces the old sort and direction fields)\n\tif request.OrderBy != \"\" {\n\t\tif err := s.parseMemoOrderBy(request.OrderBy, memoFind); err != nil {\n\t\t\treturn nil, status.Errorf(codes.InvalidArgument, \"invalid order_by: %v\", err)\n\t\t}\n\t} else {\n\t\t// Default ordering by display_time desc\n\t\tmemoFind.OrderByTimeAsc = false\n\t}\n\n\tif request.Filter != \"\" {\n\t\tif err := s.validateFilter(ctx, request.Filter); err != nil {\n\t\t\treturn nil, status.Errorf(codes.InvalidArgument, \"invalid filter: %v\", err)\n\t\t}\n\t\tmemoFind.Filters = append(memoFind.Filters, request.Filter)\n\t}\n\n\tif currentUser == nil {\n\t\tmemoFind.VisibilityList = []store.Visibility{store.Public}\n\t} else {\n\t\tif memoFind.CreatorID == nil {\n\t\t\tfilter := fmt.Sprintf(`creator_id == %d || visibility in [\"PUBLIC\", \"PROTECTED\"]`, currentUser.ID)\n\t\t\tmemoFind.Filters = append(memoFind.Filters, filter)\n\t\t} else if *memoFind.CreatorID != currentUser.ID {\n\t\t\tmemoFind.VisibilityList = []store.Visibility{store.Public, store.Protected}\n\t\t}\n\t}\n\n\tinstanceMemoRelatedSetting, err := s.Store.GetInstanceMemoRelatedSetting(ctx)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to get instance memo related setting\")\n\t}\n\tif instanceMemoRelatedSetting.DisplayWithUpdateTime {\n\t\tmemoFind.OrderByUpdatedTs = true\n\t}\n\n\tvar limit, offset int\n\tif request.PageToken != \"\" {\n\t\tvar pageToken v1pb.PageToken\n\t\tif err := unmarshalPageToken(request.PageToken, &pageToken); err != nil {\n\t\t\treturn nil, status.Errorf(codes.InvalidArgument, \"invalid page token: %v\", err)\n\t\t}\n\t\tlimit = int(pageToken.Limit)\n\t\toffset = int(pageToken.Offset)\n\t} else {\n\t\tlimit = int(request.PageSize)\n\t}\n\tif limit <= 0 {\n\t\tlimit = DefaultPageSize\n\t}\n\tlimitPlusOne := limit + 1\n\tmemoFind.Limit = &limitPlusOne\n\tmemoFind.Offset = &offset\n\tmemos, err := s.Store.ListMemos(ctx, memoFind)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to list memos: %v\", err)\n\t}\n\n\tmemoMessages := []*v1pb.Memo{}\n\tnextPageToken := \"\"\n\tif len(memos) == limitPlusOne {\n\t\tmemos = memos[:limit]\n\t\tnextPageToken, err = getPageToken(limit, offset+limit)\n\t\tif err != nil {\n\t\t\treturn nil, status.Errorf(codes.Internal, \"failed to get next page token, error: %v\", err)\n\t\t}\n\t}\n\n\tif len(memos) == 0 {\n\t\tresponse := &v1pb.ListMemosResponse{\n\t\t\tMemos:         memoMessages,\n\t\t\tNextPageToken: nextPageToken,\n\t\t}\n\t\treturn response, nil\n\t}\n\n\treactionMap := make(map[string][]*store.Reaction)\n\tcontentIDs := make([]string, 0, len(memos))\n\n\tattachmentMap := make(map[int32][]*store.Attachment)\n\tmemoIDs := make([]int32, 0, len(memos))\n\n\tfor _, m := range memos {\n\t\tcontentIDs = append(contentIDs, fmt.Sprintf(\"%s%s\", MemoNamePrefix, m.UID))\n\t\tmemoIDs = append(memoIDs, m.ID)\n\t}\n\n\t// REACTIONS\n\treactions, err := s.Store.ListReactions(ctx, &store.FindReaction{ContentIDList: contentIDs})\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to list reactions\")\n\t}\n\tfor _, reaction := range reactions {\n\t\treactionMap[reaction.ContentID] = append(reactionMap[reaction.ContentID], reaction)\n\t}\n\n\t// ATTACHMENTS\n\tattachments, err := s.Store.ListAttachments(ctx, &store.FindAttachment{MemoIDList: memoIDs})\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to list attachments\")\n\t}\n\tfor _, attachment := range attachments {\n\t\tattachmentMap[*attachment.MemoID] = append(attachmentMap[*attachment.MemoID], attachment)\n\t}\n\n\t// RELATIONS (batch load to avoid N+1)\n\trelationMap, err := s.batchConvertMemoRelations(ctx, memos)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to batch load memo relations\")\n\t}\n\n\tfor _, memo := range memos {\n\t\tmemoName := fmt.Sprintf(\"%s%s\", MemoNamePrefix, memo.UID)\n\t\treactions := reactionMap[memoName]\n\t\tattachments := attachmentMap[memo.ID]\n\t\trelations := relationMap[memo.ID]\n\n\t\tmemoMessage, err := s.convertMemoFromStore(ctx, memo, reactions, attachments, relations)\n\t\tif err != nil {\n\t\t\treturn nil, errors.Wrap(err, \"failed to convert memo\")\n\t\t}\n\n\t\tmemoMessages = append(memoMessages, memoMessage)\n\t}\n\n\tresponse := &v1pb.ListMemosResponse{\n\t\tMemos:         memoMessages,\n\t\tNextPageToken: nextPageToken,\n\t}\n\treturn response, nil\n}\n\nfunc (s *APIV1Service) GetMemo(ctx context.Context, request *v1pb.GetMemoRequest) (*v1pb.Memo, error) {\n\tmemoUID, err := ExtractMemoUIDFromName(request.Name)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.InvalidArgument, \"invalid memo name: %v\", err)\n\t}\n\tmemo, err := s.Store.GetMemo(ctx, &store.FindMemo{\n\t\tUID: &memoUID,\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif memo == nil {\n\t\treturn nil, status.Errorf(codes.NotFound, \"memo not found\")\n\t}\n\n\t// Archived memos are only visible to their creator.\n\tif memo.RowStatus == store.Archived {\n\t\tuser, err := s.fetchCurrentUser(ctx)\n\t\tif err != nil {\n\t\t\treturn nil, status.Errorf(codes.Internal, \"failed to get user\")\n\t\t}\n\t\tif user == nil || memo.CreatorID != user.ID {\n\t\t\treturn nil, status.Errorf(codes.NotFound, \"memo not found\")\n\t\t}\n\t}\n\n\tif memo.Visibility != store.Public {\n\t\tuser, err := s.fetchCurrentUser(ctx)\n\t\tif err != nil {\n\t\t\treturn nil, status.Errorf(codes.Internal, \"failed to get user\")\n\t\t}\n\t\tif user == nil {\n\t\t\treturn nil, status.Errorf(codes.Unauthenticated, \"user not authenticated\")\n\t\t}\n\t\tif memo.Visibility == store.Private && memo.CreatorID != user.ID {\n\t\t\treturn nil, status.Errorf(codes.PermissionDenied, \"permission denied\")\n\t\t}\n\t}\n\n\treactions, err := s.Store.ListReactions(ctx, &store.FindReaction{\n\t\tContentID: &request.Name,\n\t})\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to list reactions\")\n\t}\n\n\tattachments, err := s.Store.ListAttachments(ctx, &store.FindAttachment{\n\t\tMemoID: &memo.ID,\n\t})\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to list attachments\")\n\t}\n\n\trelations, err := s.loadMemoRelations(ctx, memo)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"failed to load memo relations\")\n\t}\n\tmemoMessage, err := s.convertMemoFromStore(ctx, memo, reactions, attachments, relations)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"failed to convert memo\")\n\t}\n\treturn memoMessage, nil\n}\n\nfunc (s *APIV1Service) UpdateMemo(ctx context.Context, request *v1pb.UpdateMemoRequest) (*v1pb.Memo, error) {\n\tmemoUID, err := ExtractMemoUIDFromName(request.Memo.Name)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.InvalidArgument, \"invalid memo name: %v\", err)\n\t}\n\tif request.UpdateMask == nil || len(request.UpdateMask.Paths) == 0 {\n\t\treturn nil, status.Errorf(codes.InvalidArgument, \"update mask is required\")\n\t}\n\n\tmemo, err := s.Store.GetMemo(ctx, &store.FindMemo{UID: &memoUID})\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to get memo: %v\", err)\n\t}\n\tif memo == nil {\n\t\treturn nil, status.Errorf(codes.NotFound, \"memo not found\")\n\t}\n\n\tuser, err := s.fetchCurrentUser(ctx)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to get current user\")\n\t}\n\tif user == nil {\n\t\treturn nil, status.Errorf(codes.Unauthenticated, \"user not authenticated\")\n\t}\n\t// Only the creator or admin can update the memo.\n\tif memo.CreatorID != user.ID && !isSuperUser(user) {\n\t\treturn nil, status.Errorf(codes.PermissionDenied, \"permission denied\")\n\t}\n\n\tupdate := &store.UpdateMemo{\n\t\tID: memo.ID,\n\t}\n\tfor _, path := range request.UpdateMask.Paths {\n\t\tif path == \"content\" {\n\t\t\tcontentLengthLimit, err := s.getContentLengthLimit(ctx)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, status.Errorf(codes.Internal, \"failed to get content length limit\")\n\t\t\t}\n\t\t\tif len(request.Memo.Content) > contentLengthLimit {\n\t\t\t\treturn nil, status.Errorf(codes.InvalidArgument, \"content too long (max %d characters)\", contentLengthLimit)\n\t\t\t}\n\t\t\tmemo.Content = request.Memo.Content\n\t\t\tif err := memopayload.RebuildMemoPayload(memo, s.MarkdownService); err != nil {\n\t\t\t\treturn nil, status.Errorf(codes.Internal, \"failed to rebuild memo payload: %v\", err)\n\t\t\t}\n\t\t\tupdate.Content = &memo.Content\n\t\t\tupdate.Payload = memo.Payload\n\t\t} else if path == \"visibility\" {\n\t\t\tvisibility := convertVisibilityToStore(request.Memo.Visibility)\n\t\t\tupdate.Visibility = &visibility\n\t\t} else if path == \"pinned\" {\n\t\t\tupdate.Pinned = &request.Memo.Pinned\n\t\t} else if path == \"state\" {\n\t\t\trowStatus := convertStateToStore(request.Memo.State)\n\t\t\tupdate.RowStatus = &rowStatus\n\t\t} else if path == \"create_time\" {\n\t\t\tcreatedTs := request.Memo.CreateTime.AsTime().Unix()\n\t\t\tupdate.CreatedTs = &createdTs\n\t\t} else if path == \"update_time\" {\n\t\t\tupdatedTs := time.Now().Unix()\n\t\t\tif request.Memo.UpdateTime != nil {\n\t\t\t\tupdatedTs = request.Memo.UpdateTime.AsTime().Unix()\n\t\t\t}\n\t\t\tupdate.UpdatedTs = &updatedTs\n\t\t} else if path == \"display_time\" {\n\t\t\tdisplayTs := request.Memo.DisplayTime.AsTime().Unix()\n\t\t\tmemoRelatedSetting, err := s.Store.GetInstanceMemoRelatedSetting(ctx)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, status.Errorf(codes.Internal, \"failed to get instance memo related setting\")\n\t\t\t}\n\t\t\tif memoRelatedSetting.DisplayWithUpdateTime {\n\t\t\t\tupdate.UpdatedTs = &displayTs\n\t\t\t} else {\n\t\t\t\tupdate.CreatedTs = &displayTs\n\t\t\t}\n\t\t} else if path == \"location\" {\n\t\t\tpayload := memo.Payload\n\t\t\tpayload.Location = convertLocationToStore(request.Memo.Location)\n\t\t\tupdate.Payload = payload\n\t\t} else if path == \"attachments\" {\n\t\t\t_, err := s.SetMemoAttachments(ctx, &v1pb.SetMemoAttachmentsRequest{\n\t\t\t\tName:        request.Memo.Name,\n\t\t\t\tAttachments: request.Memo.Attachments,\n\t\t\t})\n\t\t\tif err != nil {\n\t\t\t\treturn nil, errors.Wrap(err, \"failed to set memo attachments\")\n\t\t\t}\n\t\t} else if path == \"relations\" {\n\t\t\t_, err := s.SetMemoRelations(ctx, &v1pb.SetMemoRelationsRequest{\n\t\t\t\tName:      request.Memo.Name,\n\t\t\t\tRelations: request.Memo.Relations,\n\t\t\t})\n\t\t\tif err != nil {\n\t\t\t\treturn nil, errors.Wrap(err, \"failed to set memo relations\")\n\t\t\t}\n\t\t}\n\t}\n\n\tif err = s.Store.UpdateMemo(ctx, update); err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to update memo\")\n\t}\n\n\tmemo, err = s.Store.GetMemo(ctx, &store.FindMemo{\n\t\tID: &memo.ID,\n\t})\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"failed to get memo\")\n\t}\n\treactions, err := s.Store.ListReactions(ctx, &store.FindReaction{\n\t\tContentID: &request.Memo.Name,\n\t})\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to list reactions\")\n\t}\n\tattachments, err := s.Store.ListAttachments(ctx, &store.FindAttachment{\n\t\tMemoID: &memo.ID,\n\t})\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to list attachments\")\n\t}\n\n\trelations, err := s.loadMemoRelations(ctx, memo)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"failed to load memo relations\")\n\t}\n\tmemoMessage, err := s.convertMemoFromStore(ctx, memo, reactions, attachments, relations)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"failed to convert memo\")\n\t}\n\t// Try to dispatch webhook when memo is updated.\n\tif err := s.DispatchMemoUpdatedWebhook(ctx, memoMessage); err != nil {\n\t\tslog.Warn(\"Failed to dispatch memo updated webhook\", slog.Any(\"err\", err))\n\t}\n\n\t// Broadcast live refresh event.\n\ts.SSEHub.Broadcast(&SSEEvent{\n\t\tType: SSEEventMemoUpdated,\n\t\tName: memoMessage.Name,\n\t})\n\n\treturn memoMessage, nil\n}\n\nfunc (s *APIV1Service) DeleteMemo(ctx context.Context, request *v1pb.DeleteMemoRequest) (*emptypb.Empty, error) {\n\tmemoUID, err := ExtractMemoUIDFromName(request.Name)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.InvalidArgument, \"invalid memo name: %v\", err)\n\t}\n\tmemo, err := s.Store.GetMemo(ctx, &store.FindMemo{\n\t\tUID: &memoUID,\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif memo == nil {\n\t\treturn nil, status.Errorf(codes.NotFound, \"memo not found\")\n\t}\n\n\tuser, err := s.fetchCurrentUser(ctx)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to get current user\")\n\t}\n\tif user == nil {\n\t\treturn nil, status.Errorf(codes.Unauthenticated, \"user not authenticated\")\n\t}\n\t// Only the creator or admin can update the memo.\n\tif memo.CreatorID != user.ID && !isSuperUser(user) {\n\t\treturn nil, status.Errorf(codes.PermissionDenied, \"permission denied\")\n\t}\n\n\treactions, err := s.Store.ListReactions(ctx, &store.FindReaction{\n\t\tContentID: &request.Name,\n\t})\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to list reactions\")\n\t}\n\n\tattachments, err := s.Store.ListAttachments(ctx, &store.FindAttachment{\n\t\tMemoID: &memo.ID,\n\t})\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to list attachments\")\n\t}\n\n\tdeleteRelations, _ := s.loadMemoRelations(ctx, memo)\n\tif memoMessage, err := s.convertMemoFromStore(ctx, memo, reactions, attachments, deleteRelations); err == nil {\n\t\t// Try to dispatch webhook when memo is deleted.\n\t\tif err := s.DispatchMemoDeletedWebhook(ctx, memoMessage); err != nil {\n\t\t\tslog.Warn(\"Failed to dispatch memo deleted webhook\", slog.Any(\"err\", err))\n\t\t}\n\t}\n\n\t// Delete memo comments first (store.DeleteMemo handles their relations and attachments)\n\tcommentType := store.MemoRelationComment\n\trelations, err := s.Store.ListMemoRelations(ctx, &store.FindMemoRelation{RelatedMemoID: &memo.ID, Type: &commentType})\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to list memo comments\")\n\t}\n\tfor _, relation := range relations {\n\t\tif err := s.Store.DeleteMemo(ctx, &store.DeleteMemo{ID: relation.MemoID}); err != nil {\n\t\t\treturn nil, status.Errorf(codes.Internal, \"failed to delete memo comment\")\n\t\t}\n\t}\n\n\t// Delete the memo (store.DeleteMemo handles relation and attachment cleanup)\n\tif err = s.Store.DeleteMemo(ctx, &store.DeleteMemo{ID: memo.ID}); err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to delete memo\")\n\t}\n\n\t// Broadcast live refresh event.\n\ts.SSEHub.Broadcast(&SSEEvent{\n\t\tType: SSEEventMemoDeleted,\n\t\tName: request.Name,\n\t})\n\n\treturn &emptypb.Empty{}, nil\n}\n\nfunc (s *APIV1Service) CreateMemoComment(ctx context.Context, request *v1pb.CreateMemoCommentRequest) (*v1pb.Memo, error) {\n\tmemoUID, err := ExtractMemoUIDFromName(request.Name)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.InvalidArgument, \"invalid memo name: %v\", err)\n\t}\n\trelatedMemo, err := s.Store.GetMemo(ctx, &store.FindMemo{UID: &memoUID})\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to get memo\")\n\t}\n\tif relatedMemo == nil {\n\t\treturn nil, status.Errorf(codes.NotFound, \"memo not found\")\n\t}\n\n\t// Check memo visibility before allowing comment.\n\tuser, err := s.fetchCurrentUser(ctx)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to get user\")\n\t}\n\tif user == nil {\n\t\treturn nil, status.Errorf(codes.Unauthenticated, \"user not authenticated\")\n\t}\n\tif relatedMemo.Visibility == store.Private && relatedMemo.CreatorID != user.ID && !isSuperUser(user) {\n\t\treturn nil, status.Errorf(codes.PermissionDenied, \"permission denied\")\n\t}\n\n\t// Create the memo comment first.\n\tmemoComment, err := s.CreateMemo(ctx, &v1pb.CreateMemoRequest{\n\t\tMemo:   request.Comment,\n\t\tMemoId: request.CommentId,\n\t})\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to create memo\")\n\t}\n\tmemoUID, err = ExtractMemoUIDFromName(memoComment.Name)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.InvalidArgument, \"invalid memo name: %v\", err)\n\t}\n\tmemo, err := s.Store.GetMemo(ctx, &store.FindMemo{UID: &memoUID})\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to get memo\")\n\t}\n\n\t// Build the relation between the comment memo and the original memo.\n\t_, err = s.Store.UpsertMemoRelation(ctx, &store.MemoRelation{\n\t\tMemoID:        memo.ID,\n\t\tRelatedMemoID: relatedMemo.ID,\n\t\tType:          store.MemoRelationComment,\n\t})\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to create memo relation\")\n\t}\n\tcreatorID, err := ExtractUserIDFromName(memoComment.Creator)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.InvalidArgument, \"invalid memo creator\")\n\t}\n\tif memoComment.Visibility != v1pb.Visibility_PRIVATE && creatorID != relatedMemo.CreatorID {\n\t\tif _, err := s.Store.CreateInbox(ctx, &store.Inbox{\n\t\t\tSenderID:   creatorID,\n\t\t\tReceiverID: relatedMemo.CreatorID,\n\t\t\tStatus:     store.UNREAD,\n\t\t\tMessage: &storepb.InboxMessage{\n\t\t\t\tType: storepb.InboxMessage_MEMO_COMMENT,\n\t\t\t\tPayload: &storepb.InboxMessage_MemoComment{\n\t\t\t\t\tMemoComment: &storepb.InboxMessage_MemoCommentPayload{\n\t\t\t\t\t\tMemoId:        memo.ID,\n\t\t\t\t\t\tRelatedMemoId: relatedMemo.ID,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t}); err != nil {\n\t\t\treturn nil, status.Errorf(codes.Internal, \"failed to create inbox\")\n\t\t}\n\t}\n\n\tif err := s.DispatchMemoCommentCreatedWebhook(ctx, memoComment, relatedMemo.CreatorID); err != nil {\n\t\tslog.Warn(\"Failed to dispatch memo comment created webhook\", slog.Any(\"err\", err))\n\t}\n\n\t// Broadcast live refresh event for the parent memo so subscribers see the new comment.\n\ts.SSEHub.Broadcast(&SSEEvent{\n\t\tType: SSEEventMemoCommentCreated,\n\t\tName: request.Name,\n\t})\n\n\treturn memoComment, nil\n}\n\nfunc (s *APIV1Service) ListMemoComments(ctx context.Context, request *v1pb.ListMemoCommentsRequest) (*v1pb.ListMemoCommentsResponse, error) {\n\tmemoUID, err := ExtractMemoUIDFromName(request.Name)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.InvalidArgument, \"invalid memo name: %v\", err)\n\t}\n\tmemo, err := s.Store.GetMemo(ctx, &store.FindMemo{UID: &memoUID})\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to get memo\")\n\t}\n\n\tcurrentUser, err := s.fetchCurrentUser(ctx)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to get user\")\n\t}\n\tvar memoFilter string\n\tif currentUser == nil {\n\t\tmemoFilter = `visibility == \"PUBLIC\"`\n\t} else {\n\t\tmemoFilter = fmt.Sprintf(`creator_id == %d || visibility in [\"PUBLIC\", \"PROTECTED\"]`, currentUser.ID)\n\t}\n\tmemoRelationComment := store.MemoRelationComment\n\tmemoRelations, err := s.Store.ListMemoRelations(ctx, &store.FindMemoRelation{\n\t\tRelatedMemoID: &memo.ID,\n\t\tType:          &memoRelationComment,\n\t\tMemoFilter:    &memoFilter,\n\t})\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to list memo relations\")\n\t}\n\n\tif len(memoRelations) == 0 {\n\t\tresponse := &v1pb.ListMemoCommentsResponse{\n\t\t\tMemos: []*v1pb.Memo{},\n\t\t}\n\t\treturn response, nil\n\t}\n\n\tmemoRelationIDs := make([]int32, 0, len(memoRelations))\n\tfor _, m := range memoRelations {\n\t\tmemoRelationIDs = append(memoRelationIDs, m.MemoID)\n\t}\n\tmemos, err := s.Store.ListMemos(ctx, &store.FindMemo{IDList: memoRelationIDs})\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to list memos\")\n\t}\n\n\tmemoIDToNameMap := make(map[int32]string)\n\tcontentIDs := make([]string, 0, len(memos))\n\tmemoIDsForAttachments := make([]int32, 0, len(memos))\n\n\tfor _, memo := range memos {\n\t\tmemoName := fmt.Sprintf(\"%s%s\", MemoNamePrefix, memo.UID)\n\t\tmemoIDToNameMap[memo.ID] = memoName\n\t\tcontentIDs = append(contentIDs, memoName)\n\t\tmemoIDsForAttachments = append(memoIDsForAttachments, memo.ID)\n\t}\n\treactions, err := s.Store.ListReactions(ctx, &store.FindReaction{ContentIDList: contentIDs})\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to list reactions\")\n\t}\n\n\tmemoReactionsMap := make(map[string][]*store.Reaction)\n\tfor _, reaction := range reactions {\n\t\tmemoReactionsMap[reaction.ContentID] = append(memoReactionsMap[reaction.ContentID], reaction)\n\t}\n\n\tattachments, err := s.Store.ListAttachments(ctx, &store.FindAttachment{MemoIDList: memoIDsForAttachments})\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to list attachments\")\n\t}\n\tattachmentMap := make(map[int32][]*store.Attachment)\n\tfor _, attachment := range attachments {\n\t\tattachmentMap[*attachment.MemoID] = append(attachmentMap[*attachment.MemoID], attachment)\n\t}\n\n\t// RELATIONS (batch load to avoid N+1)\n\trelationMap, err := s.batchConvertMemoRelations(ctx, memos)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to batch load memo relations\")\n\t}\n\n\tvar memosResponse []*v1pb.Memo\n\tfor _, m := range memos {\n\t\tmemoName := memoIDToNameMap[m.ID]\n\t\treactions := memoReactionsMap[memoName]\n\t\tattachments := attachmentMap[m.ID]\n\t\trelations := relationMap[m.ID]\n\n\t\tmemoMessage, err := s.convertMemoFromStore(ctx, m, reactions, attachments, relations)\n\t\tif err != nil {\n\t\t\treturn nil, errors.Wrap(err, \"failed to convert memo\")\n\t\t}\n\t\tmemosResponse = append(memosResponse, memoMessage)\n\t}\n\n\tresponse := &v1pb.ListMemoCommentsResponse{\n\t\tMemos: memosResponse,\n\t}\n\treturn response, nil\n}\n\nfunc (s *APIV1Service) getContentLengthLimit(ctx context.Context) (int, error) {\n\tinstanceMemoRelatedSetting, err := s.Store.GetInstanceMemoRelatedSetting(ctx)\n\tif err != nil {\n\t\treturn 0, status.Errorf(codes.Internal, \"failed to get instance memo related setting\")\n\t}\n\treturn int(instanceMemoRelatedSetting.ContentLengthLimit), nil\n}\n\n// DispatchMemoCreatedWebhook dispatches webhook when memo is created.\nfunc (s *APIV1Service) DispatchMemoCreatedWebhook(ctx context.Context, memo *v1pb.Memo) error {\n\treturn s.dispatchMemoRelatedWebhook(ctx, memo, \"memos.memo.created\")\n}\n\n// DispatchMemoUpdatedWebhook dispatches webhook when memo is updated.\nfunc (s *APIV1Service) DispatchMemoUpdatedWebhook(ctx context.Context, memo *v1pb.Memo) error {\n\treturn s.dispatchMemoRelatedWebhook(ctx, memo, \"memos.memo.updated\")\n}\n\n// DispatchMemoDeletedWebhook dispatches webhook when memo is deleted.\nfunc (s *APIV1Service) DispatchMemoDeletedWebhook(ctx context.Context, memo *v1pb.Memo) error {\n\treturn s.dispatchMemoRelatedWebhook(ctx, memo, \"memos.memo.deleted\")\n}\n\n// DispatchMemoCommentCreatedWebhook dispatches webhook to the related memo owner when a comment is created.\nfunc (s *APIV1Service) DispatchMemoCommentCreatedWebhook(ctx context.Context, commentMemo *v1pb.Memo, relatedMemoCreatorID int32) error {\n\twebhooks, err := s.Store.GetUserWebhooks(ctx, relatedMemoCreatorID)\n\tif err != nil {\n\t\treturn err\n\t}\n\tfor _, hook := range webhooks {\n\t\tpayload, err := convertMemoToWebhookPayload(commentMemo)\n\t\tif err != nil {\n\t\t\treturn errors.Wrap(err, \"failed to convert memo to webhook payload\")\n\t\t}\n\t\tpayload.ActivityType = \"memos.memo.comment.created\"\n\t\tpayload.URL = hook.Url\n\t\twebhook.PostAsync(payload)\n\t}\n\treturn nil\n}\n\nfunc (s *APIV1Service) dispatchMemoRelatedWebhook(ctx context.Context, memo *v1pb.Memo, activityType string) error {\n\tcreatorID, err := ExtractUserIDFromName(memo.Creator)\n\tif err != nil {\n\t\treturn status.Errorf(codes.InvalidArgument, \"invalid memo creator\")\n\t}\n\twebhooks, err := s.Store.GetUserWebhooks(ctx, creatorID)\n\tif err != nil {\n\t\treturn err\n\t}\n\tfor _, hook := range webhooks {\n\t\tpayload, err := convertMemoToWebhookPayload(memo)\n\t\tif err != nil {\n\t\t\treturn errors.Wrap(err, \"failed to convert memo to webhook payload\")\n\t\t}\n\t\tpayload.ActivityType = activityType\n\t\tpayload.URL = hook.Url\n\n\t\t// Use asynchronous webhook dispatch\n\t\twebhook.PostAsync(payload)\n\t}\n\treturn nil\n}\n\nfunc convertMemoToWebhookPayload(memo *v1pb.Memo) (*webhook.WebhookRequestPayload, error) {\n\tcreatorID, err := ExtractUserIDFromName(memo.Creator)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"invalid memo creator\")\n\t}\n\treturn &webhook.WebhookRequestPayload{\n\t\tCreator: fmt.Sprintf(\"%s%d\", UserNamePrefix, creatorID),\n\t\tMemo:    memo,\n\t}, nil\n}\n\nfunc (s *APIV1Service) getMemoContentSnippet(content string) (string, error) {\n\t// Use goldmark service for snippet generation\n\tsnippet, err := s.MarkdownService.GenerateSnippet([]byte(content), 64)\n\tif err != nil {\n\t\treturn \"\", errors.Wrap(err, \"failed to generate snippet\")\n\t}\n\treturn snippet, nil\n}\n\n// parseMemoOrderBy parses the order_by field and sets the appropriate ordering in memoFind.\n// Follows AIP-132: supports comma-separated list of fields with optional \"desc\" suffix.\n// Example: \"pinned desc, display_time desc\" or \"create_time asc\".\nfunc (*APIV1Service) parseMemoOrderBy(orderBy string, memoFind *store.FindMemo) error {\n\tif strings.TrimSpace(orderBy) == \"\" {\n\t\treturn errors.New(\"empty order_by\")\n\t}\n\n\t// Split by comma to support multiple sort fields per AIP-132.\n\tfields := strings.Split(orderBy, \",\")\n\n\t// Track if we've seen pinned field.\n\thasPinned := false\n\n\tfor _, field := range fields {\n\t\tparts := strings.Fields(strings.TrimSpace(field))\n\t\tif len(parts) == 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\tfieldName := parts[0]\n\t\tfieldDirection := \"desc\" // default per AIP-132 (we use desc as default for time fields)\n\t\tif len(parts) > 1 {\n\t\t\tfieldDirection = strings.ToLower(parts[1])\n\t\t\tif fieldDirection != \"asc\" && fieldDirection != \"desc\" {\n\t\t\t\treturn errors.Errorf(\"invalid order direction: %s, must be 'asc' or 'desc'\", parts[1])\n\t\t\t}\n\t\t}\n\n\t\tswitch fieldName {\n\t\tcase \"pinned\":\n\t\t\thasPinned = true\n\t\t\tmemoFind.OrderByPinned = true\n\t\t\t// Note: pinned is always DESC (true first) regardless of direction specified.\n\t\tcase \"display_time\", \"create_time\", \"name\":\n\t\t\t// Only set if this is the first time field we encounter.\n\t\t\tif !memoFind.OrderByUpdatedTs {\n\t\t\t\tmemoFind.OrderByTimeAsc = fieldDirection == \"asc\"\n\t\t\t}\n\t\tcase \"update_time\":\n\t\t\tmemoFind.OrderByUpdatedTs = true\n\t\t\tmemoFind.OrderByTimeAsc = fieldDirection == \"asc\"\n\t\tdefault:\n\t\t\treturn errors.Errorf(\"unsupported order field: %s, supported fields are: pinned, display_time, create_time, update_time, name\", fieldName)\n\t\t}\n\t}\n\n\t// If only pinned was specified, still need to set a default time ordering.\n\tif hasPinned && !memoFind.OrderByUpdatedTs && len(fields) == 1 {\n\t\tmemoFind.OrderByTimeAsc = false // default to desc\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "server/router/api/v1/memo_service_converter.go",
    "content": "package v1\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/pkg/errors\"\n\t\"google.golang.org/protobuf/types/known/timestamppb\"\n\n\tv1pb \"github.com/usememos/memos/proto/gen/api/v1\"\n\tstorepb \"github.com/usememos/memos/proto/gen/store\"\n\t\"github.com/usememos/memos/store\"\n)\n\nfunc (s *APIV1Service) convertMemoFromStore(ctx context.Context, memo *store.Memo, reactions []*store.Reaction, attachments []*store.Attachment, relations []*v1pb.MemoRelation) (*v1pb.Memo, error) {\n\tdisplayTs := memo.CreatedTs\n\tinstanceMemoRelatedSetting, err := s.Store.GetInstanceMemoRelatedSetting(ctx)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"failed to get instance memo related setting\")\n\t}\n\tif instanceMemoRelatedSetting.DisplayWithUpdateTime {\n\t\tdisplayTs = memo.UpdatedTs\n\t}\n\n\tname := fmt.Sprintf(\"%s%s\", MemoNamePrefix, memo.UID)\n\tmemoMessage := &v1pb.Memo{\n\t\tName:        name,\n\t\tState:       convertStateFromStore(memo.RowStatus),\n\t\tCreator:     fmt.Sprintf(\"%s%d\", UserNamePrefix, memo.CreatorID),\n\t\tCreateTime:  timestamppb.New(time.Unix(memo.CreatedTs, 0)),\n\t\tUpdateTime:  timestamppb.New(time.Unix(memo.UpdatedTs, 0)),\n\t\tDisplayTime: timestamppb.New(time.Unix(displayTs, 0)),\n\t\tContent:     memo.Content,\n\t\tVisibility:  convertVisibilityFromStore(memo.Visibility),\n\t\tPinned:      memo.Pinned,\n\t}\n\tif memo.Payload != nil {\n\t\tmemoMessage.Tags = memo.Payload.Tags\n\t\tmemoMessage.Property = convertMemoPropertyFromStore(memo.Payload.Property)\n\t\tmemoMessage.Location = convertLocationFromStore(memo.Payload.Location)\n\t}\n\n\tif memo.ParentUID != nil {\n\t\tparentName := fmt.Sprintf(\"%s%s\", MemoNamePrefix, *memo.ParentUID)\n\t\tmemoMessage.Parent = &parentName\n\t}\n\n\tmemoMessage.Reactions = []*v1pb.Reaction{}\n\tfor _, reaction := range reactions {\n\t\treactionResponse := convertReactionFromStore(reaction)\n\t\tmemoMessage.Reactions = append(memoMessage.Reactions, reactionResponse)\n\t}\n\n\tif relations != nil {\n\t\tmemoMessage.Relations = relations\n\t} else {\n\t\tmemoMessage.Relations = []*v1pb.MemoRelation{}\n\t}\n\n\tmemoMessage.Attachments = []*v1pb.Attachment{}\n\tfor _, attachment := range attachments {\n\t\tattachmentResponse := convertAttachmentFromStore(attachment)\n\t\tmemoMessage.Attachments = append(memoMessage.Attachments, attachmentResponse)\n\t}\n\n\tsnippet, err := s.getMemoContentSnippet(memo.Content)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"failed to get memo content snippet\")\n\t}\n\tmemoMessage.Snippet = snippet\n\n\treturn memoMessage, nil\n}\n\n// batchConvertMemoRelations batch-loads relations for a list of memos and returns\n// a map from memo ID to its converted relations. This avoids N+1 queries when listing memos.\nfunc (s *APIV1Service) batchConvertMemoRelations(ctx context.Context, memos []*store.Memo) (map[int32][]*v1pb.MemoRelation, error) {\n\tif len(memos) == 0 {\n\t\treturn map[int32][]*v1pb.MemoRelation{}, nil\n\t}\n\n\tcurrentUser, err := s.fetchCurrentUser(ctx)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"failed to get user\")\n\t}\n\tvar memoFilter string\n\tif currentUser == nil {\n\t\tmemoFilter = `visibility == \"PUBLIC\"`\n\t} else {\n\t\tmemoFilter = fmt.Sprintf(`creator_id == %d || visibility in [\"PUBLIC\", \"PROTECTED\"]`, currentUser.ID)\n\t}\n\n\tmemoIDs := make([]int32, len(memos))\n\tmemoIDSet := make(map[int32]bool, len(memos))\n\tfor i, m := range memos {\n\t\tmemoIDs[i] = m.ID\n\t\tmemoIDSet[m.ID] = true\n\t}\n\n\t// Single batch query to get all relations involving any of these memos.\n\tallRelations, err := s.Store.ListMemoRelations(ctx, &store.FindMemoRelation{\n\t\tMemoIDList: memoIDs,\n\t\tMemoFilter: &memoFilter,\n\t})\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"failed to batch list memo relations\")\n\t}\n\n\t// Collect all memo IDs referenced in relations that we need to resolve.\n\tneededIDs := make(map[int32]bool)\n\tfor _, r := range allRelations {\n\t\tneededIDs[r.MemoID] = true\n\t\tneededIDs[r.RelatedMemoID] = true\n\t}\n\n\t// Build ID→UID map from the memos we already have.\n\tmemoIDToUID := make(map[int32]string, len(memos))\n\tmemoIDToContent := make(map[int32]string, len(memos))\n\tfor _, m := range memos {\n\t\tmemoIDToUID[m.ID] = m.UID\n\t\tmemoIDToContent[m.ID] = m.Content\n\t\tdelete(neededIDs, m.ID)\n\t}\n\n\t// Batch fetch any additional memos referenced by relations that we don't already have.\n\tif len(neededIDs) > 0 {\n\t\textraIDs := make([]int32, 0, len(neededIDs))\n\t\tfor id := range neededIDs {\n\t\t\textraIDs = append(extraIDs, id)\n\t\t}\n\t\textraMemos, err := s.Store.ListMemos(ctx, &store.FindMemo{IDList: extraIDs})\n\t\tif err != nil {\n\t\t\treturn nil, errors.Wrap(err, \"failed to batch fetch related memos\")\n\t\t}\n\t\tfor _, m := range extraMemos {\n\t\t\tmemoIDToUID[m.ID] = m.UID\n\t\t\tmemoIDToContent[m.ID] = m.Content\n\t\t}\n\t}\n\n\t// Build the result map: memo ID → its relations (both directions).\n\tresult := make(map[int32][]*v1pb.MemoRelation, len(memos))\n\tfor _, r := range allRelations {\n\t\tmemoUID, ok1 := memoIDToUID[r.MemoID]\n\t\trelatedUID, ok2 := memoIDToUID[r.RelatedMemoID]\n\t\tif !ok1 || !ok2 {\n\t\t\tcontinue\n\t\t}\n\n\t\tmemoSnippet, _ := s.getMemoContentSnippet(memoIDToContent[r.MemoID])\n\t\trelatedSnippet, _ := s.getMemoContentSnippet(memoIDToContent[r.RelatedMemoID])\n\t\trelation := &v1pb.MemoRelation{\n\t\t\tMemo: &v1pb.MemoRelation_Memo{\n\t\t\t\tName:    fmt.Sprintf(\"%s%s\", MemoNamePrefix, memoUID),\n\t\t\t\tSnippet: memoSnippet,\n\t\t\t},\n\t\t\tRelatedMemo: &v1pb.MemoRelation_Memo{\n\t\t\t\tName:    fmt.Sprintf(\"%s%s\", MemoNamePrefix, relatedUID),\n\t\t\t\tSnippet: relatedSnippet,\n\t\t\t},\n\t\t\tType: convertMemoRelationTypeFromStore(r.Type),\n\t\t}\n\n\t\t// Add to the memo that owns this relation (both directions).\n\t\tif memoIDSet[r.MemoID] {\n\t\t\tresult[r.MemoID] = append(result[r.MemoID], relation)\n\t\t}\n\t\tif memoIDSet[r.RelatedMemoID] {\n\t\t\tresult[r.RelatedMemoID] = append(result[r.RelatedMemoID], relation)\n\t\t}\n\t}\n\n\treturn result, nil\n}\n\n// loadMemoRelations loads relations for a single memo and converts them to API format.\nfunc (s *APIV1Service) loadMemoRelations(ctx context.Context, memo *store.Memo) ([]*v1pb.MemoRelation, error) {\n\trelationMap, err := s.batchConvertMemoRelations(ctx, []*store.Memo{memo})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn relationMap[memo.ID], nil\n}\n\nfunc convertMemoPropertyFromStore(property *storepb.MemoPayload_Property) *v1pb.Memo_Property {\n\tif property == nil {\n\t\treturn nil\n\t}\n\treturn &v1pb.Memo_Property{\n\t\tHasLink:            property.HasLink,\n\t\tHasTaskList:        property.HasTaskList,\n\t\tHasCode:            property.HasCode,\n\t\tHasIncompleteTasks: property.HasIncompleteTasks,\n\t\tTitle:              property.Title,\n\t}\n}\n\nfunc convertLocationFromStore(location *storepb.MemoPayload_Location) *v1pb.Location {\n\tif location == nil {\n\t\treturn nil\n\t}\n\treturn &v1pb.Location{\n\t\tPlaceholder: location.Placeholder,\n\t\tLatitude:    location.Latitude,\n\t\tLongitude:   location.Longitude,\n\t}\n}\n\nfunc convertLocationToStore(location *v1pb.Location) *storepb.MemoPayload_Location {\n\tif location == nil {\n\t\treturn nil\n\t}\n\treturn &storepb.MemoPayload_Location{\n\t\tPlaceholder: location.Placeholder,\n\t\tLatitude:    location.Latitude,\n\t\tLongitude:   location.Longitude,\n\t}\n}\n\nfunc convertVisibilityFromStore(visibility store.Visibility) v1pb.Visibility {\n\tswitch visibility {\n\tcase store.Private:\n\t\treturn v1pb.Visibility_PRIVATE\n\tcase store.Protected:\n\t\treturn v1pb.Visibility_PROTECTED\n\tcase store.Public:\n\t\treturn v1pb.Visibility_PUBLIC\n\tdefault:\n\t\treturn v1pb.Visibility_VISIBILITY_UNSPECIFIED\n\t}\n}\n\nfunc convertVisibilityToStore(visibility v1pb.Visibility) store.Visibility {\n\tswitch visibility {\n\tcase v1pb.Visibility_PROTECTED:\n\t\treturn store.Protected\n\tcase v1pb.Visibility_PUBLIC:\n\t\treturn store.Public\n\tdefault:\n\t\treturn store.Private\n\t}\n}\n"
  },
  {
    "path": "server/router/api/v1/memo_service_filter.go",
    "content": "package v1\n"
  },
  {
    "path": "server/router/api/v1/memo_share_service.go",
    "content": "package v1\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"google.golang.org/grpc/codes\"\n\t\"google.golang.org/grpc/status\"\n\t\"google.golang.org/protobuf/types/known/emptypb\"\n\t\"google.golang.org/protobuf/types/known/timestamppb\"\n\n\t\"github.com/lithammer/shortuuid/v4\"\n\t\"github.com/pkg/errors\"\n\n\tv1pb \"github.com/usememos/memos/proto/gen/api/v1\"\n\t\"github.com/usememos/memos/store\"\n)\n\n// CreateMemoShare creates an opaque share link for a memo.\n// Only the memo's creator or an admin may call this.\nfunc (s *APIV1Service) CreateMemoShare(ctx context.Context, request *v1pb.CreateMemoShareRequest) (*v1pb.MemoShare, error) {\n\tuser, err := s.fetchCurrentUser(ctx)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to get user\")\n\t}\n\tif user == nil {\n\t\treturn nil, status.Errorf(codes.Unauthenticated, \"user not authenticated\")\n\t}\n\n\tmemoUID, err := ExtractMemoUIDFromName(request.Parent)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.InvalidArgument, \"invalid memo name: %v\", err)\n\t}\n\tmemo, err := s.Store.GetMemo(ctx, &store.FindMemo{UID: &memoUID})\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to get memo\")\n\t}\n\tif memo == nil {\n\t\treturn nil, status.Errorf(codes.NotFound, \"memo not found\")\n\t}\n\tif memo.CreatorID != user.ID && !isSuperUser(user) {\n\t\treturn nil, status.Errorf(codes.PermissionDenied, \"permission denied\")\n\t}\n\n\tvar expiresTs *int64\n\tif request.MemoShare != nil && request.MemoShare.ExpireTime != nil {\n\t\tts := request.MemoShare.ExpireTime.AsTime().Unix()\n\t\tif ts <= time.Now().Unix() {\n\t\t\treturn nil, status.Errorf(codes.InvalidArgument, \"expire_time must be in the future\")\n\t\t}\n\t\texpiresTs = &ts\n\t}\n\n\t// Generate a URL-safe token using shortuuid (base57-encoded UUID v4, 22 chars, 122-bit entropy).\n\tms, err := s.Store.CreateMemoShare(ctx, &store.MemoShare{\n\t\tUID:       shortuuid.New(),\n\t\tMemoID:    memo.ID,\n\t\tCreatorID: user.ID,\n\t\tExpiresTs: expiresTs,\n\t})\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to create memo share\")\n\t}\n\n\treturn convertMemoShareFromStore(ms, memo.UID), nil\n}\n\n// ListMemoShares lists all share links for a memo.\n// Only the memo's creator or an admin may call this.\nfunc (s *APIV1Service) ListMemoShares(ctx context.Context, request *v1pb.ListMemoSharesRequest) (*v1pb.ListMemoSharesResponse, error) {\n\tuser, err := s.fetchCurrentUser(ctx)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to get user\")\n\t}\n\tif user == nil {\n\t\treturn nil, status.Errorf(codes.Unauthenticated, \"user not authenticated\")\n\t}\n\n\tmemoUID, err := ExtractMemoUIDFromName(request.Parent)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.InvalidArgument, \"invalid memo name: %v\", err)\n\t}\n\tmemo, err := s.Store.GetMemo(ctx, &store.FindMemo{UID: &memoUID})\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to get memo\")\n\t}\n\tif memo == nil {\n\t\treturn nil, status.Errorf(codes.NotFound, \"memo not found\")\n\t}\n\tif memo.CreatorID != user.ID && !isSuperUser(user) {\n\t\treturn nil, status.Errorf(codes.PermissionDenied, \"permission denied\")\n\t}\n\n\tshares, err := s.Store.ListMemoShares(ctx, &store.FindMemoShare{MemoID: &memo.ID})\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to list memo shares\")\n\t}\n\n\tresponse := &v1pb.ListMemoSharesResponse{}\n\tfor _, ms := range shares {\n\t\tresponse.MemoShares = append(response.MemoShares, convertMemoShareFromStore(ms, memo.UID))\n\t}\n\treturn response, nil\n}\n\n// DeleteMemoShare revokes a share link.\n// Only the memo's creator or an admin may call this.\nfunc (s *APIV1Service) DeleteMemoShare(ctx context.Context, request *v1pb.DeleteMemoShareRequest) (*emptypb.Empty, error) {\n\tuser, err := s.fetchCurrentUser(ctx)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to get user\")\n\t}\n\tif user == nil {\n\t\treturn nil, status.Errorf(codes.Unauthenticated, \"user not authenticated\")\n\t}\n\n\t// name format: memos/{memoUID}/shares/{shareToken}\n\ttokens, err := GetNameParentTokens(request.Name, MemoNamePrefix, MemoShareNamePrefix)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.InvalidArgument, \"invalid share name: %v\", err)\n\t}\n\tmemoUID, shareToken := tokens[0], tokens[1]\n\n\tmemo, err := s.Store.GetMemo(ctx, &store.FindMemo{UID: &memoUID})\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to get memo\")\n\t}\n\tif memo == nil {\n\t\treturn nil, status.Errorf(codes.NotFound, \"memo not found\")\n\t}\n\tif memo.CreatorID != user.ID && !isSuperUser(user) {\n\t\treturn nil, status.Errorf(codes.PermissionDenied, \"permission denied\")\n\t}\n\n\tms, err := s.Store.GetMemoShare(ctx, &store.FindMemoShare{UID: &shareToken})\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to get memo share\")\n\t}\n\tif ms == nil || ms.MemoID != memo.ID {\n\t\treturn nil, status.Errorf(codes.NotFound, \"memo share not found\")\n\t}\n\n\tif err := s.Store.DeleteMemoShare(ctx, &store.DeleteMemoShare{UID: &shareToken}); err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to delete memo share\")\n\t}\n\treturn &emptypb.Empty{}, nil\n}\n\n// GetMemoByShare resolves a share token to its memo. No authentication required.\n// Returns NOT_FOUND for invalid or expired tokens (no information leakage).\nfunc (s *APIV1Service) GetMemoByShare(ctx context.Context, request *v1pb.GetMemoByShareRequest) (*v1pb.Memo, error) {\n\tms, err := s.getActiveMemoShare(ctx, request.ShareId)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tmemo, err := s.Store.GetMemo(ctx, &store.FindMemo{ID: &ms.MemoID})\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to get memo\")\n\t}\n\t// Treat archived or missing memos the same as an invalid token — no information leakage.\n\tif memo == nil || memo.RowStatus == store.Archived {\n\t\treturn nil, status.Errorf(codes.NotFound, \"not found\")\n\t}\n\n\treactions, err := s.Store.ListReactions(ctx, &store.FindReaction{\n\t\tContentID: stringPointer(fmt.Sprintf(\"%s%s\", MemoNamePrefix, memo.UID)),\n\t})\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to list reactions\")\n\t}\n\n\tattachments, err := s.Store.ListAttachments(ctx, &store.FindAttachment{MemoID: &memo.ID})\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to list attachments\")\n\t}\n\trelations, err := s.batchConvertMemoRelations(ctx, []*store.Memo{memo})\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to load memo relations\")\n\t}\n\n\tmemoMessage, err := s.convertMemoFromStore(ctx, memo, reactions, attachments, relations[memo.ID])\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"failed to convert memo\")\n\t}\n\treturn memoMessage, nil\n}\n\n// isMemoShareExpired returns true if the share has a defined expiry that has already passed.\nfunc isMemoShareExpired(ms *store.MemoShare) bool {\n\treturn ms.ExpiresTs != nil && time.Now().Unix() > *ms.ExpiresTs\n}\n\nfunc (s *APIV1Service) getActiveMemoShare(ctx context.Context, shareID string) (*store.MemoShare, error) {\n\tms, err := s.Store.GetMemoShare(ctx, &store.FindMemoShare{UID: &shareID})\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to get memo share\")\n\t}\n\tif ms == nil || isMemoShareExpired(ms) {\n\t\treturn nil, status.Errorf(codes.NotFound, \"not found\")\n\t}\n\treturn ms, nil\n}\n\nfunc stringPointer(s string) *string {\n\treturn &s\n}\n\n// convertMemoShareFromStore converts a store MemoShare to the proto MemoShare message.\n// name format: memos/{memoUID}/shares/{shareToken}.\nfunc convertMemoShareFromStore(ms *store.MemoShare, memoUID string) *v1pb.MemoShare {\n\tname := fmt.Sprintf(\"%s%s/%s%s\", MemoNamePrefix, memoUID, MemoShareNamePrefix, ms.UID)\n\tpb := &v1pb.MemoShare{\n\t\tName:       name,\n\t\tCreateTime: timestamppb.New(time.Unix(ms.CreatedTs, 0)),\n\t}\n\tif ms.ExpiresTs != nil {\n\t\tpb.ExpireTime = timestamppb.New(time.Unix(*ms.ExpiresTs, 0))\n\t}\n\treturn pb\n}\n"
  },
  {
    "path": "server/router/api/v1/reaction_service.go",
    "content": "package v1\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"google.golang.org/grpc/codes\"\n\t\"google.golang.org/grpc/status\"\n\t\"google.golang.org/protobuf/types/known/emptypb\"\n\t\"google.golang.org/protobuf/types/known/timestamppb\"\n\n\tv1pb \"github.com/usememos/memos/proto/gen/api/v1\"\n\t\"github.com/usememos/memos/store\"\n)\n\nfunc (s *APIV1Service) ListMemoReactions(ctx context.Context, request *v1pb.ListMemoReactionsRequest) (*v1pb.ListMemoReactionsResponse, error) {\n\t// Extract memo UID and check visibility.\n\tmemoUID, err := ExtractMemoUIDFromName(request.Name)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.InvalidArgument, \"invalid memo name: %v\", err)\n\t}\n\tmemo, err := s.Store.GetMemo(ctx, &store.FindMemo{UID: &memoUID})\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to get memo: %v\", err)\n\t}\n\tif memo == nil {\n\t\treturn nil, status.Errorf(codes.NotFound, \"memo not found\")\n\t}\n\n\t// Check memo visibility.\n\tif memo.Visibility != store.Public {\n\t\tuser, err := s.fetchCurrentUser(ctx)\n\t\tif err != nil {\n\t\t\treturn nil, status.Errorf(codes.Internal, \"failed to get user\")\n\t\t}\n\t\tif user == nil {\n\t\t\treturn nil, status.Errorf(codes.Unauthenticated, \"user not authenticated\")\n\t\t}\n\t\tif memo.Visibility == store.Private && memo.CreatorID != user.ID && !isSuperUser(user) {\n\t\t\treturn nil, status.Errorf(codes.PermissionDenied, \"permission denied\")\n\t\t}\n\t}\n\n\treactions, err := s.Store.ListReactions(ctx, &store.FindReaction{\n\t\tContentID: &request.Name,\n\t})\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to list reactions\")\n\t}\n\n\tresponse := &v1pb.ListMemoReactionsResponse{\n\t\tReactions: []*v1pb.Reaction{},\n\t}\n\tfor _, reaction := range reactions {\n\t\treactionMessage := convertReactionFromStore(reaction)\n\t\tresponse.Reactions = append(response.Reactions, reactionMessage)\n\t}\n\treturn response, nil\n}\n\nfunc (s *APIV1Service) UpsertMemoReaction(ctx context.Context, request *v1pb.UpsertMemoReactionRequest) (*v1pb.Reaction, error) {\n\tuser, err := s.fetchCurrentUser(ctx)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to get current user\")\n\t}\n\tif user == nil {\n\t\treturn nil, status.Errorf(codes.Unauthenticated, \"user not authenticated\")\n\t}\n\n\t// Extract memo UID and check visibility before allowing reaction.\n\tmemoUID, err := ExtractMemoUIDFromName(request.Reaction.ContentId)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.InvalidArgument, \"invalid memo name: %v\", err)\n\t}\n\tmemo, err := s.Store.GetMemo(ctx, &store.FindMemo{UID: &memoUID})\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to get memo: %v\", err)\n\t}\n\tif memo == nil {\n\t\treturn nil, status.Errorf(codes.NotFound, \"memo not found\")\n\t}\n\n\t// Check memo visibility.\n\tif memo.Visibility == store.Private && memo.CreatorID != user.ID && !isSuperUser(user) {\n\t\treturn nil, status.Errorf(codes.PermissionDenied, \"permission denied\")\n\t}\n\n\treaction, err := s.Store.UpsertReaction(ctx, &store.Reaction{\n\t\tCreatorID:    user.ID,\n\t\tContentID:    request.Reaction.ContentId,\n\t\tReactionType: request.Reaction.ReactionType,\n\t})\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to upsert reaction\")\n\t}\n\n\treactionMessage := convertReactionFromStore(reaction)\n\n\t// Broadcast live refresh event (reaction belongs to a memo).\n\ts.SSEHub.Broadcast(&SSEEvent{\n\t\tType: SSEEventReactionUpserted,\n\t\tName: request.Reaction.ContentId,\n\t})\n\n\treturn reactionMessage, nil\n}\n\nfunc (s *APIV1Service) DeleteMemoReaction(ctx context.Context, request *v1pb.DeleteMemoReactionRequest) (*emptypb.Empty, error) {\n\tuser, err := s.fetchCurrentUser(ctx)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to get current user: %v\", err)\n\t}\n\tif user == nil {\n\t\treturn nil, status.Errorf(codes.Unauthenticated, \"user not authenticated\")\n\t}\n\n\t_, reactionID, err := ExtractMemoReactionIDFromName(request.Name)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.InvalidArgument, \"invalid reaction name: %v\", err)\n\t}\n\n\t// Get reaction and check ownership.\n\treaction, err := s.Store.GetReaction(ctx, &store.FindReaction{\n\t\tID: &reactionID,\n\t})\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to get reaction\")\n\t}\n\tif reaction == nil {\n\t\t// Return permission denied to avoid revealing if reaction exists.\n\t\treturn nil, status.Errorf(codes.PermissionDenied, \"permission denied\")\n\t}\n\n\tif reaction.CreatorID != user.ID && !isSuperUser(user) {\n\t\treturn nil, status.Errorf(codes.PermissionDenied, \"permission denied\")\n\t}\n\n\tif err := s.Store.DeleteReaction(ctx, &store.DeleteReaction{\n\t\tID: reactionID,\n\t}); err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to delete reaction\")\n\t}\n\n\t// Broadcast live refresh event (reaction belongs to a memo).\n\ts.SSEHub.Broadcast(&SSEEvent{\n\t\tType: SSEEventReactionDeleted,\n\t\tName: reaction.ContentID,\n\t})\n\n\treturn &emptypb.Empty{}, nil\n}\n\nfunc convertReactionFromStore(reaction *store.Reaction) *v1pb.Reaction {\n\treactionUID := fmt.Sprintf(\"%d\", reaction.ID)\n\t// Generate nested resource name: memos/{memo}/reactions/{reaction}\n\t// reaction.ContentID already contains \"memos/{memo}\"\n\treturn &v1pb.Reaction{\n\t\tName:         fmt.Sprintf(\"%s/%s%s\", reaction.ContentID, ReactionNamePrefix, reactionUID),\n\t\tCreator:      fmt.Sprintf(\"%s%d\", UserNamePrefix, reaction.CreatorID),\n\t\tContentId:    reaction.ContentID,\n\t\tReactionType: reaction.ReactionType,\n\t\tCreateTime:   timestamppb.New(time.Unix(reaction.CreatedTs, 0)),\n\t}\n}\n"
  },
  {
    "path": "server/router/api/v1/resource_name.go",
    "content": "package v1\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/lithammer/shortuuid/v4\"\n\t\"github.com/pkg/errors\"\n\t\"google.golang.org/grpc/codes\"\n\t\"google.golang.org/grpc/status\"\n\n\t\"github.com/usememos/memos/internal/base\"\n\t\"github.com/usememos/memos/internal/util\"\n)\n\nconst (\n\tInstanceSettingNamePrefix  = \"instance/settings/\"\n\tUserNamePrefix             = \"users/\"\n\tMemoNamePrefix             = \"memos/\"\n\tMemoShareNamePrefix        = \"shares/\"\n\tAttachmentNamePrefix       = \"attachments/\"\n\tReactionNamePrefix         = \"reactions/\"\n\tInboxNamePrefix            = \"inboxes/\"\n\tIdentityProviderNamePrefix = \"identity-providers/\"\n\tWebhookNamePrefix          = \"webhooks/\"\n)\n\n// GetNameParentTokens returns the tokens from a resource name.\nfunc GetNameParentTokens(name string, tokenPrefixes ...string) ([]string, error) {\n\tparts := strings.Split(name, \"/\")\n\tif len(parts) != 2*len(tokenPrefixes) {\n\t\treturn nil, errors.Errorf(\"invalid request %q\", name)\n\t}\n\n\tvar tokens []string\n\tfor i, tokenPrefix := range tokenPrefixes {\n\t\tif fmt.Sprintf(\"%s/\", parts[2*i]) != tokenPrefix {\n\t\t\treturn nil, errors.Errorf(\"invalid prefix %q in request %q\", tokenPrefix, name)\n\t\t}\n\t\tif parts[2*i+1] == \"\" {\n\t\t\treturn nil, errors.Errorf(\"invalid request %q with empty prefix %q\", name, tokenPrefix)\n\t\t}\n\t\ttokens = append(tokens, parts[2*i+1])\n\t}\n\treturn tokens, nil\n}\n\nfunc ExtractInstanceSettingKeyFromName(name string) (string, error) {\n\tconst prefix = \"instance/settings/\"\n\tif !strings.HasPrefix(name, prefix) {\n\t\treturn \"\", errors.Errorf(\"invalid instance setting name: expected prefix %q, got %q\", prefix, name)\n\t}\n\n\tsettingKey := strings.TrimPrefix(name, prefix)\n\tif settingKey == \"\" {\n\t\treturn \"\", errors.Errorf(\"invalid instance setting name: empty setting key in %q\", name)\n\t}\n\n\t// Ensure there are no additional path segments\n\tif strings.Contains(settingKey, \"/\") {\n\t\treturn \"\", errors.Errorf(\"invalid instance setting name: setting key cannot contain '/' in %q\", name)\n\t}\n\n\treturn settingKey, nil\n}\n\n// ExtractUserIDFromName returns the uid from a resource name.\nfunc ExtractUserIDFromName(name string) (int32, error) {\n\ttokens, err := GetNameParentTokens(name, UserNamePrefix)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\tid, err := util.ConvertStringToInt32(tokens[0])\n\tif err != nil {\n\t\treturn 0, errors.Errorf(\"invalid user ID %q\", tokens[0])\n\t}\n\treturn id, nil\n}\n\n// extractUserIdentifierFromName extracts the identifier (ID or username) from a user resource name.\n// Supports: \"users/101\" or \"users/steven\"\n// Returns the identifier string (e.g., \"101\" or \"steven\").\nfunc extractUserIdentifierFromName(name string) string {\n\ttokens, err := GetNameParentTokens(name, UserNamePrefix)\n\tif err != nil || len(tokens) == 0 {\n\t\treturn \"\"\n\t}\n\treturn tokens[0]\n}\n\n// ExtractMemoUIDFromName returns the memo UID from a resource name.\n// e.g., \"memos/uuid\" -> \"uuid\".\nfunc ExtractMemoUIDFromName(name string) (string, error) {\n\ttokens, err := GetNameParentTokens(name, MemoNamePrefix)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tid := tokens[0]\n\treturn id, nil\n}\n\n// ExtractAttachmentUIDFromName returns the attachment UID from a resource name.\nfunc ExtractAttachmentUIDFromName(name string) (string, error) {\n\ttokens, err := GetNameParentTokens(name, AttachmentNamePrefix)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tid := tokens[0]\n\treturn id, nil\n}\n\n// ExtractMemoReactionIDFromName returns the memo UID and reaction ID from a resource name.\n// e.g., \"memos/abc/reactions/123\" -> (\"abc\", 123).\nfunc ExtractMemoReactionIDFromName(name string) (string, int32, error) {\n\ttokens, err := GetNameParentTokens(name, MemoNamePrefix, ReactionNamePrefix)\n\tif err != nil {\n\t\treturn \"\", 0, err\n\t}\n\tmemoUID := tokens[0]\n\treactionID, err := util.ConvertStringToInt32(tokens[1])\n\tif err != nil {\n\t\treturn \"\", 0, errors.Errorf(\"invalid reaction ID %q\", tokens[1])\n\t}\n\treturn memoUID, reactionID, nil\n}\n\n// ExtractInboxIDFromName returns the inbox ID from a resource name.\nfunc ExtractInboxIDFromName(name string) (int32, error) {\n\ttokens, err := GetNameParentTokens(name, InboxNamePrefix)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\tid, err := util.ConvertStringToInt32(tokens[0])\n\tif err != nil {\n\t\treturn 0, errors.Errorf(\"invalid inbox ID %q\", tokens[0])\n\t}\n\treturn id, nil\n}\n\nfunc ExtractIdentityProviderUIDFromName(name string) (string, error) {\n\ttokens, err := GetNameParentTokens(name, IdentityProviderNamePrefix)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn tokens[0], nil\n}\n\n// ValidateAndGenerateUID validates a user-provided UID or generates a new one.\n// If provided is empty, a new shortuuid is generated.\n// If provided is non-empty, it is validated against base.UIDMatcher.\nfunc ValidateAndGenerateUID(provided string) (string, error) {\n\tuid := strings.TrimSpace(provided)\n\tif uid == \"\" {\n\t\treturn shortuuid.New(), nil\n\t}\n\tif !base.UIDMatcher.MatchString(uid) {\n\t\treturn \"\", status.Errorf(codes.InvalidArgument, \"invalid ID format: must be 1-32 characters, alphanumeric and hyphens only, cannot start or end with hyphen\")\n\t}\n\treturn uid, nil\n}\n"
  },
  {
    "path": "server/router/api/v1/shortcut_service.go",
    "content": "package v1\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/pkg/errors\"\n\t\"google.golang.org/grpc/codes\"\n\t\"google.golang.org/grpc/status\"\n\t\"google.golang.org/protobuf/types/known/emptypb\"\n\n\t\"github.com/usememos/memos/internal/util\"\n\t\"github.com/usememos/memos/plugin/filter\"\n\tv1pb \"github.com/usememos/memos/proto/gen/api/v1\"\n\tstorepb \"github.com/usememos/memos/proto/gen/store\"\n\t\"github.com/usememos/memos/store\"\n)\n\n// Helper function to extract user ID and shortcut ID from shortcut resource name.\n// Format: users/{user}/shortcuts/{shortcut}.\nfunc extractUserAndShortcutIDFromName(name string) (int32, string, error) {\n\tparts := strings.Split(name, \"/\")\n\tif len(parts) != 4 || parts[0] != \"users\" || parts[2] != \"shortcuts\" {\n\t\treturn 0, \"\", errors.Errorf(\"invalid shortcut name format: %s\", name)\n\t}\n\n\tuserID, err := util.ConvertStringToInt32(parts[1])\n\tif err != nil {\n\t\treturn 0, \"\", errors.Errorf(\"invalid user ID %q\", parts[1])\n\t}\n\n\tshortcutID := parts[3]\n\tif shortcutID == \"\" {\n\t\treturn 0, \"\", errors.Errorf(\"empty shortcut ID in name: %s\", name)\n\t}\n\n\treturn userID, shortcutID, nil\n}\n\n// Helper function to construct shortcut resource name.\nfunc constructShortcutName(userID int32, shortcutID string) string {\n\treturn fmt.Sprintf(\"users/%d/shortcuts/%s\", userID, shortcutID)\n}\n\nfunc (s *APIV1Service) ListShortcuts(ctx context.Context, request *v1pb.ListShortcutsRequest) (*v1pb.ListShortcutsResponse, error) {\n\tuserID, err := ExtractUserIDFromName(request.Parent)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.InvalidArgument, \"invalid user name: %v\", err)\n\t}\n\n\tcurrentUser, err := s.fetchCurrentUser(ctx)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to get current user: %v\", err)\n\t}\n\tif currentUser == nil || currentUser.ID != userID {\n\t\treturn nil, status.Errorf(codes.PermissionDenied, \"permission denied\")\n\t}\n\n\tuserSetting, err := s.Store.GetUserSetting(ctx, &store.FindUserSetting{\n\t\tUserID: &userID,\n\t\tKey:    storepb.UserSetting_SHORTCUTS,\n\t})\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to get user setting: %v\", err)\n\t}\n\tif userSetting == nil {\n\t\treturn &v1pb.ListShortcutsResponse{\n\t\t\tShortcuts: []*v1pb.Shortcut{},\n\t\t}, nil\n\t}\n\n\tshortcutsUserSetting := userSetting.GetShortcuts()\n\tshortcuts := []*v1pb.Shortcut{}\n\tfor _, shortcut := range shortcutsUserSetting.GetShortcuts() {\n\t\tshortcuts = append(shortcuts, &v1pb.Shortcut{\n\t\t\tName:   constructShortcutName(userID, shortcut.GetId()),\n\t\t\tTitle:  shortcut.GetTitle(),\n\t\t\tFilter: shortcut.GetFilter(),\n\t\t})\n\t}\n\n\treturn &v1pb.ListShortcutsResponse{\n\t\tShortcuts: shortcuts,\n\t}, nil\n}\n\nfunc (s *APIV1Service) GetShortcut(ctx context.Context, request *v1pb.GetShortcutRequest) (*v1pb.Shortcut, error) {\n\tuserID, shortcutID, err := extractUserAndShortcutIDFromName(request.Name)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.InvalidArgument, \"invalid shortcut name: %v\", err)\n\t}\n\n\tcurrentUser, err := s.fetchCurrentUser(ctx)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to get current user: %v\", err)\n\t}\n\tif currentUser == nil || currentUser.ID != userID {\n\t\treturn nil, status.Errorf(codes.PermissionDenied, \"permission denied\")\n\t}\n\n\tuserSetting, err := s.Store.GetUserSetting(ctx, &store.FindUserSetting{\n\t\tUserID: &userID,\n\t\tKey:    storepb.UserSetting_SHORTCUTS,\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif userSetting == nil {\n\t\treturn nil, status.Errorf(codes.NotFound, \"shortcut not found\")\n\t}\n\n\tshortcutsUserSetting := userSetting.GetShortcuts()\n\tfor _, shortcut := range shortcutsUserSetting.GetShortcuts() {\n\t\tif shortcut.GetId() == shortcutID {\n\t\t\treturn &v1pb.Shortcut{\n\t\t\t\tName:   constructShortcutName(userID, shortcut.GetId()),\n\t\t\t\tTitle:  shortcut.GetTitle(),\n\t\t\t\tFilter: shortcut.GetFilter(),\n\t\t\t}, nil\n\t\t}\n\t}\n\n\treturn nil, status.Errorf(codes.NotFound, \"shortcut not found\")\n}\n\nfunc (s *APIV1Service) CreateShortcut(ctx context.Context, request *v1pb.CreateShortcutRequest) (*v1pb.Shortcut, error) {\n\tuserID, err := ExtractUserIDFromName(request.Parent)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.InvalidArgument, \"invalid user name: %v\", err)\n\t}\n\n\tcurrentUser, err := s.fetchCurrentUser(ctx)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to get current user: %v\", err)\n\t}\n\tif currentUser == nil || currentUser.ID != userID {\n\t\treturn nil, status.Errorf(codes.PermissionDenied, \"permission denied\")\n\t}\n\n\tnewShortcut := &storepb.ShortcutsUserSetting_Shortcut{\n\t\tId:     util.GenUUID(),\n\t\tTitle:  request.Shortcut.GetTitle(),\n\t\tFilter: request.Shortcut.GetFilter(),\n\t}\n\tif newShortcut.Title == \"\" {\n\t\treturn nil, status.Errorf(codes.InvalidArgument, \"title is required\")\n\t}\n\tif err := s.validateFilter(ctx, newShortcut.Filter); err != nil {\n\t\treturn nil, status.Errorf(codes.InvalidArgument, \"invalid filter: %v\", err)\n\t}\n\tif request.ValidateOnly {\n\t\treturn &v1pb.Shortcut{\n\t\t\tName:   constructShortcutName(userID, newShortcut.GetId()),\n\t\t\tTitle:  newShortcut.GetTitle(),\n\t\t\tFilter: newShortcut.GetFilter(),\n\t\t}, nil\n\t}\n\n\tuserSetting, err := s.Store.GetUserSetting(ctx, &store.FindUserSetting{\n\t\tUserID: &userID,\n\t\tKey:    storepb.UserSetting_SHORTCUTS,\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif userSetting == nil {\n\t\tuserSetting = &storepb.UserSetting{\n\t\t\tUserId: userID,\n\t\t\tKey:    storepb.UserSetting_SHORTCUTS,\n\t\t\tValue: &storepb.UserSetting_Shortcuts{\n\t\t\t\tShortcuts: &storepb.ShortcutsUserSetting{\n\t\t\t\t\tShortcuts: []*storepb.ShortcutsUserSetting_Shortcut{},\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\t}\n\tshortcutsUserSetting := userSetting.GetShortcuts()\n\tshortcuts := shortcutsUserSetting.GetShortcuts()\n\tshortcuts = append(shortcuts, newShortcut)\n\tshortcutsUserSetting.Shortcuts = shortcuts\n\n\tuserSetting.Value = &storepb.UserSetting_Shortcuts{\n\t\tShortcuts: shortcutsUserSetting,\n\t}\n\n\t_, err = s.Store.UpsertUserSetting(ctx, userSetting)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to upsert user setting: %v\", err)\n\t}\n\n\treturn &v1pb.Shortcut{\n\t\tName:   constructShortcutName(userID, newShortcut.GetId()),\n\t\tTitle:  newShortcut.GetTitle(),\n\t\tFilter: newShortcut.GetFilter(),\n\t}, nil\n}\n\nfunc (s *APIV1Service) UpdateShortcut(ctx context.Context, request *v1pb.UpdateShortcutRequest) (*v1pb.Shortcut, error) {\n\tuserID, shortcutID, err := extractUserAndShortcutIDFromName(request.Shortcut.Name)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.InvalidArgument, \"invalid shortcut name: %v\", err)\n\t}\n\n\tcurrentUser, err := s.fetchCurrentUser(ctx)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to get current user: %v\", err)\n\t}\n\tif currentUser == nil || currentUser.ID != userID {\n\t\treturn nil, status.Errorf(codes.PermissionDenied, \"permission denied\")\n\t}\n\tif request.UpdateMask == nil || len(request.UpdateMask.Paths) == 0 {\n\t\treturn nil, status.Errorf(codes.InvalidArgument, \"update mask is required\")\n\t}\n\n\tuserSetting, err := s.Store.GetUserSetting(ctx, &store.FindUserSetting{\n\t\tUserID: &userID,\n\t\tKey:    storepb.UserSetting_SHORTCUTS,\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif userSetting == nil {\n\t\treturn nil, status.Errorf(codes.NotFound, \"shortcut not found\")\n\t}\n\n\tshortcutsUserSetting := userSetting.GetShortcuts()\n\tshortcuts := shortcutsUserSetting.GetShortcuts()\n\tvar foundShortcut *storepb.ShortcutsUserSetting_Shortcut\n\tnewShortcuts := make([]*storepb.ShortcutsUserSetting_Shortcut, 0, len(shortcuts))\n\tfor _, shortcut := range shortcuts {\n\t\tif shortcut.GetId() == shortcutID {\n\t\t\tfoundShortcut = shortcut\n\t\t\tfor _, field := range request.UpdateMask.Paths {\n\t\t\t\tif field == \"title\" {\n\t\t\t\t\tif request.Shortcut.GetTitle() == \"\" {\n\t\t\t\t\t\treturn nil, status.Errorf(codes.InvalidArgument, \"title is required\")\n\t\t\t\t\t}\n\t\t\t\t\tshortcut.Title = request.Shortcut.GetTitle()\n\t\t\t\t} else if field == \"filter\" {\n\t\t\t\t\tif err := s.validateFilter(ctx, request.Shortcut.GetFilter()); err != nil {\n\t\t\t\t\t\treturn nil, status.Errorf(codes.InvalidArgument, \"invalid filter: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tshortcut.Filter = request.Shortcut.GetFilter()\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tnewShortcuts = append(newShortcuts, shortcut)\n\t}\n\n\tif foundShortcut == nil {\n\t\treturn nil, status.Errorf(codes.NotFound, \"shortcut not found\")\n\t}\n\n\tshortcutsUserSetting.Shortcuts = newShortcuts\n\tuserSetting.Value = &storepb.UserSetting_Shortcuts{\n\t\tShortcuts: shortcutsUserSetting,\n\t}\n\t_, err = s.Store.UpsertUserSetting(ctx, userSetting)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &v1pb.Shortcut{\n\t\tName:   constructShortcutName(userID, foundShortcut.GetId()),\n\t\tTitle:  foundShortcut.GetTitle(),\n\t\tFilter: foundShortcut.GetFilter(),\n\t}, nil\n}\n\nfunc (s *APIV1Service) DeleteShortcut(ctx context.Context, request *v1pb.DeleteShortcutRequest) (*emptypb.Empty, error) {\n\tuserID, shortcutID, err := extractUserAndShortcutIDFromName(request.Name)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.InvalidArgument, \"invalid shortcut name: %v\", err)\n\t}\n\n\tcurrentUser, err := s.fetchCurrentUser(ctx)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to get current user: %v\", err)\n\t}\n\tif currentUser == nil || currentUser.ID != userID {\n\t\treturn nil, status.Errorf(codes.PermissionDenied, \"permission denied\")\n\t}\n\n\tuserSetting, err := s.Store.GetUserSetting(ctx, &store.FindUserSetting{\n\t\tUserID: &userID,\n\t\tKey:    storepb.UserSetting_SHORTCUTS,\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif userSetting == nil {\n\t\treturn nil, status.Errorf(codes.NotFound, \"shortcut not found\")\n\t}\n\n\tshortcutsUserSetting := userSetting.GetShortcuts()\n\tshortcuts := shortcutsUserSetting.GetShortcuts()\n\tnewShortcuts := make([]*storepb.ShortcutsUserSetting_Shortcut, 0, len(shortcuts))\n\tfound := false\n\tfor _, shortcut := range shortcuts {\n\t\tif shortcut.GetId() != shortcutID {\n\t\t\tnewShortcuts = append(newShortcuts, shortcut)\n\t\t} else {\n\t\t\tfound = true\n\t\t}\n\t}\n\tif !found {\n\t\treturn nil, status.Errorf(codes.NotFound, \"shortcut not found\")\n\t}\n\tshortcutsUserSetting.Shortcuts = newShortcuts\n\tuserSetting.Value = &storepb.UserSetting_Shortcuts{\n\t\tShortcuts: shortcutsUserSetting,\n\t}\n\t_, err = s.Store.UpsertUserSetting(ctx, userSetting)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to upsert user setting: %v\", err)\n\t}\n\n\treturn &emptypb.Empty{}, nil\n}\n\nfunc (s *APIV1Service) validateFilter(ctx context.Context, filterStr string) error {\n\tif filterStr == \"\" {\n\t\treturn errors.New(\"filter cannot be empty\")\n\t}\n\n\tengine, err := filter.DefaultEngine()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar dialect filter.DialectName\n\tswitch s.Profile.Driver {\n\tcase \"mysql\":\n\t\tdialect = filter.DialectMySQL\n\tcase \"postgres\":\n\t\tdialect = filter.DialectPostgres\n\tdefault:\n\t\tdialect = filter.DialectSQLite\n\t}\n\n\tif _, err := engine.CompileToStatement(ctx, filterStr, filter.RenderOptions{Dialect: dialect}); err != nil {\n\t\treturn errors.Wrap(err, \"failed to compile filter\")\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "server/router/api/v1/sse_handler.go",
    "content": "package v1\n\nimport (\n\t\"fmt\"\n\t\"log/slog\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/labstack/echo/v5\"\n\n\t\"github.com/usememos/memos/server/auth\"\n\t\"github.com/usememos/memos/store\"\n)\n\nconst (\n\t// sseHeartbeatInterval is the interval between heartbeat pings to keep the connection alive.\n\tsseHeartbeatInterval = 30 * time.Second\n)\n\n// RegisterSSERoutes registers the SSE endpoint on the given Echo instance.\nfunc RegisterSSERoutes(echoServer *echo.Echo, hub *SSEHub, storeInstance *store.Store, secret string) {\n\tauthenticator := auth.NewAuthenticator(storeInstance, secret)\n\techoServer.GET(\"/api/v1/sse\", func(c *echo.Context) error {\n\t\treturn handleSSE(c, hub, authenticator)\n\t})\n}\n\n// handleSSE handles the SSE connection for live memo refresh.\n// Authentication is done via Bearer token in the Authorization header.\nfunc handleSSE(c *echo.Context, hub *SSEHub, authenticator *auth.Authenticator) error {\n\t// Authenticate the request.\n\tauthHeader := c.Request().Header.Get(\"Authorization\")\n\tresult := authenticator.Authenticate(c.Request().Context(), authHeader)\n\tif result == nil {\n\t\treturn c.JSON(http.StatusUnauthorized, map[string]string{\"error\": \"authentication required\"})\n\t}\n\n\t// Set SSE headers.\n\tw := c.Response()\n\tw.Header().Set(\"Content-Type\", \"text/event-stream\")\n\tw.Header().Set(\"Cache-Control\", \"no-cache\")\n\tw.Header().Set(\"Connection\", \"keep-alive\")\n\tw.Header().Set(\"X-Accel-Buffering\", \"no\") // Disable nginx buffering\n\tw.WriteHeader(http.StatusOK)\n\n\t// Flush headers immediately.\n\tif f, ok := w.(http.Flusher); ok {\n\t\tf.Flush()\n\t}\n\n\t// Subscribe to the hub.\n\tclient := hub.Subscribe()\n\tdefer hub.Unsubscribe(client)\n\n\t// Create a ticker for heartbeat pings.\n\theartbeat := time.NewTicker(sseHeartbeatInterval)\n\tdefer heartbeat.Stop()\n\n\tctx := c.Request().Context()\n\n\tslog.Debug(\"SSE client connected\")\n\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\t// Client disconnected.\n\t\t\tslog.Debug(\"SSE client disconnected\")\n\t\t\treturn nil\n\n\t\tcase data, ok := <-client.events:\n\t\t\tif !ok {\n\t\t\t\t// Channel closed, client was unsubscribed.\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\t// Write SSE event.\n\t\t\tif _, err := fmt.Fprintf(w, \"data: %s\\n\\n\", data); err != nil {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\tif f, ok := w.(http.Flusher); ok {\n\t\t\t\tf.Flush()\n\t\t\t}\n\n\t\tcase <-heartbeat.C:\n\t\t\t// Send a heartbeat comment to keep the connection alive.\n\t\t\tif _, err := fmt.Fprint(w, \": heartbeat\\n\\n\"); err != nil {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\tif f, ok := w.(http.Flusher); ok {\n\t\t\t\tf.Flush()\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "server/router/api/v1/sse_hub.go",
    "content": "package v1\n\nimport (\n\t\"encoding/json\"\n\t\"log/slog\"\n\t\"sync\"\n)\n\n// SSEEventType represents the type of change event.\ntype SSEEventType string\n\nconst (\n\tSSEEventMemoCreated        SSEEventType = \"memo.created\"\n\tSSEEventMemoUpdated        SSEEventType = \"memo.updated\"\n\tSSEEventMemoDeleted        SSEEventType = \"memo.deleted\"\n\tSSEEventMemoCommentCreated SSEEventType = \"memo.comment.created\"\n\tSSEEventReactionUpserted   SSEEventType = \"reaction.upserted\"\n\tSSEEventReactionDeleted    SSEEventType = \"reaction.deleted\"\n)\n\n// SSEEvent represents a change event sent to SSE clients.\ntype SSEEvent struct {\n\tType SSEEventType `json:\"type\"`\n\t// Name is the affected resource name (e.g., \"memos/xxxx\").\n\t// For reaction events, this is the memo resource name that the reaction belongs to.\n\tName string `json:\"name\"`\n}\n\n// JSON returns the JSON representation of the event.\n// Returns nil if marshaling fails (error is logged).\nfunc (e *SSEEvent) JSON() []byte {\n\tdata, err := json.Marshal(e)\n\tif err != nil {\n\t\tslog.Error(\"failed to marshal SSE event\", \"err\", err, \"event\", e)\n\t\treturn nil\n\t}\n\treturn data\n}\n\n// SSEClient represents a single SSE connection.\ntype SSEClient struct {\n\tevents chan []byte\n}\n\n// SSEHub manages SSE client connections and broadcasts events.\n// It is safe for concurrent use.\ntype SSEHub struct {\n\tmu      sync.RWMutex\n\tclients map[*SSEClient]struct{}\n}\n\n// NewSSEHub creates a new SSE hub.\nfunc NewSSEHub() *SSEHub {\n\treturn &SSEHub{\n\t\tclients: make(map[*SSEClient]struct{}),\n\t}\n}\n\n// Subscribe registers a new client and returns it.\n// The caller must call Unsubscribe when done.\nfunc (h *SSEHub) Subscribe() *SSEClient {\n\tc := &SSEClient{\n\t\t// Buffer a few events so a slow client doesn't block broadcasting.\n\t\tevents: make(chan []byte, 32),\n\t}\n\th.mu.Lock()\n\th.clients[c] = struct{}{}\n\th.mu.Unlock()\n\treturn c\n}\n\n// Unsubscribe removes a client and closes its channel.\nfunc (h *SSEHub) Unsubscribe(c *SSEClient) {\n\th.mu.Lock()\n\tif _, ok := h.clients[c]; ok {\n\t\tdelete(h.clients, c)\n\t\tclose(c.events)\n\t}\n\th.mu.Unlock()\n}\n\n// Broadcast sends an event to all connected clients.\n// Slow clients that have a full buffer will have the event dropped\n// to avoid blocking the broadcaster.\nfunc (h *SSEHub) Broadcast(event *SSEEvent) {\n\tdata := event.JSON()\n\tif len(data) == 0 {\n\t\treturn\n\t}\n\th.mu.RLock()\n\tdefer h.mu.RUnlock()\n\tfor c := range h.clients {\n\t\tselect {\n\t\tcase c.events <- data:\n\t\tdefault:\n\t\t\t// Drop event for slow client to avoid blocking.\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "server/router/api/v1/sse_hub_test.go",
    "content": "package v1\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestSSEHub_SubscribeUnsubscribe(t *testing.T) {\n\thub := NewSSEHub()\n\n\tclient := hub.Subscribe()\n\trequire.NotNil(t, client)\n\trequire.NotNil(t, client.events)\n\n\t// Unsubscribe removes the client and closes the channel.\n\thub.Unsubscribe(client)\n\n\t// Channel should be closed.\n\t_, ok := <-client.events\n\tassert.False(t, ok, \"channel should be closed after Unsubscribe\")\n}\n\nfunc TestSSEHub_Broadcast(t *testing.T) {\n\thub := NewSSEHub()\n\tclient := hub.Subscribe()\n\tdefer hub.Unsubscribe(client)\n\n\tevent := &SSEEvent{Type: SSEEventMemoCreated, Name: \"memos/123\"}\n\thub.Broadcast(event)\n\n\tselect {\n\tcase data := <-client.events:\n\t\tassert.Contains(t, string(data), `\"type\":\"memo.created\"`)\n\t\tassert.Contains(t, string(data), `\"name\":\"memos/123\"`)\n\tcase <-time.After(time.Second):\n\t\tt.Fatal(\"expected to receive event within 1s\")\n\t}\n}\n\nfunc TestSSEHub_BroadcastMultipleClients(t *testing.T) {\n\thub := NewSSEHub()\n\tc1 := hub.Subscribe()\n\tdefer hub.Unsubscribe(c1)\n\tc2 := hub.Subscribe()\n\tdefer hub.Unsubscribe(c2)\n\n\tevent := &SSEEvent{Type: SSEEventMemoDeleted, Name: \"memos/456\"}\n\thub.Broadcast(event)\n\n\tfor _, ch := range []chan []byte{c1.events, c2.events} {\n\t\tselect {\n\t\tcase data := <-ch:\n\t\t\tassert.Contains(t, string(data), \"memo.deleted\")\n\t\t\tassert.Contains(t, string(data), \"memos/456\")\n\t\tcase <-time.After(time.Second):\n\t\t\tt.Fatal(\"expected to receive event within 1s\")\n\t\t}\n\t}\n}\n\nfunc TestSSEEvent_JSON(t *testing.T) {\n\te := &SSEEvent{Type: SSEEventMemoUpdated, Name: \"memos/789\"}\n\tdata := e.JSON()\n\trequire.NotEmpty(t, data)\n\tassert.Contains(t, string(data), `\"type\":\"memo.updated\"`)\n\tassert.Contains(t, string(data), `\"name\":\"memos/789\"`)\n}\n"
  },
  {
    "path": "server/router/api/v1/test/attachment_service_test.go",
    "content": "package test\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n\n\tv1pb \"github.com/usememos/memos/proto/gen/api/v1\"\n)\n\nfunc TestCreateAttachment(t *testing.T) {\n\tts := NewTestService(t)\n\tdefer ts.Cleanup()\n\tctx := context.Background()\n\n\tuser, err := ts.CreateRegularUser(ctx, \"test_user\")\n\trequire.NoError(t, err)\n\tuserCtx := ts.CreateUserContext(ctx, user.ID)\n\n\t// Test case 1: Create attachment with empty type but known extension\n\tt.Run(\"EmptyType_KnownExtension\", func(t *testing.T) {\n\t\tattachment, err := ts.Service.CreateAttachment(userCtx, &v1pb.CreateAttachmentRequest{\n\t\t\tAttachment: &v1pb.Attachment{\n\t\t\t\tFilename: \"test.png\",\n\t\t\t\tContent:  []byte(\"fake png content\"),\n\t\t\t},\n\t\t})\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, \"image/png\", attachment.Type)\n\t})\n\n\t// Test case 2: Create attachment with empty type and unknown extension, but detectable content\n\tt.Run(\"EmptyType_UnknownExtension_ContentSniffing\", func(t *testing.T) {\n\t\t// PNG magic header: 89 50 4E 47 0D 0A 1A 0A\n\t\tpngContent := []byte{0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A}\n\t\tattachment, err := ts.Service.CreateAttachment(userCtx, &v1pb.CreateAttachmentRequest{\n\t\t\tAttachment: &v1pb.Attachment{\n\t\t\t\tFilename: \"test.unknown\",\n\t\t\t\tContent:  pngContent,\n\t\t\t},\n\t\t})\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, \"image/png\", attachment.Type)\n\t})\n\n\t// Test case 3: Empty type, unknown extension, random content -> fallback to application/octet-stream\n\tt.Run(\"EmptyType_Fallback\", func(t *testing.T) {\n\t\trandomContent := []byte{0x00, 0x01, 0x02, 0x03}\n\t\tattachment, err := ts.Service.CreateAttachment(userCtx, &v1pb.CreateAttachmentRequest{\n\t\t\tAttachment: &v1pb.Attachment{\n\t\t\t\tFilename: \"test.data\",\n\t\t\t\tContent:  randomContent,\n\t\t\t},\n\t\t})\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, \"application/octet-stream\", attachment.Type)\n\t})\n}\n"
  },
  {
    "path": "server/router/api/v1/test/auth_test.go",
    "content": "package test\n\nimport (\n\t\"context\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"google.golang.org/protobuf/types/known/timestamppb\"\n\n\t\"github.com/usememos/memos/internal/util\"\n\tstorepb \"github.com/usememos/memos/proto/gen/store\"\n\t\"github.com/usememos/memos/server/auth\"\n\t\"github.com/usememos/memos/store\"\n)\n\nfunc TestAuthenticatorAccessTokenV2(t *testing.T) {\n\tctx := context.Background()\n\n\tt.Run(\"authenticates valid access token v2\", func(t *testing.T) {\n\t\tts := NewTestService(t)\n\t\tdefer ts.Cleanup()\n\n\t\t// Create a test user\n\t\tuser, err := ts.CreateRegularUser(ctx, \"testuser\")\n\t\trequire.NoError(t, err)\n\n\t\t// Generate access token v2\n\t\ttoken, _, err := auth.GenerateAccessTokenV2(\n\t\t\tuser.ID,\n\t\t\tuser.Username,\n\t\t\tstring(user.Role),\n\t\t\tstring(user.RowStatus),\n\t\t\t[]byte(ts.Secret),\n\t\t)\n\t\trequire.NoError(t, err)\n\n\t\t// Authenticate\n\t\tauthenticator := auth.NewAuthenticator(ts.Store, ts.Secret)\n\t\tclaims, err := authenticator.AuthenticateByAccessTokenV2(token)\n\t\trequire.NoError(t, err)\n\t\tassert.NotNil(t, claims)\n\t\tassert.Equal(t, user.ID, claims.UserID)\n\t\tassert.Equal(t, user.Username, claims.Username)\n\t\tassert.Equal(t, string(user.Role), claims.Role)\n\t\tassert.Equal(t, string(user.RowStatus), claims.Status)\n\t})\n\n\tt.Run(\"fails with invalid token\", func(t *testing.T) {\n\t\tts := NewTestService(t)\n\t\tdefer ts.Cleanup()\n\n\t\tauthenticator := auth.NewAuthenticator(ts.Store, ts.Secret)\n\t\t_, err := authenticator.AuthenticateByAccessTokenV2(\"invalid-token\")\n\t\tassert.Error(t, err)\n\t})\n\n\tt.Run(\"fails with wrong secret\", func(t *testing.T) {\n\t\tts := NewTestService(t)\n\t\tdefer ts.Cleanup()\n\n\t\tuser, err := ts.CreateRegularUser(ctx, \"testuser\")\n\t\trequire.NoError(t, err)\n\n\t\t// Generate token with one secret\n\t\ttoken, _, err := auth.GenerateAccessTokenV2(\n\t\t\tuser.ID,\n\t\t\tuser.Username,\n\t\t\tstring(user.Role),\n\t\t\tstring(user.RowStatus),\n\t\t\t[]byte(\"secret-1\"),\n\t\t)\n\t\trequire.NoError(t, err)\n\n\t\t// Try to authenticate with different secret\n\t\tauthenticator := auth.NewAuthenticator(ts.Store, \"secret-2\")\n\t\t_, err = authenticator.AuthenticateByAccessTokenV2(token)\n\t\tassert.Error(t, err)\n\t})\n}\n\nfunc TestAuthenticatorRefreshToken(t *testing.T) {\n\tctx := context.Background()\n\n\tt.Run(\"authenticates valid refresh token\", func(t *testing.T) {\n\t\tts := NewTestService(t)\n\t\tdefer ts.Cleanup()\n\n\t\t// Create a test user\n\t\tuser, err := ts.CreateRegularUser(ctx, \"testuser\")\n\t\trequire.NoError(t, err)\n\n\t\t// Create refresh token record in store\n\t\ttokenID := util.GenUUID()\n\t\trefreshTokenRecord := &storepb.RefreshTokensUserSetting_RefreshToken{\n\t\t\tTokenId:   tokenID,\n\t\t\tExpiresAt: timestamppb.New(time.Now().Add(auth.RefreshTokenDuration)),\n\t\t\tCreatedAt: timestamppb.Now(),\n\t\t}\n\t\terr = ts.Store.AddUserRefreshToken(ctx, user.ID, refreshTokenRecord)\n\t\trequire.NoError(t, err)\n\n\t\t// Generate refresh token JWT\n\t\ttoken, _, err := auth.GenerateRefreshToken(user.ID, tokenID, []byte(ts.Secret))\n\t\trequire.NoError(t, err)\n\n\t\t// Authenticate\n\t\tauthenticator := auth.NewAuthenticator(ts.Store, ts.Secret)\n\t\tauthenticatedUser, returnedTokenID, err := authenticator.AuthenticateByRefreshToken(ctx, token)\n\t\trequire.NoError(t, err)\n\t\tassert.NotNil(t, authenticatedUser)\n\t\tassert.Equal(t, user.ID, authenticatedUser.ID)\n\t\tassert.Equal(t, tokenID, returnedTokenID)\n\t})\n\n\tt.Run(\"fails with revoked token\", func(t *testing.T) {\n\t\tts := NewTestService(t)\n\t\tdefer ts.Cleanup()\n\n\t\tuser, err := ts.CreateRegularUser(ctx, \"testuser\")\n\t\trequire.NoError(t, err)\n\n\t\ttokenID := util.GenUUID()\n\n\t\t// Generate refresh token JWT but don't store it in database (simulates revocation)\n\t\ttoken, _, err := auth.GenerateRefreshToken(user.ID, tokenID, []byte(ts.Secret))\n\t\trequire.NoError(t, err)\n\n\t\t// Try to authenticate\n\t\tauthenticator := auth.NewAuthenticator(ts.Store, ts.Secret)\n\t\t_, _, err = authenticator.AuthenticateByRefreshToken(ctx, token)\n\t\tassert.Error(t, err)\n\t\tassert.Contains(t, err.Error(), \"revoked\")\n\t})\n\n\tt.Run(\"fails with expired token\", func(t *testing.T) {\n\t\tts := NewTestService(t)\n\t\tdefer ts.Cleanup()\n\n\t\tuser, err := ts.CreateRegularUser(ctx, \"testuser\")\n\t\trequire.NoError(t, err)\n\n\t\t// Create expired refresh token record in store\n\t\ttokenID := util.GenUUID()\n\t\texpiredToken := &storepb.RefreshTokensUserSetting_RefreshToken{\n\t\t\tTokenId:   tokenID,\n\t\t\tExpiresAt: timestamppb.New(time.Now().Add(-1 * time.Hour)), // Expired\n\t\t\tCreatedAt: timestamppb.Now(),\n\t\t}\n\t\terr = ts.Store.AddUserRefreshToken(ctx, user.ID, expiredToken)\n\t\trequire.NoError(t, err)\n\n\t\t// Generate refresh token JWT (JWT itself isn't expired yet)\n\t\ttoken, _, err := auth.GenerateRefreshToken(user.ID, tokenID, []byte(ts.Secret))\n\t\trequire.NoError(t, err)\n\n\t\t// Try to authenticate\n\t\tauthenticator := auth.NewAuthenticator(ts.Store, ts.Secret)\n\t\t_, _, err = authenticator.AuthenticateByRefreshToken(ctx, token)\n\t\tassert.Error(t, err)\n\t\tassert.Contains(t, err.Error(), \"expired\")\n\t})\n\n\tt.Run(\"fails with archived user\", func(t *testing.T) {\n\t\tts := NewTestService(t)\n\t\tdefer ts.Cleanup()\n\n\t\tuser, err := ts.CreateRegularUser(ctx, \"testuser\")\n\t\trequire.NoError(t, err)\n\n\t\t// Create valid refresh token\n\t\ttokenID := util.GenUUID()\n\t\trefreshTokenRecord := &storepb.RefreshTokensUserSetting_RefreshToken{\n\t\t\tTokenId:   tokenID,\n\t\t\tExpiresAt: timestamppb.New(time.Now().Add(auth.RefreshTokenDuration)),\n\t\t\tCreatedAt: timestamppb.Now(),\n\t\t}\n\t\terr = ts.Store.AddUserRefreshToken(ctx, user.ID, refreshTokenRecord)\n\t\trequire.NoError(t, err)\n\n\t\ttoken, _, err := auth.GenerateRefreshToken(user.ID, tokenID, []byte(ts.Secret))\n\t\trequire.NoError(t, err)\n\n\t\t// Archive the user\n\t\tarchivedStatus := store.Archived\n\t\t_, err = ts.Store.UpdateUser(ctx, &store.UpdateUser{\n\t\t\tID:        user.ID,\n\t\t\tRowStatus: &archivedStatus,\n\t\t})\n\t\trequire.NoError(t, err)\n\n\t\t// Try to authenticate\n\t\tauthenticator := auth.NewAuthenticator(ts.Store, ts.Secret)\n\t\t_, _, err = authenticator.AuthenticateByRefreshToken(ctx, token)\n\t\tassert.Error(t, err)\n\t\tassert.Contains(t, err.Error(), \"archived\")\n\t})\n}\n\nfunc TestAuthenticatorPAT(t *testing.T) {\n\tctx := context.Background()\n\n\tt.Run(\"authenticates valid PAT\", func(t *testing.T) {\n\t\tts := NewTestService(t)\n\t\tdefer ts.Cleanup()\n\n\t\t// Create a test user\n\t\tuser, err := ts.CreateRegularUser(ctx, \"testuser\")\n\t\trequire.NoError(t, err)\n\n\t\t// Generate PAT\n\t\ttoken := auth.GeneratePersonalAccessToken()\n\t\ttokenHash := auth.HashPersonalAccessToken(token)\n\t\ttokenID := util.GenUUID()\n\n\t\t// Store PAT in database\n\t\tpatRecord := &storepb.PersonalAccessTokensUserSetting_PersonalAccessToken{\n\t\t\tTokenId:     tokenID,\n\t\t\tTokenHash:   tokenHash,\n\t\t\tDescription: \"Test PAT\",\n\t\t\tCreatedAt:   timestamppb.Now(),\n\t\t}\n\t\terr = ts.Store.AddUserPersonalAccessToken(ctx, user.ID, patRecord)\n\t\trequire.NoError(t, err)\n\n\t\t// Authenticate\n\t\tauthenticator := auth.NewAuthenticator(ts.Store, ts.Secret)\n\t\tauthenticatedUser, pat, err := authenticator.AuthenticateByPAT(ctx, token)\n\t\trequire.NoError(t, err)\n\t\tassert.NotNil(t, authenticatedUser)\n\t\tassert.NotNil(t, pat)\n\t\tassert.Equal(t, user.ID, authenticatedUser.ID)\n\t\tassert.Equal(t, tokenID, pat.TokenId)\n\t})\n\n\tt.Run(\"fails with invalid PAT format\", func(t *testing.T) {\n\t\tts := NewTestService(t)\n\t\tdefer ts.Cleanup()\n\n\t\tauthenticator := auth.NewAuthenticator(ts.Store, ts.Secret)\n\t\t_, _, err := authenticator.AuthenticateByPAT(ctx, \"invalid-token-without-prefix\")\n\t\tassert.Error(t, err)\n\t\tassert.Contains(t, err.Error(), \"invalid PAT format\")\n\t})\n\n\tt.Run(\"fails with non-existent PAT\", func(t *testing.T) {\n\t\tts := NewTestService(t)\n\t\tdefer ts.Cleanup()\n\n\t\t// Generate a PAT but don't store it\n\t\ttoken := auth.GeneratePersonalAccessToken()\n\n\t\tauthenticator := auth.NewAuthenticator(ts.Store, ts.Secret)\n\t\t_, _, err := authenticator.AuthenticateByPAT(ctx, token)\n\t\tassert.Error(t, err)\n\t})\n\n\tt.Run(\"fails with expired PAT\", func(t *testing.T) {\n\t\tts := NewTestService(t)\n\t\tdefer ts.Cleanup()\n\n\t\tuser, err := ts.CreateRegularUser(ctx, \"testuser\")\n\t\trequire.NoError(t, err)\n\n\t\t// Generate and store expired PAT\n\t\ttoken := auth.GeneratePersonalAccessToken()\n\t\ttokenHash := auth.HashPersonalAccessToken(token)\n\t\ttokenID := util.GenUUID()\n\n\t\texpiredPAT := &storepb.PersonalAccessTokensUserSetting_PersonalAccessToken{\n\t\t\tTokenId:     tokenID,\n\t\t\tTokenHash:   tokenHash,\n\t\t\tDescription: \"Expired PAT\",\n\t\t\tExpiresAt:   timestamppb.New(time.Now().Add(-1 * time.Hour)), // Expired\n\t\t\tCreatedAt:   timestamppb.Now(),\n\t\t}\n\t\terr = ts.Store.AddUserPersonalAccessToken(ctx, user.ID, expiredPAT)\n\t\trequire.NoError(t, err)\n\n\t\t// Try to authenticate\n\t\tauthenticator := auth.NewAuthenticator(ts.Store, ts.Secret)\n\t\t_, _, err = authenticator.AuthenticateByPAT(ctx, token)\n\t\tassert.Error(t, err)\n\t\tassert.Contains(t, err.Error(), \"expired\")\n\t})\n\n\tt.Run(\"succeeds with non-expiring PAT\", func(t *testing.T) {\n\t\tts := NewTestService(t)\n\t\tdefer ts.Cleanup()\n\n\t\tuser, err := ts.CreateRegularUser(ctx, \"testuser\")\n\t\trequire.NoError(t, err)\n\n\t\t// Generate and store PAT without expiration\n\t\ttoken := auth.GeneratePersonalAccessToken()\n\t\ttokenHash := auth.HashPersonalAccessToken(token)\n\t\ttokenID := util.GenUUID()\n\n\t\tpatRecord := &storepb.PersonalAccessTokensUserSetting_PersonalAccessToken{\n\t\t\tTokenId:     tokenID,\n\t\t\tTokenHash:   tokenHash,\n\t\t\tDescription: \"Never-expiring PAT\",\n\t\t\tExpiresAt:   nil, // No expiration\n\t\t\tCreatedAt:   timestamppb.Now(),\n\t\t}\n\t\terr = ts.Store.AddUserPersonalAccessToken(ctx, user.ID, patRecord)\n\t\trequire.NoError(t, err)\n\n\t\t// Authenticate\n\t\tauthenticator := auth.NewAuthenticator(ts.Store, ts.Secret)\n\t\tauthenticatedUser, pat, err := authenticator.AuthenticateByPAT(ctx, token)\n\t\trequire.NoError(t, err)\n\t\tassert.NotNil(t, authenticatedUser)\n\t\tassert.NotNil(t, pat)\n\t\tassert.Nil(t, pat.ExpiresAt)\n\t})\n\n\tt.Run(\"fails with archived user\", func(t *testing.T) {\n\t\tts := NewTestService(t)\n\t\tdefer ts.Cleanup()\n\n\t\tuser, err := ts.CreateRegularUser(ctx, \"testuser\")\n\t\trequire.NoError(t, err)\n\n\t\t// Generate and store PAT\n\t\ttoken := auth.GeneratePersonalAccessToken()\n\t\ttokenHash := auth.HashPersonalAccessToken(token)\n\t\ttokenID := util.GenUUID()\n\n\t\tpatRecord := &storepb.PersonalAccessTokensUserSetting_PersonalAccessToken{\n\t\t\tTokenId:     tokenID,\n\t\t\tTokenHash:   tokenHash,\n\t\t\tDescription: \"Test PAT\",\n\t\t\tCreatedAt:   timestamppb.Now(),\n\t\t}\n\t\terr = ts.Store.AddUserPersonalAccessToken(ctx, user.ID, patRecord)\n\t\trequire.NoError(t, err)\n\n\t\t// Archive the user\n\t\tarchivedStatus := store.Archived\n\t\t_, err = ts.Store.UpdateUser(ctx, &store.UpdateUser{\n\t\t\tID:        user.ID,\n\t\t\tRowStatus: &archivedStatus,\n\t\t})\n\t\trequire.NoError(t, err)\n\n\t\t// Try to authenticate\n\t\tauthenticator := auth.NewAuthenticator(ts.Store, ts.Secret)\n\t\t_, _, err = authenticator.AuthenticateByPAT(ctx, token)\n\t\tassert.Error(t, err)\n\t\tassert.Contains(t, err.Error(), \"archived\")\n\t})\n}\n\nfunc TestStoreRefreshTokenMethods(t *testing.T) {\n\tctx := context.Background()\n\n\tt.Run(\"adds and retrieves refresh token\", func(t *testing.T) {\n\t\tts := NewTestService(t)\n\t\tdefer ts.Cleanup()\n\n\t\tuser, err := ts.CreateRegularUser(ctx, \"testuser\")\n\t\trequire.NoError(t, err)\n\n\t\ttokenID := util.GenUUID()\n\t\ttoken := &storepb.RefreshTokensUserSetting_RefreshToken{\n\t\t\tTokenId:   tokenID,\n\t\t\tExpiresAt: timestamppb.New(time.Now().Add(30 * 24 * time.Hour)),\n\t\t\tCreatedAt: timestamppb.Now(),\n\t\t}\n\n\t\terr = ts.Store.AddUserRefreshToken(ctx, user.ID, token)\n\t\trequire.NoError(t, err)\n\n\t\t// Retrieve tokens\n\t\ttokens, err := ts.Store.GetUserRefreshTokens(ctx, user.ID)\n\t\trequire.NoError(t, err)\n\t\tassert.Len(t, tokens, 1)\n\t\tassert.Equal(t, tokenID, tokens[0].TokenId)\n\t})\n\n\tt.Run(\"retrieves specific refresh token by ID\", func(t *testing.T) {\n\t\tts := NewTestService(t)\n\t\tdefer ts.Cleanup()\n\n\t\tuser, err := ts.CreateRegularUser(ctx, \"testuser\")\n\t\trequire.NoError(t, err)\n\n\t\ttokenID := util.GenUUID()\n\t\ttoken := &storepb.RefreshTokensUserSetting_RefreshToken{\n\t\t\tTokenId:   tokenID,\n\t\t\tExpiresAt: timestamppb.New(time.Now().Add(30 * 24 * time.Hour)),\n\t\t\tCreatedAt: timestamppb.Now(),\n\t\t}\n\n\t\terr = ts.Store.AddUserRefreshToken(ctx, user.ID, token)\n\t\trequire.NoError(t, err)\n\n\t\t// Retrieve specific token\n\t\tretrievedToken, err := ts.Store.GetUserRefreshTokenByID(ctx, user.ID, tokenID)\n\t\trequire.NoError(t, err)\n\t\tassert.NotNil(t, retrievedToken)\n\t\tassert.Equal(t, tokenID, retrievedToken.TokenId)\n\t})\n\n\tt.Run(\"removes refresh token\", func(t *testing.T) {\n\t\tts := NewTestService(t)\n\t\tdefer ts.Cleanup()\n\n\t\tuser, err := ts.CreateRegularUser(ctx, \"testuser\")\n\t\trequire.NoError(t, err)\n\n\t\ttokenID := util.GenUUID()\n\t\ttoken := &storepb.RefreshTokensUserSetting_RefreshToken{\n\t\t\tTokenId:   tokenID,\n\t\t\tExpiresAt: timestamppb.New(time.Now().Add(30 * 24 * time.Hour)),\n\t\t\tCreatedAt: timestamppb.Now(),\n\t\t}\n\n\t\terr = ts.Store.AddUserRefreshToken(ctx, user.ID, token)\n\t\trequire.NoError(t, err)\n\n\t\t// Remove token\n\t\terr = ts.Store.RemoveUserRefreshToken(ctx, user.ID, tokenID)\n\t\trequire.NoError(t, err)\n\n\t\t// Verify removal\n\t\ttokens, err := ts.Store.GetUserRefreshTokens(ctx, user.ID)\n\t\trequire.NoError(t, err)\n\t\tassert.Len(t, tokens, 0)\n\t})\n\n\tt.Run(\"handles multiple refresh tokens\", func(t *testing.T) {\n\t\tts := NewTestService(t)\n\t\tdefer ts.Cleanup()\n\n\t\tuser, err := ts.CreateRegularUser(ctx, \"testuser\")\n\t\trequire.NoError(t, err)\n\n\t\t// Add multiple tokens\n\t\ttokenID1 := util.GenUUID()\n\t\ttokenID2 := util.GenUUID()\n\n\t\ttoken1 := &storepb.RefreshTokensUserSetting_RefreshToken{\n\t\t\tTokenId:   tokenID1,\n\t\t\tExpiresAt: timestamppb.New(time.Now().Add(30 * 24 * time.Hour)),\n\t\t\tCreatedAt: timestamppb.Now(),\n\t\t}\n\t\ttoken2 := &storepb.RefreshTokensUserSetting_RefreshToken{\n\t\t\tTokenId:   tokenID2,\n\t\t\tExpiresAt: timestamppb.New(time.Now().Add(30 * 24 * time.Hour)),\n\t\t\tCreatedAt: timestamppb.Now(),\n\t\t}\n\n\t\terr = ts.Store.AddUserRefreshToken(ctx, user.ID, token1)\n\t\trequire.NoError(t, err)\n\t\terr = ts.Store.AddUserRefreshToken(ctx, user.ID, token2)\n\t\trequire.NoError(t, err)\n\n\t\t// Retrieve all tokens\n\t\ttokens, err := ts.Store.GetUserRefreshTokens(ctx, user.ID)\n\t\trequire.NoError(t, err)\n\t\tassert.Len(t, tokens, 2)\n\n\t\t// Remove one token\n\t\terr = ts.Store.RemoveUserRefreshToken(ctx, user.ID, tokenID1)\n\t\trequire.NoError(t, err)\n\n\t\t// Verify only one token remains\n\t\ttokens, err = ts.Store.GetUserRefreshTokens(ctx, user.ID)\n\t\trequire.NoError(t, err)\n\t\tassert.Len(t, tokens, 1)\n\t\tassert.Equal(t, tokenID2, tokens[0].TokenId)\n\t})\n}\n\nfunc TestStorePersonalAccessTokenMethods(t *testing.T) {\n\tctx := context.Background()\n\n\tt.Run(\"adds and retrieves PAT\", func(t *testing.T) {\n\t\tts := NewTestService(t)\n\t\tdefer ts.Cleanup()\n\n\t\tuser, err := ts.CreateRegularUser(ctx, \"testuser\")\n\t\trequire.NoError(t, err)\n\n\t\ttoken := auth.GeneratePersonalAccessToken()\n\t\ttokenHash := auth.HashPersonalAccessToken(token)\n\t\ttokenID := util.GenUUID()\n\n\t\tpat := &storepb.PersonalAccessTokensUserSetting_PersonalAccessToken{\n\t\t\tTokenId:     tokenID,\n\t\t\tTokenHash:   tokenHash,\n\t\t\tDescription: \"Test PAT\",\n\t\t\tCreatedAt:   timestamppb.Now(),\n\t\t}\n\n\t\terr = ts.Store.AddUserPersonalAccessToken(ctx, user.ID, pat)\n\t\trequire.NoError(t, err)\n\n\t\t// Retrieve PATs\n\t\tpats, err := ts.Store.GetUserPersonalAccessTokens(ctx, user.ID)\n\t\trequire.NoError(t, err)\n\t\tassert.Len(t, pats, 1)\n\t\tassert.Equal(t, tokenID, pats[0].TokenId)\n\t\tassert.Equal(t, tokenHash, pats[0].TokenHash)\n\t})\n\n\tt.Run(\"removes PAT\", func(t *testing.T) {\n\t\tts := NewTestService(t)\n\t\tdefer ts.Cleanup()\n\n\t\tuser, err := ts.CreateRegularUser(ctx, \"testuser\")\n\t\trequire.NoError(t, err)\n\n\t\ttoken := auth.GeneratePersonalAccessToken()\n\t\ttokenHash := auth.HashPersonalAccessToken(token)\n\t\ttokenID := util.GenUUID()\n\n\t\tpat := &storepb.PersonalAccessTokensUserSetting_PersonalAccessToken{\n\t\t\tTokenId:     tokenID,\n\t\t\tTokenHash:   tokenHash,\n\t\t\tDescription: \"Test PAT\",\n\t\t\tCreatedAt:   timestamppb.Now(),\n\t\t}\n\n\t\terr = ts.Store.AddUserPersonalAccessToken(ctx, user.ID, pat)\n\t\trequire.NoError(t, err)\n\n\t\t// Remove PAT\n\t\terr = ts.Store.RemoveUserPersonalAccessToken(ctx, user.ID, tokenID)\n\t\trequire.NoError(t, err)\n\n\t\t// Verify removal\n\t\tpats, err := ts.Store.GetUserPersonalAccessTokens(ctx, user.ID)\n\t\trequire.NoError(t, err)\n\t\tassert.Len(t, pats, 0)\n\t})\n\n\tt.Run(\"updates PAT last used time\", func(t *testing.T) {\n\t\tts := NewTestService(t)\n\t\tdefer ts.Cleanup()\n\n\t\tuser, err := ts.CreateRegularUser(ctx, \"testuser\")\n\t\trequire.NoError(t, err)\n\n\t\ttoken := auth.GeneratePersonalAccessToken()\n\t\ttokenHash := auth.HashPersonalAccessToken(token)\n\t\ttokenID := util.GenUUID()\n\n\t\tpat := &storepb.PersonalAccessTokensUserSetting_PersonalAccessToken{\n\t\t\tTokenId:     tokenID,\n\t\t\tTokenHash:   tokenHash,\n\t\t\tDescription: \"Test PAT\",\n\t\t\tCreatedAt:   timestamppb.Now(),\n\t\t}\n\n\t\terr = ts.Store.AddUserPersonalAccessToken(ctx, user.ID, pat)\n\t\trequire.NoError(t, err)\n\n\t\t// Update last used time\n\t\tlastUsed := timestamppb.Now()\n\t\terr = ts.Store.UpdatePATLastUsed(ctx, user.ID, tokenID, lastUsed)\n\t\trequire.NoError(t, err)\n\n\t\t// Verify update\n\t\tpats, err := ts.Store.GetUserPersonalAccessTokens(ctx, user.ID)\n\t\trequire.NoError(t, err)\n\t\tassert.Len(t, pats, 1)\n\t\tassert.NotNil(t, pats[0].LastUsedAt)\n\t})\n\n\tt.Run(\"handles multiple PATs\", func(t *testing.T) {\n\t\tts := NewTestService(t)\n\t\tdefer ts.Cleanup()\n\n\t\tuser, err := ts.CreateRegularUser(ctx, \"testuser\")\n\t\trequire.NoError(t, err)\n\n\t\t// Add multiple PATs\n\t\ttoken1 := auth.GeneratePersonalAccessToken()\n\t\ttokenHash1 := auth.HashPersonalAccessToken(token1)\n\t\ttokenID1 := util.GenUUID()\n\n\t\ttoken2 := auth.GeneratePersonalAccessToken()\n\t\ttokenHash2 := auth.HashPersonalAccessToken(token2)\n\t\ttokenID2 := util.GenUUID()\n\n\t\tpat1 := &storepb.PersonalAccessTokensUserSetting_PersonalAccessToken{\n\t\t\tTokenId:     tokenID1,\n\t\t\tTokenHash:   tokenHash1,\n\t\t\tDescription: \"PAT 1\",\n\t\t\tCreatedAt:   timestamppb.Now(),\n\t\t}\n\t\tpat2 := &storepb.PersonalAccessTokensUserSetting_PersonalAccessToken{\n\t\t\tTokenId:     tokenID2,\n\t\t\tTokenHash:   tokenHash2,\n\t\t\tDescription: \"PAT 2\",\n\t\t\tCreatedAt:   timestamppb.Now(),\n\t\t}\n\n\t\terr = ts.Store.AddUserPersonalAccessToken(ctx, user.ID, pat1)\n\t\trequire.NoError(t, err)\n\t\terr = ts.Store.AddUserPersonalAccessToken(ctx, user.ID, pat2)\n\t\trequire.NoError(t, err)\n\n\t\t// Retrieve all PATs\n\t\tpats, err := ts.Store.GetUserPersonalAccessTokens(ctx, user.ID)\n\t\trequire.NoError(t, err)\n\t\tassert.Len(t, pats, 2)\n\n\t\t// Remove one PAT\n\t\terr = ts.Store.RemoveUserPersonalAccessToken(ctx, user.ID, tokenID1)\n\t\trequire.NoError(t, err)\n\n\t\t// Verify only one PAT remains\n\t\tpats, err = ts.Store.GetUserPersonalAccessTokens(ctx, user.ID)\n\t\trequire.NoError(t, err)\n\t\tassert.Len(t, pats, 1)\n\t\tassert.Equal(t, tokenID2, pats[0].TokenId)\n\t})\n\n\tt.Run(\"finds user by PAT hash\", func(t *testing.T) {\n\t\tts := NewTestService(t)\n\t\tdefer ts.Cleanup()\n\n\t\tuser, err := ts.CreateRegularUser(ctx, \"testuser\")\n\t\trequire.NoError(t, err)\n\n\t\ttoken := auth.GeneratePersonalAccessToken()\n\t\ttokenHash := auth.HashPersonalAccessToken(token)\n\t\ttokenID := util.GenUUID()\n\n\t\tpat := &storepb.PersonalAccessTokensUserSetting_PersonalAccessToken{\n\t\t\tTokenId:     tokenID,\n\t\t\tTokenHash:   tokenHash,\n\t\t\tDescription: \"Test PAT\",\n\t\t\tCreatedAt:   timestamppb.Now(),\n\t\t}\n\n\t\terr = ts.Store.AddUserPersonalAccessToken(ctx, user.ID, pat)\n\t\trequire.NoError(t, err)\n\n\t\t// Find user by PAT hash\n\t\tresult, err := ts.Store.GetUserByPATHash(ctx, tokenHash)\n\t\trequire.NoError(t, err)\n\t\tassert.NotNil(t, result)\n\t\tassert.Equal(t, user.ID, result.UserID)\n\t\tassert.NotNil(t, result.User)\n\t\tassert.Equal(t, user.Username, result.User.Username)\n\t\tassert.NotNil(t, result.PAT)\n\t\tassert.Equal(t, tokenID, result.PAT.TokenId)\n\t})\n}\n"
  },
  {
    "path": "server/router/api/v1/test/idp_service_test.go",
    "content": "package test\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n\t\"google.golang.org/protobuf/types/known/fieldmaskpb\"\n\n\tv1pb \"github.com/usememos/memos/proto/gen/api/v1\"\n)\n\nfunc TestCreateIdentityProvider(t *testing.T) {\n\tctx := context.Background()\n\n\tt.Run(\"CreateIdentityProvider success\", func(t *testing.T) {\n\t\tts := NewTestService(t)\n\t\tdefer ts.Cleanup()\n\n\t\t// Create host user\n\t\thostUser, err := ts.CreateHostUser(ctx, \"admin\")\n\t\trequire.NoError(t, err)\n\n\t\t// Set user context\n\t\tctx := ts.CreateUserContext(ctx, hostUser.ID)\n\n\t\t// Create OAuth2 identity provider\n\t\treq := &v1pb.CreateIdentityProviderRequest{\n\t\t\tIdentityProvider: &v1pb.IdentityProvider{\n\t\t\t\tTitle:            \"Test OAuth2 Provider\",\n\t\t\t\tIdentifierFilter: \"\",\n\t\t\t\tType:             v1pb.IdentityProvider_OAUTH2,\n\t\t\t\tConfig: &v1pb.IdentityProviderConfig{\n\t\t\t\t\tConfig: &v1pb.IdentityProviderConfig_Oauth2Config{\n\t\t\t\t\t\tOauth2Config: &v1pb.OAuth2Config{\n\t\t\t\t\t\t\tClientId:     \"test-client-id\",\n\t\t\t\t\t\t\tClientSecret: \"test-client-secret\",\n\t\t\t\t\t\t\tAuthUrl:      \"https://example.com/oauth/authorize\",\n\t\t\t\t\t\t\tTokenUrl:     \"https://example.com/oauth/token\",\n\t\t\t\t\t\t\tUserInfoUrl:  \"https://example.com/oauth/userinfo\",\n\t\t\t\t\t\t\tScopes:       []string{\"openid\", \"profile\", \"email\"},\n\t\t\t\t\t\t\tFieldMapping: &v1pb.FieldMapping{\n\t\t\t\t\t\t\t\tIdentifier:  \"id\",\n\t\t\t\t\t\t\t\tDisplayName: \"name\",\n\t\t\t\t\t\t\t\tEmail:       \"email\",\n\t\t\t\t\t\t\t\tAvatarUrl:   \"avatar_url\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\n\t\tresp, err := ts.Service.CreateIdentityProvider(ctx, req)\n\t\trequire.NoError(t, err)\n\t\trequire.NotNil(t, resp)\n\t\trequire.Equal(t, \"Test OAuth2 Provider\", resp.Title)\n\t\trequire.Equal(t, v1pb.IdentityProvider_OAUTH2, resp.Type)\n\t\trequire.Contains(t, resp.Name, \"identity-providers/\")\n\t\trequire.NotNil(t, resp.Config.GetOauth2Config())\n\t\trequire.Equal(t, \"test-client-id\", resp.Config.GetOauth2Config().ClientId)\n\t})\n\n\tt.Run(\"CreateIdentityProvider permission denied for non-host user\", func(t *testing.T) {\n\t\tts := NewTestService(t)\n\t\tdefer ts.Cleanup()\n\n\t\t// Create regular user\n\t\tregularUser, err := ts.CreateRegularUser(ctx, \"user\")\n\t\trequire.NoError(t, err)\n\n\t\t// Set user context\n\t\tctx := ts.CreateUserContext(ctx, regularUser.ID)\n\n\t\treq := &v1pb.CreateIdentityProviderRequest{\n\t\t\tIdentityProvider: &v1pb.IdentityProvider{\n\t\t\t\tTitle: \"Test Provider\",\n\t\t\t\tType:  v1pb.IdentityProvider_OAUTH2,\n\t\t\t},\n\t\t}\n\n\t\t_, err = ts.Service.CreateIdentityProvider(ctx, req)\n\t\trequire.Error(t, err)\n\t\trequire.Contains(t, err.Error(), \"permission denied\")\n\t})\n\n\tt.Run(\"CreateIdentityProvider unauthenticated\", func(t *testing.T) {\n\t\tts := NewTestService(t)\n\t\tdefer ts.Cleanup()\n\n\t\treq := &v1pb.CreateIdentityProviderRequest{\n\t\t\tIdentityProvider: &v1pb.IdentityProvider{\n\t\t\t\tTitle: \"Test Provider\",\n\t\t\t\tType:  v1pb.IdentityProvider_OAUTH2,\n\t\t\t},\n\t\t}\n\n\t\t_, err := ts.Service.CreateIdentityProvider(ctx, req)\n\t\trequire.Error(t, err)\n\t\trequire.Contains(t, err.Error(), \"user not authenticated\")\n\t})\n}\n\nfunc TestListIdentityProviders(t *testing.T) {\n\tctx := context.Background()\n\n\tt.Run(\"ListIdentityProviders empty\", func(t *testing.T) {\n\t\tts := NewTestService(t)\n\t\tdefer ts.Cleanup()\n\n\t\treq := &v1pb.ListIdentityProvidersRequest{}\n\t\tresp, err := ts.Service.ListIdentityProviders(ctx, req)\n\t\trequire.NoError(t, err)\n\t\trequire.NotNil(t, resp)\n\t\trequire.Empty(t, resp.IdentityProviders)\n\t})\n\n\tt.Run(\"ListIdentityProviders with providers\", func(t *testing.T) {\n\t\tts := NewTestService(t)\n\t\tdefer ts.Cleanup()\n\n\t\t// Create host user\n\t\thostUser, err := ts.CreateHostUser(ctx, \"admin\")\n\t\trequire.NoError(t, err)\n\n\t\t// Set user context\n\t\tuserCtx := ts.CreateUserContext(ctx, hostUser.ID)\n\n\t\t// Create a couple of identity providers\n\t\tcreateReq1 := &v1pb.CreateIdentityProviderRequest{\n\t\t\tIdentityProvider: &v1pb.IdentityProvider{\n\t\t\t\tTitle: \"Provider 1\",\n\t\t\t\tType:  v1pb.IdentityProvider_OAUTH2,\n\t\t\t\tConfig: &v1pb.IdentityProviderConfig{\n\t\t\t\t\tConfig: &v1pb.IdentityProviderConfig_Oauth2Config{\n\t\t\t\t\t\tOauth2Config: &v1pb.OAuth2Config{\n\t\t\t\t\t\t\tClientId:    \"client1\",\n\t\t\t\t\t\t\tAuthUrl:     \"https://example1.com/auth\",\n\t\t\t\t\t\t\tTokenUrl:    \"https://example1.com/token\",\n\t\t\t\t\t\t\tUserInfoUrl: \"https://example1.com/user\",\n\t\t\t\t\t\t\tFieldMapping: &v1pb.FieldMapping{\n\t\t\t\t\t\t\t\tIdentifier: \"id\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\n\t\tcreateReq2 := &v1pb.CreateIdentityProviderRequest{\n\t\t\tIdentityProvider: &v1pb.IdentityProvider{\n\t\t\t\tTitle: \"Provider 2\",\n\t\t\t\tType:  v1pb.IdentityProvider_OAUTH2,\n\t\t\t\tConfig: &v1pb.IdentityProviderConfig{\n\t\t\t\t\tConfig: &v1pb.IdentityProviderConfig_Oauth2Config{\n\t\t\t\t\t\tOauth2Config: &v1pb.OAuth2Config{\n\t\t\t\t\t\t\tClientId:    \"client2\",\n\t\t\t\t\t\t\tAuthUrl:     \"https://example2.com/auth\",\n\t\t\t\t\t\t\tTokenUrl:    \"https://example2.com/token\",\n\t\t\t\t\t\t\tUserInfoUrl: \"https://example2.com/user\",\n\t\t\t\t\t\t\tFieldMapping: &v1pb.FieldMapping{\n\t\t\t\t\t\t\t\tIdentifier: \"id\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\n\t\t_, err = ts.Service.CreateIdentityProvider(userCtx, createReq1)\n\t\trequire.NoError(t, err)\n\t\t_, err = ts.Service.CreateIdentityProvider(userCtx, createReq2)\n\t\trequire.NoError(t, err)\n\n\t\t// List providers\n\t\tlistReq := &v1pb.ListIdentityProvidersRequest{}\n\t\tresp, err := ts.Service.ListIdentityProviders(ctx, listReq)\n\t\trequire.NoError(t, err)\n\t\trequire.NotNil(t, resp)\n\t\trequire.Len(t, resp.IdentityProviders, 2)\n\n\t\t// Verify response contains expected providers\n\t\ttitles := []string{resp.IdentityProviders[0].Title, resp.IdentityProviders[1].Title}\n\t\trequire.Contains(t, titles, \"Provider 1\")\n\t\trequire.Contains(t, titles, \"Provider 2\")\n\t})\n}\n\nfunc TestGetIdentityProvider(t *testing.T) {\n\tctx := context.Background()\n\n\tt.Run(\"GetIdentityProvider success\", func(t *testing.T) {\n\t\tts := NewTestService(t)\n\t\tdefer ts.Cleanup()\n\n\t\t// Create host user\n\t\thostUser, err := ts.CreateHostUser(ctx, \"admin\")\n\t\trequire.NoError(t, err)\n\n\t\t// Set user context\n\t\tuserCtx := ts.CreateUserContext(ctx, hostUser.ID)\n\n\t\t// Create identity provider\n\t\tcreateReq := &v1pb.CreateIdentityProviderRequest{\n\t\t\tIdentityProvider: &v1pb.IdentityProvider{\n\t\t\t\tTitle: \"Test Provider\",\n\t\t\t\tType:  v1pb.IdentityProvider_OAUTH2,\n\t\t\t\tConfig: &v1pb.IdentityProviderConfig{\n\t\t\t\t\tConfig: &v1pb.IdentityProviderConfig_Oauth2Config{\n\t\t\t\t\t\tOauth2Config: &v1pb.OAuth2Config{\n\t\t\t\t\t\t\tClientId:     \"test-client\",\n\t\t\t\t\t\t\tClientSecret: \"test-secret\",\n\t\t\t\t\t\t\tAuthUrl:      \"https://example.com/auth\",\n\t\t\t\t\t\t\tTokenUrl:     \"https://example.com/token\",\n\t\t\t\t\t\t\tUserInfoUrl:  \"https://example.com/user\",\n\t\t\t\t\t\t\tScopes:       []string{\"openid\", \"profile\"},\n\t\t\t\t\t\t\tFieldMapping: &v1pb.FieldMapping{\n\t\t\t\t\t\t\t\tIdentifier:  \"id\",\n\t\t\t\t\t\t\t\tDisplayName: \"name\",\n\t\t\t\t\t\t\t\tEmail:       \"email\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\n\t\tcreated, err := ts.Service.CreateIdentityProvider(userCtx, createReq)\n\t\trequire.NoError(t, err)\n\n\t\t// Get identity provider\n\t\tgetReq := &v1pb.GetIdentityProviderRequest{\n\t\t\tName: created.Name,\n\t\t}\n\n\t\t// Test unauthenticated, should not contain client secret\n\t\tresp, err := ts.Service.GetIdentityProvider(ctx, getReq)\n\t\trequire.NoError(t, err)\n\t\trequire.NotNil(t, resp)\n\t\trequire.Equal(t, created.Name, resp.Name)\n\t\trequire.Equal(t, \"Test Provider\", resp.Title)\n\t\trequire.Equal(t, v1pb.IdentityProvider_OAUTH2, resp.Type)\n\t\trequire.NotNil(t, resp.Config.GetOauth2Config())\n\t\trequire.Equal(t, \"test-client\", resp.Config.GetOauth2Config().ClientId)\n\t\trequire.Equal(t, \"\", resp.Config.GetOauth2Config().ClientSecret)\n\n\t\t// Test as host user, should contain client secret\n\t\trespHostUser, err := ts.Service.GetIdentityProvider(userCtx, getReq)\n\t\trequire.NoError(t, err)\n\t\trequire.NotNil(t, respHostUser)\n\t\trequire.Equal(t, created.Name, respHostUser.Name)\n\t\trequire.Equal(t, \"Test Provider\", respHostUser.Title)\n\t\trequire.Equal(t, v1pb.IdentityProvider_OAUTH2, respHostUser.Type)\n\t\trequire.NotNil(t, respHostUser.Config.GetOauth2Config())\n\t\trequire.Equal(t, \"test-client\", respHostUser.Config.GetOauth2Config().ClientId)\n\t\trequire.Equal(t, \"test-secret\", respHostUser.Config.GetOauth2Config().ClientSecret)\n\t})\n\n\tt.Run(\"GetIdentityProvider not found\", func(t *testing.T) {\n\t\tts := NewTestService(t)\n\t\tdefer ts.Cleanup()\n\n\t\treq := &v1pb.GetIdentityProviderRequest{\n\t\t\tName: \"identity-providers/999\",\n\t\t}\n\n\t\t_, err := ts.Service.GetIdentityProvider(ctx, req)\n\t\trequire.Error(t, err)\n\t\trequire.Contains(t, err.Error(), \"not found\")\n\t})\n\n\tt.Run(\"GetIdentityProvider invalid name\", func(t *testing.T) {\n\t\tts := NewTestService(t)\n\t\tdefer ts.Cleanup()\n\n\t\treq := &v1pb.GetIdentityProviderRequest{\n\t\t\tName: \"invalid-name\",\n\t\t}\n\n\t\t_, err := ts.Service.GetIdentityProvider(ctx, req)\n\t\trequire.Error(t, err)\n\t\trequire.Contains(t, err.Error(), \"invalid identity provider name\")\n\t})\n}\n\nfunc TestUpdateIdentityProvider(t *testing.T) {\n\tctx := context.Background()\n\n\tt.Run(\"UpdateIdentityProvider success\", func(t *testing.T) {\n\t\tts := NewTestService(t)\n\t\tdefer ts.Cleanup()\n\n\t\t// Create host user\n\t\thostUser, err := ts.CreateHostUser(ctx, \"admin\")\n\t\trequire.NoError(t, err)\n\n\t\t// Set user context\n\t\tuserCtx := ts.CreateUserContext(ctx, hostUser.ID)\n\n\t\t// Create identity provider\n\t\tcreateReq := &v1pb.CreateIdentityProviderRequest{\n\t\t\tIdentityProvider: &v1pb.IdentityProvider{\n\t\t\t\tTitle:            \"Original Provider\",\n\t\t\t\tIdentifierFilter: \"\",\n\t\t\t\tType:             v1pb.IdentityProvider_OAUTH2,\n\t\t\t\tConfig: &v1pb.IdentityProviderConfig{\n\t\t\t\t\tConfig: &v1pb.IdentityProviderConfig_Oauth2Config{\n\t\t\t\t\t\tOauth2Config: &v1pb.OAuth2Config{\n\t\t\t\t\t\t\tClientId:    \"original-client\",\n\t\t\t\t\t\t\tAuthUrl:     \"https://original.com/auth\",\n\t\t\t\t\t\t\tTokenUrl:    \"https://original.com/token\",\n\t\t\t\t\t\t\tUserInfoUrl: \"https://original.com/user\",\n\t\t\t\t\t\t\tFieldMapping: &v1pb.FieldMapping{\n\t\t\t\t\t\t\t\tIdentifier: \"id\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\n\t\tcreated, err := ts.Service.CreateIdentityProvider(userCtx, createReq)\n\t\trequire.NoError(t, err)\n\n\t\t// Update identity provider\n\t\tupdateReq := &v1pb.UpdateIdentityProviderRequest{\n\t\t\tIdentityProvider: &v1pb.IdentityProvider{\n\t\t\t\tName:             created.Name,\n\t\t\t\tTitle:            \"Updated Provider\",\n\t\t\t\tIdentifierFilter: \"test@example.com\",\n\t\t\t\tType:             v1pb.IdentityProvider_OAUTH2,\n\t\t\t\tConfig: &v1pb.IdentityProviderConfig{\n\t\t\t\t\tConfig: &v1pb.IdentityProviderConfig_Oauth2Config{\n\t\t\t\t\t\tOauth2Config: &v1pb.OAuth2Config{\n\t\t\t\t\t\t\tClientId:     \"updated-client\",\n\t\t\t\t\t\t\tClientSecret: \"updated-secret\",\n\t\t\t\t\t\t\tAuthUrl:      \"https://updated.com/auth\",\n\t\t\t\t\t\t\tTokenUrl:     \"https://updated.com/token\",\n\t\t\t\t\t\t\tUserInfoUrl:  \"https://updated.com/user\",\n\t\t\t\t\t\t\tScopes:       []string{\"openid\", \"profile\", \"email\"},\n\t\t\t\t\t\t\tFieldMapping: &v1pb.FieldMapping{\n\t\t\t\t\t\t\t\tIdentifier:  \"sub\",\n\t\t\t\t\t\t\t\tDisplayName: \"given_name\",\n\t\t\t\t\t\t\t\tEmail:       \"email\",\n\t\t\t\t\t\t\t\tAvatarUrl:   \"picture\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tUpdateMask: &fieldmaskpb.FieldMask{\n\t\t\t\tPaths: []string{\"title\", \"identifier_filter\", \"config\"},\n\t\t\t},\n\t\t}\n\n\t\tupdated, err := ts.Service.UpdateIdentityProvider(userCtx, updateReq)\n\t\trequire.NoError(t, err)\n\t\trequire.NotNil(t, updated)\n\t\trequire.Equal(t, \"Updated Provider\", updated.Title)\n\t\trequire.Equal(t, \"test@example.com\", updated.IdentifierFilter)\n\t\trequire.Equal(t, \"updated-client\", updated.Config.GetOauth2Config().ClientId)\n\t})\n\n\tt.Run(\"UpdateIdentityProvider missing update mask\", func(t *testing.T) {\n\t\tts := NewTestService(t)\n\t\tdefer ts.Cleanup()\n\n\t\t// Create host user\n\t\thostUser, err := ts.CreateHostUser(ctx, \"admin\")\n\t\trequire.NoError(t, err)\n\n\t\t// Set user context\n\t\tuserCtx := ts.CreateUserContext(ctx, hostUser.ID)\n\n\t\treq := &v1pb.UpdateIdentityProviderRequest{\n\t\t\tIdentityProvider: &v1pb.IdentityProvider{\n\t\t\t\tName:  \"identity-providers/1\",\n\t\t\t\tTitle: \"Updated Provider\",\n\t\t\t},\n\t\t}\n\n\t\t_, err = ts.Service.UpdateIdentityProvider(userCtx, req)\n\t\trequire.Error(t, err)\n\t\trequire.Contains(t, err.Error(), \"update_mask is required\")\n\t})\n\n\tt.Run(\"UpdateIdentityProvider invalid name\", func(t *testing.T) {\n\t\tts := NewTestService(t)\n\t\tdefer ts.Cleanup()\n\n\t\t// Create host user\n\t\thostUser, err := ts.CreateHostUser(ctx, \"admin\")\n\t\trequire.NoError(t, err)\n\n\t\t// Set user context\n\t\tuserCtx := ts.CreateUserContext(ctx, hostUser.ID)\n\n\t\treq := &v1pb.UpdateIdentityProviderRequest{\n\t\t\tIdentityProvider: &v1pb.IdentityProvider{\n\t\t\t\tName:  \"invalid-name\",\n\t\t\t\tTitle: \"Updated Provider\",\n\t\t\t},\n\t\t\tUpdateMask: &fieldmaskpb.FieldMask{\n\t\t\t\tPaths: []string{\"title\"},\n\t\t\t},\n\t\t}\n\n\t\t_, err = ts.Service.UpdateIdentityProvider(userCtx, req)\n\t\trequire.Error(t, err)\n\t\trequire.Contains(t, err.Error(), \"invalid identity provider name\")\n\t})\n}\n\nfunc TestDeleteIdentityProvider(t *testing.T) {\n\tctx := context.Background()\n\n\tt.Run(\"DeleteIdentityProvider success\", func(t *testing.T) {\n\t\tts := NewTestService(t)\n\t\tdefer ts.Cleanup()\n\n\t\t// Create host user\n\t\thostUser, err := ts.CreateHostUser(ctx, \"admin\")\n\t\trequire.NoError(t, err)\n\n\t\t// Set user context\n\t\tuserCtx := ts.CreateUserContext(ctx, hostUser.ID)\n\n\t\t// Create identity provider\n\t\tcreateReq := &v1pb.CreateIdentityProviderRequest{\n\t\t\tIdentityProvider: &v1pb.IdentityProvider{\n\t\t\t\tTitle: \"Provider to Delete\",\n\t\t\t\tType:  v1pb.IdentityProvider_OAUTH2,\n\t\t\t\tConfig: &v1pb.IdentityProviderConfig{\n\t\t\t\t\tConfig: &v1pb.IdentityProviderConfig_Oauth2Config{\n\t\t\t\t\t\tOauth2Config: &v1pb.OAuth2Config{\n\t\t\t\t\t\t\tClientId:    \"client-to-delete\",\n\t\t\t\t\t\t\tAuthUrl:     \"https://example.com/auth\",\n\t\t\t\t\t\t\tTokenUrl:    \"https://example.com/token\",\n\t\t\t\t\t\t\tUserInfoUrl: \"https://example.com/user\",\n\t\t\t\t\t\t\tFieldMapping: &v1pb.FieldMapping{\n\t\t\t\t\t\t\t\tIdentifier: \"id\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\n\t\tcreated, err := ts.Service.CreateIdentityProvider(userCtx, createReq)\n\t\trequire.NoError(t, err)\n\n\t\t// Delete identity provider\n\t\tdeleteReq := &v1pb.DeleteIdentityProviderRequest{\n\t\t\tName: created.Name,\n\t\t}\n\n\t\t_, err = ts.Service.DeleteIdentityProvider(userCtx, deleteReq)\n\t\trequire.NoError(t, err)\n\n\t\t// Verify deletion\n\t\tgetReq := &v1pb.GetIdentityProviderRequest{\n\t\t\tName: created.Name,\n\t\t}\n\n\t\t_, err = ts.Service.GetIdentityProvider(ctx, getReq)\n\t\trequire.Error(t, err)\n\t\trequire.Contains(t, err.Error(), \"not found\")\n\t})\n\n\tt.Run(\"DeleteIdentityProvider invalid name\", func(t *testing.T) {\n\t\tts := NewTestService(t)\n\t\tdefer ts.Cleanup()\n\n\t\t// Create host user\n\t\thostUser, err := ts.CreateHostUser(ctx, \"admin\")\n\t\trequire.NoError(t, err)\n\n\t\t// Set user context\n\t\tuserCtx := ts.CreateUserContext(ctx, hostUser.ID)\n\n\t\treq := &v1pb.DeleteIdentityProviderRequest{\n\t\t\tName: \"invalid-name\",\n\t\t}\n\n\t\t_, err = ts.Service.DeleteIdentityProvider(userCtx, req)\n\t\trequire.Error(t, err)\n\t\trequire.Contains(t, err.Error(), \"invalid identity provider name\")\n\t})\n\n\tt.Run(\"DeleteIdentityProvider not found\", func(t *testing.T) {\n\t\tts := NewTestService(t)\n\t\tdefer ts.Cleanup()\n\n\t\t// Create host user\n\t\thostUser, err := ts.CreateHostUser(ctx, \"admin\")\n\t\trequire.NoError(t, err)\n\n\t\t// Set user context\n\t\tuserCtx := ts.CreateUserContext(ctx, hostUser.ID)\n\n\t\treq := &v1pb.DeleteIdentityProviderRequest{\n\t\t\tName: \"identity-providers/999\",\n\t\t}\n\n\t\t_, err = ts.Service.DeleteIdentityProvider(userCtx, req)\n\t\trequire.Error(t, err)\n\t\t// Note: Delete might succeed even if item doesn't exist, depending on store implementation\n\t})\n}\n\nfunc TestIdentityProviderPermissions(t *testing.T) {\n\tctx := context.Background()\n\n\tt.Run(\"Only host users can create identity providers\", func(t *testing.T) {\n\t\tts := NewTestService(t)\n\t\tdefer ts.Cleanup()\n\n\t\t// Create regular user\n\t\tregularUser, err := ts.CreateRegularUser(ctx, \"regularuser\")\n\t\trequire.NoError(t, err)\n\n\t\t// Set user context\n\t\tuserCtx := ts.CreateUserContext(ctx, regularUser.ID)\n\n\t\treq := &v1pb.CreateIdentityProviderRequest{\n\t\t\tIdentityProvider: &v1pb.IdentityProvider{\n\t\t\t\tTitle: \"Test Provider\",\n\t\t\t\tType:  v1pb.IdentityProvider_OAUTH2,\n\t\t\t},\n\t\t}\n\n\t\t_, err = ts.Service.CreateIdentityProvider(userCtx, req)\n\t\trequire.Error(t, err)\n\t\trequire.Contains(t, err.Error(), \"permission denied\")\n\t})\n\n\tt.Run(\"Authentication required\", func(t *testing.T) {\n\t\tts := NewTestService(t)\n\t\tdefer ts.Cleanup()\n\n\t\treq := &v1pb.CreateIdentityProviderRequest{\n\t\t\tIdentityProvider: &v1pb.IdentityProvider{\n\t\t\t\tTitle: \"Test Provider\",\n\t\t\t\tType:  v1pb.IdentityProvider_OAUTH2,\n\t\t\t},\n\t\t}\n\n\t\t_, err := ts.Service.CreateIdentityProvider(ctx, req)\n\t\trequire.Error(t, err)\n\t\trequire.Contains(t, err.Error(), \"user not authenticated\")\n\t})\n}\n"
  },
  {
    "path": "server/router/api/v1/test/instance_admin_cache_test.go",
    "content": "package test\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n\n\tv1pb \"github.com/usememos/memos/proto/gen/api/v1\"\n)\n\nfunc TestInstanceAdminRetrieval(t *testing.T) {\n\tctx := context.Background()\n\n\tt.Run(\"Instance becomes initialized after first admin user is created\", func(t *testing.T) {\n\t\t// Create test service\n\t\tts := NewTestService(t)\n\t\tdefer ts.Cleanup()\n\n\t\t// Verify instance is not initialized initially\n\t\tprofile1, err := ts.Service.GetInstanceProfile(ctx, &v1pb.GetInstanceProfileRequest{})\n\t\trequire.NoError(t, err)\n\t\trequire.Nil(t, profile1.Admin, \"Instance should not be initialized before first admin user\")\n\n\t\t// Create the first admin user\n\t\tuser, err := ts.CreateHostUser(ctx, \"admin\")\n\t\trequire.NoError(t, err)\n\t\trequire.NotNil(t, user)\n\n\t\t// Verify instance is now initialized\n\t\tprofile2, err := ts.Service.GetInstanceProfile(ctx, &v1pb.GetInstanceProfileRequest{})\n\t\trequire.NoError(t, err)\n\t\trequire.NotNil(t, profile2.Admin, \"Instance should be initialized after first admin user is created\")\n\t\trequire.Equal(t, user.Username, profile2.Admin.Username)\n\t})\n\n\tt.Run(\"Admin retrieval is cached by Store layer\", func(t *testing.T) {\n\t\t// Create test service\n\t\tts := NewTestService(t)\n\t\tdefer ts.Cleanup()\n\n\t\t// Create admin user\n\t\tuser, err := ts.CreateHostUser(ctx, \"admin\")\n\t\trequire.NoError(t, err)\n\n\t\t// Multiple calls should return consistent admin user (from cache)\n\t\tfor i := 0; i < 5; i++ {\n\t\t\tprofile, err := ts.Service.GetInstanceProfile(ctx, &v1pb.GetInstanceProfileRequest{})\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.NotNil(t, profile.Admin)\n\t\t\trequire.Equal(t, user.Username, profile.Admin.Username)\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "server/router/api/v1/test/instance_service_test.go",
    "content": "package test\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n\tcolorpb \"google.golang.org/genproto/googleapis/type/color\"\n\t\"google.golang.org/protobuf/types/known/fieldmaskpb\"\n\n\tv1pb \"github.com/usememos/memos/proto/gen/api/v1\"\n)\n\nfunc TestGetInstanceProfile(t *testing.T) {\n\tctx := context.Background()\n\n\tt.Run(\"GetInstanceProfile returns instance profile\", func(t *testing.T) {\n\t\t// Create test service for this specific test\n\t\tts := NewTestService(t)\n\t\tdefer ts.Cleanup()\n\n\t\t// Call GetInstanceProfile directly\n\t\treq := &v1pb.GetInstanceProfileRequest{}\n\t\tresp, err := ts.Service.GetInstanceProfile(ctx, req)\n\n\t\t// Verify response\n\t\trequire.NoError(t, err)\n\t\trequire.NotNil(t, resp)\n\n\t\t// Verify the response contains expected data\n\t\trequire.Equal(t, \"test-1.0.0\", resp.Version)\n\t\trequire.True(t, resp.Demo)\n\t\trequire.Equal(t, \"http://localhost:8080\", resp.InstanceUrl)\n\n\t\t// Instance should not be initialized since no admin users are created\n\t\trequire.Nil(t, resp.Admin)\n\t})\n\n\tt.Run(\"GetInstanceProfile with initialized instance\", func(t *testing.T) {\n\t\t// Create test service for this specific test\n\t\tts := NewTestService(t)\n\t\tdefer ts.Cleanup()\n\n\t\t// Create a host user in the store\n\t\thostUser, err := ts.CreateHostUser(ctx, \"admin\")\n\t\trequire.NoError(t, err)\n\t\trequire.NotNil(t, hostUser)\n\n\t\t// Call GetInstanceProfile directly\n\t\treq := &v1pb.GetInstanceProfileRequest{}\n\t\tresp, err := ts.Service.GetInstanceProfile(ctx, req)\n\n\t\t// Verify response\n\t\trequire.NoError(t, err)\n\t\trequire.NotNil(t, resp)\n\n\t\t// Verify the response contains expected data with initialized flag\n\t\trequire.Equal(t, \"test-1.0.0\", resp.Version)\n\t\trequire.True(t, resp.Demo)\n\t\trequire.Equal(t, \"http://localhost:8080\", resp.InstanceUrl)\n\n\t\t// Instance should be initialized since an admin user exists\n\t\trequire.NotNil(t, resp.Admin)\n\t\trequire.Equal(t, hostUser.Username, resp.Admin.Username)\n\t})\n}\n\nfunc TestGetInstanceProfile_Concurrency(t *testing.T) {\n\tctx := context.Background()\n\n\tt.Run(\"Concurrent access to service\", func(t *testing.T) {\n\t\t// Create test service for this specific test\n\t\tts := NewTestService(t)\n\t\tdefer ts.Cleanup()\n\n\t\t// Create a host user\n\t\t_, err := ts.CreateHostUser(ctx, \"admin\")\n\t\trequire.NoError(t, err)\n\n\t\t// Make concurrent requests\n\t\tnumGoroutines := 10\n\t\tresults := make(chan *v1pb.InstanceProfile, numGoroutines)\n\t\terrors := make(chan error, numGoroutines)\n\n\t\tfor i := 0; i < numGoroutines; i++ {\n\t\t\tgo func() {\n\t\t\t\treq := &v1pb.GetInstanceProfileRequest{}\n\t\t\t\tresp, err := ts.Service.GetInstanceProfile(ctx, req)\n\t\t\t\tif err != nil {\n\t\t\t\t\terrors <- err\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tresults <- resp\n\t\t\t}()\n\t\t}\n\n\t\t// Collect all results\n\t\tfor i := 0; i < numGoroutines; i++ {\n\t\t\tselect {\n\t\t\tcase err := <-errors:\n\t\t\t\tt.Fatalf(\"Goroutine returned error: %v\", err)\n\t\t\tcase resp := <-results:\n\t\t\t\trequire.NotNil(t, resp)\n\t\t\t\trequire.Equal(t, \"test-1.0.0\", resp.Version)\n\t\t\t\trequire.True(t, resp.Demo)\n\t\t\t\trequire.Equal(t, \"http://localhost:8080\", resp.InstanceUrl)\n\t\t\t\trequire.NotNil(t, resp.Admin)\n\t\t\t}\n\t\t}\n\t})\n}\n\nfunc TestGetInstanceSetting(t *testing.T) {\n\tctx := context.Background()\n\n\tt.Run(\"GetInstanceSetting - general setting\", func(t *testing.T) {\n\t\t// Create test service for this specific test\n\t\tts := NewTestService(t)\n\t\tdefer ts.Cleanup()\n\n\t\t// Call GetInstanceSetting for general setting\n\t\treq := &v1pb.GetInstanceSettingRequest{\n\t\t\tName: \"instance/settings/GENERAL\",\n\t\t}\n\t\tresp, err := ts.Service.GetInstanceSetting(ctx, req)\n\n\t\t// Verify response\n\t\trequire.NoError(t, err)\n\t\trequire.NotNil(t, resp)\n\t\trequire.Equal(t, \"instance/settings/GENERAL\", resp.Name)\n\n\t\t// The general setting should have a general_setting field\n\t\tgeneralSetting := resp.GetGeneralSetting()\n\t\trequire.NotNil(t, generalSetting)\n\n\t\t// General setting should have default values\n\t\trequire.False(t, generalSetting.DisallowUserRegistration)\n\t\trequire.False(t, generalSetting.DisallowPasswordAuth)\n\t\trequire.Empty(t, generalSetting.AdditionalScript)\n\t})\n\n\tt.Run(\"GetInstanceSetting - storage setting\", func(t *testing.T) {\n\t\t// Create test service for this specific test\n\t\tts := NewTestService(t)\n\t\tdefer ts.Cleanup()\n\n\t\t// Create a host user for storage setting access\n\t\thostUser, err := ts.CreateHostUser(ctx, \"testhost\")\n\t\trequire.NoError(t, err)\n\n\t\t// Add user to context\n\t\tuserCtx := ts.CreateUserContext(ctx, hostUser.ID)\n\n\t\t// Call GetInstanceSetting for storage setting\n\t\treq := &v1pb.GetInstanceSettingRequest{\n\t\t\tName: \"instance/settings/STORAGE\",\n\t\t}\n\t\tresp, err := ts.Service.GetInstanceSetting(userCtx, req)\n\n\t\t// Verify response\n\t\trequire.NoError(t, err)\n\t\trequire.NotNil(t, resp)\n\t\trequire.Equal(t, \"instance/settings/STORAGE\", resp.Name)\n\n\t\t// The storage setting should have a storage_setting field\n\t\tstorageSetting := resp.GetStorageSetting()\n\t\trequire.NotNil(t, storageSetting)\n\t})\n\n\tt.Run(\"GetInstanceSetting - memo related setting\", func(t *testing.T) {\n\t\t// Create test service for this specific test\n\t\tts := NewTestService(t)\n\t\tdefer ts.Cleanup()\n\n\t\t// Call GetInstanceSetting for memo related setting\n\t\treq := &v1pb.GetInstanceSettingRequest{\n\t\t\tName: \"instance/settings/MEMO_RELATED\",\n\t\t}\n\t\tresp, err := ts.Service.GetInstanceSetting(ctx, req)\n\n\t\t// Verify response\n\t\trequire.NoError(t, err)\n\t\trequire.NotNil(t, resp)\n\t\trequire.Equal(t, \"instance/settings/MEMO_RELATED\", resp.Name)\n\n\t\t// The memo related setting should have a memo_related_setting field\n\t\tmemoRelatedSetting := resp.GetMemoRelatedSetting()\n\t\trequire.NotNil(t, memoRelatedSetting)\n\t})\n\n\tt.Run(\"GetInstanceSetting - tags setting\", func(t *testing.T) {\n\t\tts := NewTestService(t)\n\t\tdefer ts.Cleanup()\n\n\t\treq := &v1pb.GetInstanceSettingRequest{\n\t\t\tName: \"instance/settings/TAGS\",\n\t\t}\n\t\tresp, err := ts.Service.GetInstanceSetting(ctx, req)\n\n\t\trequire.NoError(t, err)\n\t\trequire.NotNil(t, resp)\n\t\trequire.Equal(t, \"instance/settings/TAGS\", resp.Name)\n\t\trequire.NotNil(t, resp.GetTagsSetting())\n\t\trequire.Empty(t, resp.GetTagsSetting().GetTags())\n\t})\n\n\tt.Run(\"GetInstanceSetting - notification setting\", func(t *testing.T) {\n\t\tts := NewTestService(t)\n\t\tdefer ts.Cleanup()\n\n\t\treq := &v1pb.GetInstanceSettingRequest{\n\t\t\tName: \"instance/settings/NOTIFICATION\",\n\t\t}\n\t\tresp, err := ts.Service.GetInstanceSetting(ctx, req)\n\n\t\trequire.NoError(t, err)\n\t\trequire.NotNil(t, resp)\n\t\trequire.Equal(t, \"instance/settings/NOTIFICATION\", resp.Name)\n\t\trequire.NotNil(t, resp.GetNotificationSetting())\n\t\trequire.NotNil(t, resp.GetNotificationSetting().GetEmail())\n\t\trequire.False(t, resp.GetNotificationSetting().GetEmail().GetEnabled())\n\t})\n\n\tt.Run(\"GetInstanceSetting - invalid setting name\", func(t *testing.T) {\n\t\t// Create test service for this specific test\n\t\tts := NewTestService(t)\n\t\tdefer ts.Cleanup()\n\n\t\t// Call GetInstanceSetting with invalid name\n\t\treq := &v1pb.GetInstanceSettingRequest{\n\t\t\tName: \"invalid/setting/name\",\n\t\t}\n\t\t_, err := ts.Service.GetInstanceSetting(ctx, req)\n\n\t\t// Should return an error\n\t\trequire.Error(t, err)\n\t\trequire.Contains(t, err.Error(), \"invalid instance setting name\")\n\t})\n}\n\nfunc TestUpdateInstanceSetting(t *testing.T) {\n\tctx := context.Background()\n\n\tt.Run(\"UpdateInstanceSetting - tags setting\", func(t *testing.T) {\n\t\tts := NewTestService(t)\n\t\tdefer ts.Cleanup()\n\n\t\thostUser, err := ts.CreateHostUser(ctx, \"admin\")\n\t\trequire.NoError(t, err)\n\n\t\tresp, err := ts.Service.UpdateInstanceSetting(ts.CreateUserContext(ctx, hostUser.ID), &v1pb.UpdateInstanceSettingRequest{\n\t\t\tSetting: &v1pb.InstanceSetting{\n\t\t\t\tName: \"instance/settings/TAGS\",\n\t\t\t\tValue: &v1pb.InstanceSetting_TagsSetting_{\n\t\t\t\t\tTagsSetting: &v1pb.InstanceSetting_TagsSetting{\n\t\t\t\t\t\tTags: map[string]*v1pb.InstanceSetting_TagMetadata{\n\t\t\t\t\t\t\t\"bug\": {\n\t\t\t\t\t\t\t\tBackgroundColor: &colorpb.Color{\n\t\t\t\t\t\t\t\t\tRed:   0.9,\n\t\t\t\t\t\t\t\t\tGreen: 0.1,\n\t\t\t\t\t\t\t\t\tBlue:  0.1,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tUpdateMask: &fieldmaskpb.FieldMask{Paths: []string{\"tags\"}},\n\t\t})\n\t\trequire.NoError(t, err)\n\t\trequire.NotNil(t, resp.GetTagsSetting())\n\t\trequire.Contains(t, resp.GetTagsSetting().GetTags(), \"bug\")\n\t})\n\n\tt.Run(\"UpdateInstanceSetting - invalid tags color\", func(t *testing.T) {\n\t\tts := NewTestService(t)\n\t\tdefer ts.Cleanup()\n\n\t\thostUser, err := ts.CreateHostUser(ctx, \"admin\")\n\t\trequire.NoError(t, err)\n\n\t\t_, err = ts.Service.UpdateInstanceSetting(ts.CreateUserContext(ctx, hostUser.ID), &v1pb.UpdateInstanceSettingRequest{\n\t\t\tSetting: &v1pb.InstanceSetting{\n\t\t\t\tName: \"instance/settings/TAGS\",\n\t\t\t\tValue: &v1pb.InstanceSetting_TagsSetting_{\n\t\t\t\t\tTagsSetting: &v1pb.InstanceSetting_TagsSetting{\n\t\t\t\t\t\tTags: map[string]*v1pb.InstanceSetting_TagMetadata{\n\t\t\t\t\t\t\t\"bug\": {\n\t\t\t\t\t\t\t\tBackgroundColor: &colorpb.Color{\n\t\t\t\t\t\t\t\t\tRed:   1.2,\n\t\t\t\t\t\t\t\t\tGreen: 0.1,\n\t\t\t\t\t\t\t\t\tBlue:  0.1,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t})\n\t\trequire.Error(t, err)\n\t\trequire.Contains(t, err.Error(), \"invalid instance setting\")\n\t})\n\n\tt.Run(\"UpdateInstanceSetting - notification setting\", func(t *testing.T) {\n\t\tts := NewTestService(t)\n\t\tdefer ts.Cleanup()\n\n\t\thostUser, err := ts.CreateHostUser(ctx, \"admin\")\n\t\trequire.NoError(t, err)\n\n\t\tresp, err := ts.Service.UpdateInstanceSetting(ts.CreateUserContext(ctx, hostUser.ID), &v1pb.UpdateInstanceSettingRequest{\n\t\t\tSetting: &v1pb.InstanceSetting{\n\t\t\t\tName: \"instance/settings/NOTIFICATION\",\n\t\t\t\tValue: &v1pb.InstanceSetting_NotificationSetting_{\n\t\t\t\t\tNotificationSetting: &v1pb.InstanceSetting_NotificationSetting{\n\t\t\t\t\t\tEmail: &v1pb.InstanceSetting_NotificationSetting_EmailSetting{\n\t\t\t\t\t\t\tEnabled:      true,\n\t\t\t\t\t\t\tSmtpHost:     \"smtp.example.com\",\n\t\t\t\t\t\t\tSmtpPort:     587,\n\t\t\t\t\t\t\tSmtpUsername: \"bot@example.com\",\n\t\t\t\t\t\t\tSmtpPassword: \"secret\",\n\t\t\t\t\t\t\tFromEmail:    \"bot@example.com\",\n\t\t\t\t\t\t\tFromName:     \"Memos Bot\",\n\t\t\t\t\t\t\tReplyTo:      \"support@example.com\",\n\t\t\t\t\t\t\tUseTls:       true,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tUpdateMask: &fieldmaskpb.FieldMask{Paths: []string{\"notification_setting\"}},\n\t\t})\n\t\trequire.NoError(t, err)\n\t\trequire.NotNil(t, resp.GetNotificationSetting())\n\t\trequire.NotNil(t, resp.GetNotificationSetting().GetEmail())\n\t\trequire.True(t, resp.GetNotificationSetting().GetEmail().GetEnabled())\n\t\trequire.Equal(t, \"smtp.example.com\", resp.GetNotificationSetting().GetEmail().GetSmtpHost())\n\t})\n}\n"
  },
  {
    "path": "server/router/api/v1/test/memo_attachment_service_test.go",
    "content": "package test\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n\n\tapiv1 \"github.com/usememos/memos/proto/gen/api/v1\"\n)\n\nfunc TestSetMemoAttachments(t *testing.T) {\n\tctx := context.Background()\n\n\tt.Run(\"SetMemoAttachments success by memo owner\", func(t *testing.T) {\n\t\tts := NewTestService(t)\n\t\tdefer ts.Cleanup()\n\n\t\t// Create user\n\t\tuser, err := ts.CreateRegularUser(ctx, \"user\")\n\t\trequire.NoError(t, err)\n\t\tuserCtx := ts.CreateUserContext(ctx, user.ID)\n\n\t\t// Create memo\n\t\tmemo, err := ts.Service.CreateMemo(userCtx, &apiv1.CreateMemoRequest{\n\t\t\tMemo: &apiv1.Memo{\n\t\t\t\tContent:    \"Test memo\",\n\t\t\t\tVisibility: apiv1.Visibility_PRIVATE,\n\t\t\t},\n\t\t})\n\t\trequire.NoError(t, err)\n\t\trequire.NotNil(t, memo)\n\n\t\t// Create attachment\n\t\tattachment, err := ts.Service.CreateAttachment(userCtx, &apiv1.CreateAttachmentRequest{\n\t\t\tAttachment: &apiv1.Attachment{\n\t\t\t\tFilename: \"test.txt\",\n\t\t\t\tSize:     5,\n\t\t\t\tType:     \"text/plain\",\n\t\t\t\tContent:  []byte(\"hello\"),\n\t\t\t},\n\t\t})\n\t\trequire.NoError(t, err)\n\t\trequire.NotNil(t, attachment)\n\n\t\t// Set memo attachments - should succeed\n\t\t_, err = ts.Service.SetMemoAttachments(userCtx, &apiv1.SetMemoAttachmentsRequest{\n\t\t\tName: memo.Name,\n\t\t\tAttachments: []*apiv1.Attachment{\n\t\t\t\t{Name: attachment.Name},\n\t\t\t},\n\t\t})\n\t\trequire.NoError(t, err)\n\t})\n\n\tt.Run(\"SetMemoAttachments success by host user\", func(t *testing.T) {\n\t\tts := NewTestService(t)\n\t\tdefer ts.Cleanup()\n\n\t\t// Create regular user\n\t\tregularUser, err := ts.CreateRegularUser(ctx, \"user\")\n\t\trequire.NoError(t, err)\n\t\tregularUserCtx := ts.CreateUserContext(ctx, regularUser.ID)\n\n\t\t// Create host user\n\t\thostUser, err := ts.CreateHostUser(ctx, \"admin\")\n\t\trequire.NoError(t, err)\n\t\thostCtx := ts.CreateUserContext(ctx, hostUser.ID)\n\n\t\t// Create memo by regular user\n\t\tmemo, err := ts.Service.CreateMemo(regularUserCtx, &apiv1.CreateMemoRequest{\n\t\t\tMemo: &apiv1.Memo{\n\t\t\t\tContent:    \"Test memo\",\n\t\t\t\tVisibility: apiv1.Visibility_PRIVATE,\n\t\t\t},\n\t\t})\n\t\trequire.NoError(t, err)\n\t\trequire.NotNil(t, memo)\n\n\t\t// Host user can modify attachments - should succeed\n\t\t_, err = ts.Service.SetMemoAttachments(hostCtx, &apiv1.SetMemoAttachmentsRequest{\n\t\t\tName:        memo.Name,\n\t\t\tAttachments: []*apiv1.Attachment{},\n\t\t})\n\t\trequire.NoError(t, err)\n\t})\n\n\tt.Run(\"SetMemoAttachments permission denied for non-owner\", func(t *testing.T) {\n\t\tts := NewTestService(t)\n\t\tdefer ts.Cleanup()\n\n\t\t// Create user1\n\t\tuser1, err := ts.CreateRegularUser(ctx, \"user1\")\n\t\trequire.NoError(t, err)\n\t\tuser1Ctx := ts.CreateUserContext(ctx, user1.ID)\n\n\t\t// Create user2\n\t\tuser2, err := ts.CreateRegularUser(ctx, \"user2\")\n\t\trequire.NoError(t, err)\n\t\tuser2Ctx := ts.CreateUserContext(ctx, user2.ID)\n\n\t\t// Create memo by user1\n\t\tmemo, err := ts.Service.CreateMemo(user1Ctx, &apiv1.CreateMemoRequest{\n\t\t\tMemo: &apiv1.Memo{\n\t\t\t\tContent:    \"Test memo\",\n\t\t\t\tVisibility: apiv1.Visibility_PRIVATE,\n\t\t\t},\n\t\t})\n\t\trequire.NoError(t, err)\n\t\trequire.NotNil(t, memo)\n\n\t\t// User2 tries to modify attachments - should fail\n\t\t_, err = ts.Service.SetMemoAttachments(user2Ctx, &apiv1.SetMemoAttachmentsRequest{\n\t\t\tName:        memo.Name,\n\t\t\tAttachments: []*apiv1.Attachment{},\n\t\t})\n\t\trequire.Error(t, err)\n\t\trequire.Contains(t, err.Error(), \"permission denied\")\n\t})\n\n\tt.Run(\"SetMemoAttachments unauthenticated\", func(t *testing.T) {\n\t\tts := NewTestService(t)\n\t\tdefer ts.Cleanup()\n\n\t\t// Create user\n\t\tuser, err := ts.CreateRegularUser(ctx, \"user\")\n\t\trequire.NoError(t, err)\n\t\tuserCtx := ts.CreateUserContext(ctx, user.ID)\n\n\t\t// Create memo\n\t\tmemo, err := ts.Service.CreateMemo(userCtx, &apiv1.CreateMemoRequest{\n\t\t\tMemo: &apiv1.Memo{\n\t\t\t\tContent:    \"Test memo\",\n\t\t\t\tVisibility: apiv1.Visibility_PRIVATE,\n\t\t\t},\n\t\t})\n\t\trequire.NoError(t, err)\n\t\trequire.NotNil(t, memo)\n\n\t\t// Unauthenticated user tries to modify attachments - should fail\n\t\t_, err = ts.Service.SetMemoAttachments(ctx, &apiv1.SetMemoAttachmentsRequest{\n\t\t\tName:        memo.Name,\n\t\t\tAttachments: []*apiv1.Attachment{},\n\t\t})\n\t\trequire.Error(t, err)\n\t\trequire.Contains(t, err.Error(), \"not authenticated\")\n\t})\n\n\tt.Run(\"SetMemoAttachments memo not found\", func(t *testing.T) {\n\t\tts := NewTestService(t)\n\t\tdefer ts.Cleanup()\n\n\t\t// Create user\n\t\tuser, err := ts.CreateRegularUser(ctx, \"user\")\n\t\trequire.NoError(t, err)\n\t\tuserCtx := ts.CreateUserContext(ctx, user.ID)\n\n\t\t// Try to set attachments on non-existent memo - should fail\n\t\t_, err = ts.Service.SetMemoAttachments(userCtx, &apiv1.SetMemoAttachmentsRequest{\n\t\t\tName:        \"memos/nonexistent-uid-12345\",\n\t\t\tAttachments: []*apiv1.Attachment{},\n\t\t})\n\t\trequire.Error(t, err)\n\t\trequire.Contains(t, err.Error(), \"not found\")\n\t})\n}\n"
  },
  {
    "path": "server/router/api/v1/test/memo_relation_service_test.go",
    "content": "package test\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n\n\tapiv1 \"github.com/usememos/memos/proto/gen/api/v1\"\n)\n\nfunc TestSetMemoRelations(t *testing.T) {\n\tctx := context.Background()\n\n\tt.Run(\"SetMemoRelations success by memo owner\", func(t *testing.T) {\n\t\tts := NewTestService(t)\n\t\tdefer ts.Cleanup()\n\n\t\t// Create user\n\t\tuser, err := ts.CreateRegularUser(ctx, \"user\")\n\t\trequire.NoError(t, err)\n\t\tuserCtx := ts.CreateUserContext(ctx, user.ID)\n\n\t\t// Create memo1\n\t\tmemo1, err := ts.Service.CreateMemo(userCtx, &apiv1.CreateMemoRequest{\n\t\t\tMemo: &apiv1.Memo{\n\t\t\t\tContent:    \"Test memo 1\",\n\t\t\t\tVisibility: apiv1.Visibility_PRIVATE,\n\t\t\t},\n\t\t})\n\t\trequire.NoError(t, err)\n\t\trequire.NotNil(t, memo1)\n\n\t\t// Create memo2\n\t\tmemo2, err := ts.Service.CreateMemo(userCtx, &apiv1.CreateMemoRequest{\n\t\t\tMemo: &apiv1.Memo{\n\t\t\t\tContent:    \"Test memo 2\",\n\t\t\t\tVisibility: apiv1.Visibility_PRIVATE,\n\t\t\t},\n\t\t})\n\t\trequire.NoError(t, err)\n\t\trequire.NotNil(t, memo2)\n\n\t\t// Set memo relations - should succeed\n\t\t_, err = ts.Service.SetMemoRelations(userCtx, &apiv1.SetMemoRelationsRequest{\n\t\t\tName: memo1.Name,\n\t\t\tRelations: []*apiv1.MemoRelation{\n\t\t\t\t{\n\t\t\t\t\tRelatedMemo: &apiv1.MemoRelation_Memo{\n\t\t\t\t\t\tName: memo2.Name,\n\t\t\t\t\t},\n\t\t\t\t\tType: apiv1.MemoRelation_REFERENCE,\n\t\t\t\t},\n\t\t\t},\n\t\t})\n\t\trequire.NoError(t, err)\n\t})\n\n\tt.Run(\"SetMemoRelations success by host user\", func(t *testing.T) {\n\t\tts := NewTestService(t)\n\t\tdefer ts.Cleanup()\n\n\t\t// Create regular user\n\t\tregularUser, err := ts.CreateRegularUser(ctx, \"user\")\n\t\trequire.NoError(t, err)\n\t\tregularUserCtx := ts.CreateUserContext(ctx, regularUser.ID)\n\n\t\t// Create host user\n\t\thostUser, err := ts.CreateHostUser(ctx, \"admin\")\n\t\trequire.NoError(t, err)\n\t\thostCtx := ts.CreateUserContext(ctx, hostUser.ID)\n\n\t\t// Create memo by regular user\n\t\tmemo, err := ts.Service.CreateMemo(regularUserCtx, &apiv1.CreateMemoRequest{\n\t\t\tMemo: &apiv1.Memo{\n\t\t\t\tContent:    \"Test memo\",\n\t\t\t\tVisibility: apiv1.Visibility_PRIVATE,\n\t\t\t},\n\t\t})\n\t\trequire.NoError(t, err)\n\t\trequire.NotNil(t, memo)\n\n\t\t// Host user can modify relations - should succeed\n\t\t_, err = ts.Service.SetMemoRelations(hostCtx, &apiv1.SetMemoRelationsRequest{\n\t\t\tName:      memo.Name,\n\t\t\tRelations: []*apiv1.MemoRelation{},\n\t\t})\n\t\trequire.NoError(t, err)\n\t})\n\n\tt.Run(\"SetMemoRelations permission denied for non-owner\", func(t *testing.T) {\n\t\tts := NewTestService(t)\n\t\tdefer ts.Cleanup()\n\n\t\t// Create user1\n\t\tuser1, err := ts.CreateRegularUser(ctx, \"user1\")\n\t\trequire.NoError(t, err)\n\t\tuser1Ctx := ts.CreateUserContext(ctx, user1.ID)\n\n\t\t// Create user2\n\t\tuser2, err := ts.CreateRegularUser(ctx, \"user2\")\n\t\trequire.NoError(t, err)\n\t\tuser2Ctx := ts.CreateUserContext(ctx, user2.ID)\n\n\t\t// Create memo by user1\n\t\tmemo, err := ts.Service.CreateMemo(user1Ctx, &apiv1.CreateMemoRequest{\n\t\t\tMemo: &apiv1.Memo{\n\t\t\t\tContent:    \"Test memo\",\n\t\t\t\tVisibility: apiv1.Visibility_PRIVATE,\n\t\t\t},\n\t\t})\n\t\trequire.NoError(t, err)\n\t\trequire.NotNil(t, memo)\n\n\t\t// User2 tries to modify relations - should fail\n\t\t_, err = ts.Service.SetMemoRelations(user2Ctx, &apiv1.SetMemoRelationsRequest{\n\t\t\tName:      memo.Name,\n\t\t\tRelations: []*apiv1.MemoRelation{},\n\t\t})\n\t\trequire.Error(t, err)\n\t\trequire.Contains(t, err.Error(), \"permission denied\")\n\t})\n\n\tt.Run(\"SetMemoRelations unauthenticated\", func(t *testing.T) {\n\t\tts := NewTestService(t)\n\t\tdefer ts.Cleanup()\n\n\t\t// Create user\n\t\tuser, err := ts.CreateRegularUser(ctx, \"user\")\n\t\trequire.NoError(t, err)\n\t\tuserCtx := ts.CreateUserContext(ctx, user.ID)\n\n\t\t// Create memo\n\t\tmemo, err := ts.Service.CreateMemo(userCtx, &apiv1.CreateMemoRequest{\n\t\t\tMemo: &apiv1.Memo{\n\t\t\t\tContent:    \"Test memo\",\n\t\t\t\tVisibility: apiv1.Visibility_PRIVATE,\n\t\t\t},\n\t\t})\n\t\trequire.NoError(t, err)\n\t\trequire.NotNil(t, memo)\n\n\t\t// Unauthenticated user tries to modify relations - should fail\n\t\t_, err = ts.Service.SetMemoRelations(ctx, &apiv1.SetMemoRelationsRequest{\n\t\t\tName:      memo.Name,\n\t\t\tRelations: []*apiv1.MemoRelation{},\n\t\t})\n\t\trequire.Error(t, err)\n\t\trequire.Contains(t, err.Error(), \"not authenticated\")\n\t})\n\n\tt.Run(\"SetMemoRelations memo not found\", func(t *testing.T) {\n\t\tts := NewTestService(t)\n\t\tdefer ts.Cleanup()\n\n\t\t// Create user\n\t\tuser, err := ts.CreateRegularUser(ctx, \"user\")\n\t\trequire.NoError(t, err)\n\t\tuserCtx := ts.CreateUserContext(ctx, user.ID)\n\n\t\t// Try to set relations on non-existent memo - should fail\n\t\t_, err = ts.Service.SetMemoRelations(userCtx, &apiv1.SetMemoRelationsRequest{\n\t\t\tName:      \"memos/nonexistent-uid-12345\",\n\t\t\tRelations: []*apiv1.MemoRelation{},\n\t\t})\n\t\trequire.Error(t, err)\n\t\trequire.Contains(t, err.Error(), \"not found\")\n\t})\n}\n"
  },
  {
    "path": "server/router/api/v1/test/memo_service_test.go",
    "content": "package test\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"slices\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/require\"\n\t\"google.golang.org/protobuf/types/known/timestamppb\"\n\n\tapiv1 \"github.com/usememos/memos/proto/gen/api/v1\"\n)\n\nfunc TestListMemos(t *testing.T) {\n\tctx := context.Background()\n\n\tts := NewTestService(t)\n\tdefer ts.Cleanup()\n\n\t// Create userOne\n\tuserOne, err := ts.CreateRegularUser(ctx, \"test-user-1\")\n\trequire.NoError(t, err)\n\trequire.NotNil(t, userOne)\n\n\t// Create userOne context\n\tuserOneCtx := ts.CreateUserContext(ctx, userOne.ID)\n\n\t// Create userTwo\n\tuserTwo, err := ts.CreateRegularUser(ctx, \"test-user-2\")\n\trequire.NoError(t, err)\n\trequire.NotNil(t, userTwo)\n\n\t// Create userTwo context\n\tuserTwoCtx := ts.CreateUserContext(ctx, userTwo.ID)\n\n\t// Create attachmentOne by userOne\n\tattachmentOne, err := ts.Service.CreateAttachment(userOneCtx, &apiv1.CreateAttachmentRequest{\n\t\tAttachment: &apiv1.Attachment{\n\t\t\tName:     \"\",\n\t\t\tFilename: \"hello.txt\",\n\t\t\tSize:     5,\n\t\t\tType:     \"text/plain\",\n\t\t\tContent: []byte{\n\t\t\t\t104, 101, 108, 108, 111,\n\t\t\t},\n\t\t},\n\t})\n\n\trequire.NoError(t, err)\n\trequire.NotNil(t, attachmentOne)\n\n\t// Create attachmentTwo by userOne\n\tattachmentTwo, err := ts.Service.CreateAttachment(userOneCtx, &apiv1.CreateAttachmentRequest{\n\t\tAttachment: &apiv1.Attachment{\n\t\t\tName:     \"\",\n\t\t\tFilename: \"world.txt\",\n\t\t\tSize:     5,\n\t\t\tType:     \"text/plain\",\n\t\t\tContent: []byte{\n\t\t\t\t119, 111, 114, 108, 100,\n\t\t\t},\n\t\t},\n\t})\n\n\trequire.NoError(t, err)\n\trequire.NotNil(t, attachmentTwo)\n\n\t// Create memoOne with two attachments by userOne\n\tmemoOne, err := ts.Service.CreateMemo(userOneCtx, &apiv1.CreateMemoRequest{\n\t\tMemo: &apiv1.Memo{\n\t\t\tContent:    \"Hellooo, any words after this sentence won't be in the snippet. This is the next sentence. And I also have two attachments.\",\n\t\t\tVisibility: apiv1.Visibility_PROTECTED,\n\t\t\tAttachments: []*apiv1.Attachment{\n\t\t\t\t&apiv1.Attachment{\n\t\t\t\t\tName: attachmentOne.Name,\n\t\t\t\t},\n\t\t\t\t&apiv1.Attachment{\n\t\t\t\t\tName: attachmentTwo.Name,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t})\n\n\trequire.NoError(t, err)\n\trequire.NotNil(t, memoOne)\n\n\t// Create memoTwo by userTwo referencing memoOne\n\tmemoTwo, err := ts.Service.CreateMemo(userTwoCtx, &apiv1.CreateMemoRequest{\n\t\tMemo: &apiv1.Memo{\n\t\t\tContent:    \"This is a memo reminding you to check the attachment attached to memoOne. I have referenced the memo below.⬇️\",\n\t\t\tVisibility: apiv1.Visibility_PROTECTED,\n\t\t\tRelations: []*apiv1.MemoRelation{\n\t\t\t\t&apiv1.MemoRelation{\n\t\t\t\t\tRelatedMemo: &apiv1.MemoRelation_Memo{\n\t\t\t\t\t\tName: memoOne.Name,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t})\n\n\trequire.NoError(t, err)\n\trequire.NotNil(t, memoTwo)\n\n\t// Create memoThree by userOne\n\tmemoThree, err := ts.Service.CreateMemo(userOneCtx, &apiv1.CreateMemoRequest{\n\t\tMemo: &apiv1.Memo{\n\t\t\tContent:    \"This is a very popular memo. I have 2 reactions!\",\n\t\t\tVisibility: apiv1.Visibility_PROTECTED,\n\t\t},\n\t})\n\n\trequire.NoError(t, err)\n\trequire.NotNil(t, memoThree)\n\n\t// Create reaction from userOne on memoThree\n\treactionOne, err := ts.Service.UpsertMemoReaction(userOneCtx, &apiv1.UpsertMemoReactionRequest{\n\t\tName: memoThree.Name,\n\t\tReaction: &apiv1.Reaction{\n\t\t\tContentId:    memoThree.Name,\n\t\t\tReactionType: \"❤️\",\n\t\t},\n\t})\n\n\trequire.NoError(t, err)\n\trequire.NotNil(t, reactionOne)\n\n\t// Create reaction from userTwo on memoThree\n\treactionTwo, err := ts.Service.UpsertMemoReaction(userTwoCtx, &apiv1.UpsertMemoReactionRequest{\n\t\tName: memoThree.Name,\n\t\tReaction: &apiv1.Reaction{\n\t\t\tContentId:    memoThree.Name,\n\t\t\tReactionType: \"👍\",\n\t\t},\n\t})\n\n\trequire.NoError(t, err)\n\trequire.NotNil(t, reactionTwo)\n\n\tmemos, err := ts.Service.ListMemos(userOneCtx, &apiv1.ListMemosRequest{PageSize: 10})\n\n\trequire.NoError(t, err)\n\trequire.NotNil(t, memos)\n\trequire.Equal(t, 3, len(memos.Memos))\n\n\t// ///////////////\n\t// VERIFY MEMO ONE\n\t// ///////////////\n\tmemoOneResIdx := slices.IndexFunc(memos.Memos, func(m *apiv1.Memo) bool { return m.GetName() == memoOne.GetName() })\n\trequire.NotEqual(t, memoOneResIdx, -1)\n\n\tmemoOneRes := memos.Memos[memoOneResIdx]\n\trequire.NotNil(t, memoOneRes)\n\n\trequire.Equal(t, fmt.Sprintf(\"users/%d\", userOne.ID), memoOneRes.GetCreator())\n\trequire.Equal(t, apiv1.Visibility_PROTECTED, memoOneRes.GetVisibility())\n\trequire.Equal(t, memoOne.Content, memoOneRes.GetContent())\n\trequire.Equal(t, memoOne.Content[:64]+\"...\", memoOneRes.GetSnippet(), \"memoOne's content is snipped past the 64 char limit\")\n\trequire.Len(t, memoOneRes.Attachments, 2)\n\trequire.Len(t, memoOneRes.Relations, 1)\n\trequire.Empty(t, memoOneRes.Reactions)\n\n\t// verify memoOne's attachments\n\t// attachment one\n\tattachmentOneResIdx := slices.IndexFunc(memoOneRes.Attachments, func(a *apiv1.Attachment) bool { return a.GetName() == attachmentOne.GetName() })\n\trequire.NotEqual(t, attachmentOneResIdx, -1)\n\n\tattachmentOneRes := memoOneRes.Attachments[attachmentOneResIdx]\n\trequire.NotNil(t, attachmentOneRes)\n\n\trequire.Equal(t, attachmentOne.GetName(), attachmentOneRes.GetName())\n\trequire.Equal(t, attachmentOne.GetContent(), attachmentOneRes.GetContent())\n\n\t// attachment two\n\tattachmentTwoResIdx := slices.IndexFunc(memoOneRes.Attachments, func(a *apiv1.Attachment) bool { return a.GetName() == attachmentTwo.GetName() })\n\trequire.NotEqual(t, attachmentTwoResIdx, -1)\n\n\tattachmentTwoRes := memoOneRes.Attachments[attachmentTwoResIdx]\n\trequire.NotNil(t, attachmentTwoRes)\n\trequire.Equal(t, attachmentTwo.GetName(), attachmentTwoRes.GetName())\n\n\trequire.Equal(t, attachmentTwo.GetName(), attachmentTwoRes.GetName())\n\trequire.Equal(t, attachmentTwo.GetContent(), attachmentTwoRes.GetContent())\n\n\t// verify memoOne's relations\n\trequire.Len(t, memoOneRes.Relations, 1)\n\tmemoOneExpectedRelation := &apiv1.MemoRelation{\n\t\tMemo:        &apiv1.MemoRelation_Memo{Name: memoTwo.GetName()},\n\t\tRelatedMemo: &apiv1.MemoRelation_Memo{Name: memoOne.GetName()},\n\t}\n\trequire.Equal(t, memoOneExpectedRelation.Memo.GetName(), memoOneRes.Relations[0].Memo.GetName())\n\trequire.Equal(t, memoOneExpectedRelation.RelatedMemo.GetName(), memoOneRes.Relations[0].RelatedMemo.GetName())\n\n\t// ///////////////\n\t// VERIFY MEMO TWO\n\t// ///////////////\n\tmemoTwoResIdx := slices.IndexFunc(memos.Memos, func(m *apiv1.Memo) bool { return m.GetName() == memoTwo.GetName() })\n\trequire.NotEqual(t, memoTwoResIdx, -1)\n\n\tmemoTwoRes := memos.Memos[memoTwoResIdx]\n\trequire.NotNil(t, memoTwoRes)\n\n\trequire.Equal(t, fmt.Sprintf(\"users/%d\", userTwo.ID), memoTwoRes.GetCreator())\n\trequire.Equal(t, apiv1.Visibility_PROTECTED, memoTwoRes.GetVisibility())\n\trequire.Equal(t, memoTwo.Content, memoTwoRes.GetContent())\n\trequire.Empty(t, memoTwoRes.Attachments)\n\trequire.Len(t, memoTwoRes.Relations, 1)\n\trequire.Empty(t, memoTwoRes.Reactions)\n\n\t// verify memoTwo's relations\n\trequire.Len(t, memoTwoRes.Relations, 1)\n\tmemoTwoExpectedRelation := &apiv1.MemoRelation{\n\t\tMemo:        &apiv1.MemoRelation_Memo{Name: memoTwo.GetName()},\n\t\tRelatedMemo: &apiv1.MemoRelation_Memo{Name: memoOne.GetName()},\n\t}\n\trequire.Equal(t, memoTwoExpectedRelation.Memo.GetName(), memoTwoRes.Relations[0].Memo.GetName())\n\trequire.Equal(t, memoTwoExpectedRelation.RelatedMemo.GetName(), memoTwoRes.Relations[0].RelatedMemo.GetName())\n\n\t// ///////////////\n\t// VERIFY MEMO THREE\n\t// ///////////////\n\tmemoThreeResIdx := slices.IndexFunc(memos.Memos, func(m *apiv1.Memo) bool { return m.GetName() == memoThree.GetName() })\n\trequire.NotEqual(t, memoThreeResIdx, -1)\n\n\tmemoThreeRes := memos.Memos[memoThreeResIdx]\n\trequire.NotNil(t, memoThreeRes)\n\n\trequire.Equal(t, fmt.Sprintf(\"users/%d\", userOne.ID), memoThreeRes.GetCreator())\n\trequire.Equal(t, apiv1.Visibility_PROTECTED, memoThreeRes.GetVisibility())\n\trequire.Equal(t, memoThree.Content, memoThreeRes.GetContent())\n\trequire.Empty(t, memoThreeRes.Attachments)\n\trequire.Empty(t, memoThreeRes.Relations)\n\trequire.Len(t, memoThreeRes.Reactions, 2)\n\n\t// verify memoThree's reactions\n\trequire.Len(t, memoThreeRes.Reactions, 2)\n\t// userOne's reaction\n\tuserOneReactionIdx := slices.IndexFunc(memoThreeRes.Reactions, func(r *apiv1.Reaction) bool { return r.GetCreator() == fmt.Sprintf(\"users/%d\", userOne.ID) })\n\trequire.NotEqual(t, userOneReactionIdx, -1)\n\n\tuserOneReaction := memoThreeRes.Reactions[userOneReactionIdx]\n\trequire.NotNil(t, userOneReaction)\n\trequire.Equal(t, \"❤️\", userOneReaction.ReactionType)\n\n\t// userTwo's reaction\n\tuserTwoReactionIdx := slices.IndexFunc(memoThreeRes.Reactions, func(r *apiv1.Reaction) bool { return r.GetCreator() == fmt.Sprintf(\"users/%d\", userTwo.ID) })\n\trequire.NotEqual(t, userTwoReactionIdx, -1)\n\n\tuserTwoReaction := memoThreeRes.Reactions[userTwoReactionIdx]\n\trequire.NotNil(t, userTwoReaction)\n\trequire.Equal(t, \"👍\", userTwoReaction.ReactionType)\n}\n\n// TestCreateMemoWithCustomTimestamps tests that custom timestamps can be set when creating memos and comments.\n// This addresses issue #5483: https://github.com/usememos/memos/issues/5483\nfunc TestCreateMemoWithCustomTimestamps(t *testing.T) {\n\tctx := context.Background()\n\n\tts := NewTestService(t)\n\tdefer ts.Cleanup()\n\n\t// Create a test user\n\tuser, err := ts.CreateRegularUser(ctx, \"test-user-timestamps\")\n\trequire.NoError(t, err)\n\trequire.NotNil(t, user)\n\n\tuserCtx := ts.CreateUserContext(ctx, user.ID)\n\n\t// Define custom timestamps (January 1, 2020)\n\tcustomCreateTime := time.Date(2020, 1, 1, 12, 0, 0, 0, time.UTC)\n\tcustomUpdateTime := time.Date(2020, 1, 2, 12, 0, 0, 0, time.UTC)\n\tcustomDisplayTime := time.Date(2020, 1, 3, 12, 0, 0, 0, time.UTC)\n\n\t// Test 1: Create a memo with custom create_time\n\tmemoWithCreateTime, err := ts.Service.CreateMemo(userCtx, &apiv1.CreateMemoRequest{\n\t\tMemo: &apiv1.Memo{\n\t\t\tContent:    \"This memo has a custom creation time\",\n\t\t\tVisibility: apiv1.Visibility_PRIVATE,\n\t\t\tCreateTime: timestamppb.New(customCreateTime),\n\t\t},\n\t})\n\trequire.NoError(t, err)\n\trequire.NotNil(t, memoWithCreateTime)\n\trequire.Equal(t, customCreateTime.Unix(), memoWithCreateTime.CreateTime.AsTime().Unix(), \"create_time should match the custom timestamp\")\n\n\t// Test 2: Create a memo with custom update_time\n\tmemoWithUpdateTime, err := ts.Service.CreateMemo(userCtx, &apiv1.CreateMemoRequest{\n\t\tMemo: &apiv1.Memo{\n\t\t\tContent:    \"This memo has a custom update time\",\n\t\t\tVisibility: apiv1.Visibility_PRIVATE,\n\t\t\tUpdateTime: timestamppb.New(customUpdateTime),\n\t\t},\n\t})\n\trequire.NoError(t, err)\n\trequire.NotNil(t, memoWithUpdateTime)\n\trequire.Equal(t, customUpdateTime.Unix(), memoWithUpdateTime.UpdateTime.AsTime().Unix(), \"update_time should match the custom timestamp\")\n\n\t// Test 3: Create a memo with custom display_time\n\t// Note: display_time is computed from either created_ts or updated_ts based on instance setting\n\t// Since DisplayWithUpdateTime defaults to false, display_time maps to created_ts\n\tmemoWithDisplayTime, err := ts.Service.CreateMemo(userCtx, &apiv1.CreateMemoRequest{\n\t\tMemo: &apiv1.Memo{\n\t\t\tContent:     \"This memo has a custom display time\",\n\t\t\tVisibility:  apiv1.Visibility_PRIVATE,\n\t\t\tDisplayTime: timestamppb.New(customDisplayTime),\n\t\t},\n\t})\n\trequire.NoError(t, err)\n\trequire.NotNil(t, memoWithDisplayTime)\n\t// Since DisplayWithUpdateTime is false by default, display_time sets created_ts\n\trequire.Equal(t, customDisplayTime.Unix(), memoWithDisplayTime.DisplayTime.AsTime().Unix(), \"display_time should match the custom timestamp\")\n\trequire.Equal(t, customDisplayTime.Unix(), memoWithDisplayTime.CreateTime.AsTime().Unix(), \"create_time should also match since display_time maps to created_ts\")\n\n\t// Test 4: Create a memo with all custom timestamps\n\t// When both display_time and create_time are provided, create_time takes precedence\n\tmemoWithAllTimestamps, err := ts.Service.CreateMemo(userCtx, &apiv1.CreateMemoRequest{\n\t\tMemo: &apiv1.Memo{\n\t\t\tContent:     \"This memo has all custom timestamps\",\n\t\t\tVisibility:  apiv1.Visibility_PRIVATE,\n\t\t\tCreateTime:  timestamppb.New(customCreateTime),\n\t\t\tUpdateTime:  timestamppb.New(customUpdateTime),\n\t\t\tDisplayTime: timestamppb.New(customDisplayTime),\n\t\t},\n\t})\n\trequire.NoError(t, err)\n\trequire.NotNil(t, memoWithAllTimestamps)\n\trequire.Equal(t, customCreateTime.Unix(), memoWithAllTimestamps.CreateTime.AsTime().Unix(), \"create_time should match the custom timestamp\")\n\trequire.Equal(t, customUpdateTime.Unix(), memoWithAllTimestamps.UpdateTime.AsTime().Unix(), \"update_time should match the custom timestamp\")\n\t// display_time is computed from created_ts when DisplayWithUpdateTime is false\n\trequire.Equal(t, customCreateTime.Unix(), memoWithAllTimestamps.DisplayTime.AsTime().Unix(), \"display_time should be derived from create_time\")\n\n\t// Test 5: Create a comment (memo relation) with custom timestamps\n\tparentMemo, err := ts.Service.CreateMemo(userCtx, &apiv1.CreateMemoRequest{\n\t\tMemo: &apiv1.Memo{\n\t\t\tContent:    \"This is the parent memo\",\n\t\t\tVisibility: apiv1.Visibility_PRIVATE,\n\t\t},\n\t})\n\trequire.NoError(t, err)\n\trequire.NotNil(t, parentMemo)\n\n\tcustomCommentCreateTime := time.Date(2021, 6, 15, 10, 30, 0, 0, time.UTC)\n\tcomment, err := ts.Service.CreateMemoComment(userCtx, &apiv1.CreateMemoCommentRequest{\n\t\tName: parentMemo.Name,\n\t\tComment: &apiv1.Memo{\n\t\t\tContent:    \"This is a comment with custom create time\",\n\t\t\tVisibility: apiv1.Visibility_PRIVATE,\n\t\t\tCreateTime: timestamppb.New(customCommentCreateTime),\n\t\t},\n\t})\n\trequire.NoError(t, err)\n\trequire.NotNil(t, comment)\n\trequire.Equal(t, customCommentCreateTime.Unix(), comment.CreateTime.AsTime().Unix(), \"comment create_time should match the custom timestamp\")\n\n\t// Test 6: Verify that memos without custom timestamps still get auto-generated ones\n\tmemoWithoutTimestamps, err := ts.Service.CreateMemo(userCtx, &apiv1.CreateMemoRequest{\n\t\tMemo: &apiv1.Memo{\n\t\t\tContent:    \"This memo has auto-generated timestamps\",\n\t\t\tVisibility: apiv1.Visibility_PRIVATE,\n\t\t},\n\t})\n\trequire.NoError(t, err)\n\trequire.NotNil(t, memoWithoutTimestamps)\n\trequire.NotNil(t, memoWithoutTimestamps.CreateTime, \"create_time should be auto-generated\")\n\trequire.NotNil(t, memoWithoutTimestamps.UpdateTime, \"update_time should be auto-generated\")\n\trequire.True(t, time.Now().Unix()-memoWithoutTimestamps.CreateTime.AsTime().Unix() < 5, \"create_time should be recent (within 5 seconds)\")\n}\n"
  },
  {
    "path": "server/router/api/v1/test/memo_share_service_test.go",
    "content": "package test\n\nimport (\n\t\"context\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n\t\"google.golang.org/grpc/codes\"\n\t\"google.golang.org/grpc/status\"\n\n\tapiv1 \"github.com/usememos/memos/proto/gen/api/v1\"\n)\n\nfunc TestDeleteMemoShare_VerifiesShareBelongsToMemo(t *testing.T) {\n\tctx := context.Background()\n\n\tts := NewTestService(t)\n\tdefer ts.Cleanup()\n\n\tuserOne, err := ts.CreateRegularUser(ctx, \"share-owner-one\")\n\trequire.NoError(t, err)\n\tuserTwo, err := ts.CreateRegularUser(ctx, \"share-owner-two\")\n\trequire.NoError(t, err)\n\n\tuserOneCtx := ts.CreateUserContext(ctx, userOne.ID)\n\tuserTwoCtx := ts.CreateUserContext(ctx, userTwo.ID)\n\n\tmemoOne, err := ts.Service.CreateMemo(userOneCtx, &apiv1.CreateMemoRequest{\n\t\tMemo: &apiv1.Memo{\n\t\t\tContent:    \"memo one\",\n\t\t\tVisibility: apiv1.Visibility_PRIVATE,\n\t\t},\n\t})\n\trequire.NoError(t, err)\n\n\tmemoTwo, err := ts.Service.CreateMemo(userTwoCtx, &apiv1.CreateMemoRequest{\n\t\tMemo: &apiv1.Memo{\n\t\t\tContent:    \"memo two\",\n\t\t\tVisibility: apiv1.Visibility_PRIVATE,\n\t\t},\n\t})\n\trequire.NoError(t, err)\n\n\tshare, err := ts.Service.CreateMemoShare(userTwoCtx, &apiv1.CreateMemoShareRequest{\n\t\tParent:    memoTwo.Name,\n\t\tMemoShare: &apiv1.MemoShare{},\n\t})\n\trequire.NoError(t, err)\n\n\tshareToken := share.Name[strings.LastIndex(share.Name, \"/\")+1:]\n\tforgedName := memoOne.Name + \"/shares/\" + shareToken\n\n\t_, err = ts.Service.DeleteMemoShare(userOneCtx, &apiv1.DeleteMemoShareRequest{\n\t\tName: forgedName,\n\t})\n\trequire.Error(t, err)\n\trequire.Equal(t, codes.NotFound, status.Code(err))\n\n\tsharedMemo, err := ts.Service.GetMemoByShare(ctx, &apiv1.GetMemoByShareRequest{\n\t\tShareId: shareToken,\n\t})\n\trequire.NoError(t, err)\n\trequire.Equal(t, memoTwo.Name, sharedMemo.Name)\n}\n\nfunc TestGetMemoByShare_IncludesReactions(t *testing.T) {\n\tctx := context.Background()\n\n\tts := NewTestService(t)\n\tdefer ts.Cleanup()\n\n\tuser, err := ts.CreateRegularUser(ctx, \"share-reactions\")\n\trequire.NoError(t, err)\n\tuserCtx := ts.CreateUserContext(ctx, user.ID)\n\n\tmemo, err := ts.Service.CreateMemo(userCtx, &apiv1.CreateMemoRequest{\n\t\tMemo: &apiv1.Memo{\n\t\t\tContent:    \"memo with reactions\",\n\t\t\tVisibility: apiv1.Visibility_PRIVATE,\n\t\t},\n\t})\n\trequire.NoError(t, err)\n\n\treaction, err := ts.Service.UpsertMemoReaction(userCtx, &apiv1.UpsertMemoReactionRequest{\n\t\tName: memo.Name,\n\t\tReaction: &apiv1.Reaction{\n\t\t\tContentId:    memo.Name,\n\t\t\tReactionType: \"👍\",\n\t\t},\n\t})\n\trequire.NoError(t, err)\n\trequire.NotNil(t, reaction)\n\n\tshare, err := ts.Service.CreateMemoShare(userCtx, &apiv1.CreateMemoShareRequest{\n\t\tParent:    memo.Name,\n\t\tMemoShare: &apiv1.MemoShare{},\n\t})\n\trequire.NoError(t, err)\n\n\tshareToken := share.Name[strings.LastIndex(share.Name, \"/\")+1:]\n\tsharedMemo, err := ts.Service.GetMemoByShare(ctx, &apiv1.GetMemoByShareRequest{\n\t\tShareId: shareToken,\n\t})\n\trequire.NoError(t, err)\n\trequire.Len(t, sharedMemo.Reactions, 1)\n\trequire.Equal(t, \"👍\", sharedMemo.Reactions[0].ReactionType)\n\trequire.Equal(t, memo.Name, sharedMemo.Reactions[0].ContentId)\n}\n"
  },
  {
    "path": "server/router/api/v1/test/reaction_service_test.go",
    "content": "package test\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n\n\tapiv1 \"github.com/usememos/memos/proto/gen/api/v1\"\n)\n\nfunc TestDeleteMemoReaction(t *testing.T) {\n\tctx := context.Background()\n\n\tt.Run(\"DeleteMemoReaction success by reaction owner\", func(t *testing.T) {\n\t\tts := NewTestService(t)\n\t\tdefer ts.Cleanup()\n\n\t\t// Create user\n\t\tuser, err := ts.CreateRegularUser(ctx, \"user\")\n\t\trequire.NoError(t, err)\n\t\tuserCtx := ts.CreateUserContext(ctx, user.ID)\n\n\t\t// Create memo\n\t\tmemo, err := ts.Service.CreateMemo(userCtx, &apiv1.CreateMemoRequest{\n\t\t\tMemo: &apiv1.Memo{\n\t\t\t\tContent:    \"Test memo\",\n\t\t\t\tVisibility: apiv1.Visibility_PUBLIC,\n\t\t\t},\n\t\t})\n\t\trequire.NoError(t, err)\n\t\trequire.NotNil(t, memo)\n\n\t\t// Create reaction\n\t\treaction, err := ts.Service.UpsertMemoReaction(userCtx, &apiv1.UpsertMemoReactionRequest{\n\t\t\tName: memo.Name,\n\t\t\tReaction: &apiv1.Reaction{\n\t\t\t\tContentId:    memo.Name,\n\t\t\t\tReactionType: \"👍\",\n\t\t\t},\n\t\t})\n\t\trequire.NoError(t, err)\n\t\trequire.NotNil(t, reaction)\n\n\t\t// Delete reaction - should succeed\n\t\t_, err = ts.Service.DeleteMemoReaction(userCtx, &apiv1.DeleteMemoReactionRequest{\n\t\t\tName: reaction.Name,\n\t\t})\n\t\trequire.NoError(t, err)\n\t})\n\n\tt.Run(\"DeleteMemoReaction success by host user\", func(t *testing.T) {\n\t\tts := NewTestService(t)\n\t\tdefer ts.Cleanup()\n\n\t\t// Create regular user\n\t\tregularUser, err := ts.CreateRegularUser(ctx, \"user\")\n\t\trequire.NoError(t, err)\n\t\tregularUserCtx := ts.CreateUserContext(ctx, regularUser.ID)\n\n\t\t// Create host user\n\t\thostUser, err := ts.CreateHostUser(ctx, \"admin\")\n\t\trequire.NoError(t, err)\n\t\thostCtx := ts.CreateUserContext(ctx, hostUser.ID)\n\n\t\t// Create memo by regular user\n\t\tmemo, err := ts.Service.CreateMemo(regularUserCtx, &apiv1.CreateMemoRequest{\n\t\t\tMemo: &apiv1.Memo{\n\t\t\t\tContent:    \"Test memo\",\n\t\t\t\tVisibility: apiv1.Visibility_PUBLIC,\n\t\t\t},\n\t\t})\n\t\trequire.NoError(t, err)\n\t\trequire.NotNil(t, memo)\n\n\t\t// Create reaction by regular user\n\t\treaction, err := ts.Service.UpsertMemoReaction(regularUserCtx, &apiv1.UpsertMemoReactionRequest{\n\t\t\tName: memo.Name,\n\t\t\tReaction: &apiv1.Reaction{\n\t\t\t\tContentId:    memo.Name,\n\t\t\t\tReactionType: \"👍\",\n\t\t\t},\n\t\t})\n\t\trequire.NoError(t, err)\n\t\trequire.NotNil(t, reaction)\n\n\t\t// Host user can delete reaction - should succeed\n\t\t_, err = ts.Service.DeleteMemoReaction(hostCtx, &apiv1.DeleteMemoReactionRequest{\n\t\t\tName: reaction.Name,\n\t\t})\n\t\trequire.NoError(t, err)\n\t})\n\n\tt.Run(\"DeleteMemoReaction permission denied for non-owner\", func(t *testing.T) {\n\t\tts := NewTestService(t)\n\t\tdefer ts.Cleanup()\n\n\t\t// Create user1\n\t\tuser1, err := ts.CreateRegularUser(ctx, \"user1\")\n\t\trequire.NoError(t, err)\n\t\tuser1Ctx := ts.CreateUserContext(ctx, user1.ID)\n\n\t\t// Create user2\n\t\tuser2, err := ts.CreateRegularUser(ctx, \"user2\")\n\t\trequire.NoError(t, err)\n\t\tuser2Ctx := ts.CreateUserContext(ctx, user2.ID)\n\n\t\t// Create memo by user1\n\t\tmemo, err := ts.Service.CreateMemo(user1Ctx, &apiv1.CreateMemoRequest{\n\t\t\tMemo: &apiv1.Memo{\n\t\t\t\tContent:    \"Test memo\",\n\t\t\t\tVisibility: apiv1.Visibility_PUBLIC,\n\t\t\t},\n\t\t})\n\t\trequire.NoError(t, err)\n\t\trequire.NotNil(t, memo)\n\n\t\t// Create reaction by user1\n\t\treaction, err := ts.Service.UpsertMemoReaction(user1Ctx, &apiv1.UpsertMemoReactionRequest{\n\t\t\tName: memo.Name,\n\t\t\tReaction: &apiv1.Reaction{\n\t\t\t\tContentId:    memo.Name,\n\t\t\t\tReactionType: \"👍\",\n\t\t\t},\n\t\t})\n\t\trequire.NoError(t, err)\n\t\trequire.NotNil(t, reaction)\n\n\t\t// User2 tries to delete reaction - should fail with permission denied\n\t\t_, err = ts.Service.DeleteMemoReaction(user2Ctx, &apiv1.DeleteMemoReactionRequest{\n\t\t\tName: reaction.Name,\n\t\t})\n\t\trequire.Error(t, err)\n\t\trequire.Contains(t, err.Error(), \"permission denied\")\n\t})\n\n\tt.Run(\"DeleteMemoReaction unauthenticated\", func(t *testing.T) {\n\t\tts := NewTestService(t)\n\t\tdefer ts.Cleanup()\n\n\t\t// Create user\n\t\tuser, err := ts.CreateRegularUser(ctx, \"user\")\n\t\trequire.NoError(t, err)\n\t\tuserCtx := ts.CreateUserContext(ctx, user.ID)\n\n\t\t// Create memo\n\t\tmemo, err := ts.Service.CreateMemo(userCtx, &apiv1.CreateMemoRequest{\n\t\t\tMemo: &apiv1.Memo{\n\t\t\t\tContent:    \"Test memo\",\n\t\t\t\tVisibility: apiv1.Visibility_PUBLIC,\n\t\t\t},\n\t\t})\n\t\trequire.NoError(t, err)\n\t\trequire.NotNil(t, memo)\n\n\t\t// Create reaction\n\t\treaction, err := ts.Service.UpsertMemoReaction(userCtx, &apiv1.UpsertMemoReactionRequest{\n\t\t\tName: memo.Name,\n\t\t\tReaction: &apiv1.Reaction{\n\t\t\t\tContentId:    memo.Name,\n\t\t\t\tReactionType: \"👍\",\n\t\t\t},\n\t\t})\n\t\trequire.NoError(t, err)\n\t\trequire.NotNil(t, reaction)\n\n\t\t// Unauthenticated user tries to delete reaction - should fail\n\t\t_, err = ts.Service.DeleteMemoReaction(ctx, &apiv1.DeleteMemoReactionRequest{\n\t\t\tName: reaction.Name,\n\t\t})\n\t\trequire.Error(t, err)\n\t\trequire.Contains(t, err.Error(), \"not authenticated\")\n\t})\n\n\tt.Run(\"DeleteMemoReaction not found returns permission denied\", func(t *testing.T) {\n\t\tts := NewTestService(t)\n\t\tdefer ts.Cleanup()\n\n\t\t// Create user\n\t\tuser, err := ts.CreateRegularUser(ctx, \"user\")\n\t\trequire.NoError(t, err)\n\t\tuserCtx := ts.CreateUserContext(ctx, user.ID)\n\n\t\t// Try to delete non-existent reaction - should fail with permission denied\n\t\t// (not \"not found\" to avoid information disclosure)\n\t\t// Use new nested resource format: memos/{memo}/reactions/{reaction}\n\t\t_, err = ts.Service.DeleteMemoReaction(userCtx, &apiv1.DeleteMemoReactionRequest{\n\t\t\tName: \"memos/nonexistent/reactions/99999\",\n\t\t})\n\t\trequire.Error(t, err)\n\t\trequire.Contains(t, err.Error(), \"permission denied\")\n\t\trequire.NotContains(t, err.Error(), \"not found\")\n\t})\n}\n"
  },
  {
    "path": "server/router/api/v1/test/shortcut_service_test.go",
    "content": "package test\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n\t\"google.golang.org/protobuf/types/known/fieldmaskpb\"\n\n\tv1pb \"github.com/usememos/memos/proto/gen/api/v1\"\n)\n\nfunc TestListShortcuts(t *testing.T) {\n\tctx := context.Background()\n\n\tt.Run(\"ListShortcuts success\", func(t *testing.T) {\n\t\tts := NewTestService(t)\n\t\tdefer ts.Cleanup()\n\n\t\t// Create a user\n\t\tuser, err := ts.CreateRegularUser(ctx, \"testuser\")\n\t\trequire.NoError(t, err)\n\n\t\t// Set user context\n\t\tuserCtx := ts.CreateUserContext(ctx, user.ID)\n\n\t\t// List shortcuts (should be empty initially)\n\t\treq := &v1pb.ListShortcutsRequest{\n\t\t\tParent: fmt.Sprintf(\"users/%d\", user.ID),\n\t\t}\n\n\t\tresp, err := ts.Service.ListShortcuts(userCtx, req)\n\t\trequire.NoError(t, err)\n\t\trequire.NotNil(t, resp)\n\t\trequire.Empty(t, resp.Shortcuts)\n\t})\n\n\tt.Run(\"ListShortcuts permission denied for different user\", func(t *testing.T) {\n\t\tts := NewTestService(t)\n\t\tdefer ts.Cleanup()\n\n\t\t// Create two users\n\t\tuser1, err := ts.CreateRegularUser(ctx, \"user1\")\n\t\trequire.NoError(t, err)\n\t\tuser2, err := ts.CreateRegularUser(ctx, \"user2\")\n\t\trequire.NoError(t, err)\n\n\t\t// Set user1 context but try to list user2's shortcuts\n\t\tuserCtx := ts.CreateUserContext(ctx, user1.ID)\n\n\t\treq := &v1pb.ListShortcutsRequest{\n\t\t\tParent: fmt.Sprintf(\"users/%d\", user2.ID),\n\t\t}\n\n\t\t_, err = ts.Service.ListShortcuts(userCtx, req)\n\t\trequire.Error(t, err)\n\t\trequire.Contains(t, err.Error(), \"permission denied\")\n\t})\n\n\tt.Run(\"ListShortcuts invalid parent format\", func(t *testing.T) {\n\t\tts := NewTestService(t)\n\t\tdefer ts.Cleanup()\n\n\t\t// Create user\n\t\tuser, err := ts.CreateRegularUser(ctx, \"testuser\")\n\t\trequire.NoError(t, err)\n\n\t\t// Set user context\n\t\tuserCtx := ts.CreateUserContext(ctx, user.ID)\n\n\t\treq := &v1pb.ListShortcutsRequest{\n\t\t\tParent: \"invalid-parent-format\",\n\t\t}\n\n\t\t_, err = ts.Service.ListShortcuts(userCtx, req)\n\t\trequire.Error(t, err)\n\t\trequire.Contains(t, err.Error(), \"invalid user name\")\n\t})\n\n\tt.Run(\"ListShortcuts unauthenticated\", func(t *testing.T) {\n\t\tts := NewTestService(t)\n\t\tdefer ts.Cleanup()\n\n\t\treq := &v1pb.ListShortcutsRequest{\n\t\t\tParent: \"users/1\",\n\t\t}\n\n\t\t_, err := ts.Service.ListShortcuts(ctx, req)\n\t\trequire.Error(t, err)\n\t\trequire.Contains(t, err.Error(), \"permission denied\")\n\t})\n}\n\nfunc TestGetShortcut(t *testing.T) {\n\tctx := context.Background()\n\n\tt.Run(\"GetShortcut success\", func(t *testing.T) {\n\t\tts := NewTestService(t)\n\t\tdefer ts.Cleanup()\n\n\t\t// Create a user\n\t\tuser, err := ts.CreateRegularUser(ctx, \"testuser\")\n\t\trequire.NoError(t, err)\n\n\t\t// Set user context\n\t\tuserCtx := ts.CreateUserContext(ctx, user.ID)\n\n\t\t// First create a shortcut\n\t\tcreateReq := &v1pb.CreateShortcutRequest{\n\t\t\tParent: fmt.Sprintf(\"users/%d\", user.ID),\n\t\t\tShortcut: &v1pb.Shortcut{\n\t\t\t\tTitle:  \"Test Shortcut\",\n\t\t\t\tFilter: \"tag in [\\\"test\\\"]\",\n\t\t\t},\n\t\t}\n\n\t\tcreated, err := ts.Service.CreateShortcut(userCtx, createReq)\n\t\trequire.NoError(t, err)\n\n\t\t// Now get the shortcut\n\t\tgetReq := &v1pb.GetShortcutRequest{\n\t\t\tName: created.Name,\n\t\t}\n\n\t\tresp, err := ts.Service.GetShortcut(userCtx, getReq)\n\t\trequire.NoError(t, err)\n\t\trequire.NotNil(t, resp)\n\t\trequire.Equal(t, created.Name, resp.Name)\n\t\trequire.Equal(t, \"Test Shortcut\", resp.Title)\n\t\trequire.Equal(t, \"tag in [\\\"test\\\"]\", resp.Filter)\n\t})\n\n\tt.Run(\"GetShortcut permission denied for different user\", func(t *testing.T) {\n\t\tts := NewTestService(t)\n\t\tdefer ts.Cleanup()\n\n\t\t// Create two users\n\t\tuser1, err := ts.CreateRegularUser(ctx, \"user1\")\n\t\trequire.NoError(t, err)\n\t\tuser2, err := ts.CreateRegularUser(ctx, \"user2\")\n\t\trequire.NoError(t, err)\n\n\t\t// Create shortcut as user1\n\t\tuser1Ctx := ts.CreateUserContext(ctx, user1.ID)\n\t\tcreateReq := &v1pb.CreateShortcutRequest{\n\t\t\tParent: fmt.Sprintf(\"users/%d\", user1.ID),\n\t\t\tShortcut: &v1pb.Shortcut{\n\t\t\t\tTitle:  \"User1 Shortcut\",\n\t\t\t\tFilter: \"tag in [\\\"user1\\\"]\",\n\t\t\t},\n\t\t}\n\n\t\tcreated, err := ts.Service.CreateShortcut(user1Ctx, createReq)\n\t\trequire.NoError(t, err)\n\n\t\t// Try to get shortcut as user2\n\t\tuser2Ctx := ts.CreateUserContext(ctx, user2.ID)\n\t\tgetReq := &v1pb.GetShortcutRequest{\n\t\t\tName: created.Name,\n\t\t}\n\n\t\t_, err = ts.Service.GetShortcut(user2Ctx, getReq)\n\t\trequire.Error(t, err)\n\t\trequire.Contains(t, err.Error(), \"permission denied\")\n\t})\n\n\tt.Run(\"GetShortcut invalid name format\", func(t *testing.T) {\n\t\tts := NewTestService(t)\n\t\tdefer ts.Cleanup()\n\n\t\t// Create user\n\t\tuser, err := ts.CreateRegularUser(ctx, \"testuser\")\n\t\trequire.NoError(t, err)\n\n\t\t// Set user context\n\t\tuserCtx := ts.CreateUserContext(ctx, user.ID)\n\n\t\treq := &v1pb.GetShortcutRequest{\n\t\t\tName: \"invalid-shortcut-name\",\n\t\t}\n\n\t\t_, err = ts.Service.GetShortcut(userCtx, req)\n\t\trequire.Error(t, err)\n\t\trequire.Contains(t, err.Error(), \"invalid shortcut name\")\n\t})\n\n\tt.Run(\"GetShortcut not found\", func(t *testing.T) {\n\t\tts := NewTestService(t)\n\t\tdefer ts.Cleanup()\n\n\t\t// Create user\n\t\tuser, err := ts.CreateRegularUser(ctx, \"testuser\")\n\t\trequire.NoError(t, err)\n\n\t\t// Set user context\n\t\tuserCtx := ts.CreateUserContext(ctx, user.ID)\n\n\t\treq := &v1pb.GetShortcutRequest{\n\t\t\tName: fmt.Sprintf(\"users/%d\", user.ID) + \"/shortcuts/nonexistent\",\n\t\t}\n\n\t\t_, err = ts.Service.GetShortcut(userCtx, req)\n\t\trequire.Error(t, err)\n\t\trequire.Contains(t, err.Error(), \"not found\")\n\t})\n}\n\nfunc TestCreateShortcut(t *testing.T) {\n\tctx := context.Background()\n\n\tt.Run(\"CreateShortcut success\", func(t *testing.T) {\n\t\tts := NewTestService(t)\n\t\tdefer ts.Cleanup()\n\n\t\t// Create a user\n\t\tuser, err := ts.CreateRegularUser(ctx, \"testuser\")\n\t\trequire.NoError(t, err)\n\n\t\t// Set user context\n\t\tuserCtx := ts.CreateUserContext(ctx, user.ID)\n\n\t\treq := &v1pb.CreateShortcutRequest{\n\t\t\tParent: fmt.Sprintf(\"users/%d\", user.ID),\n\t\t\tShortcut: &v1pb.Shortcut{\n\t\t\t\tTitle:  \"My Shortcut\",\n\t\t\t\tFilter: \"tag in [\\\"important\\\"]\",\n\t\t\t},\n\t\t}\n\n\t\tresp, err := ts.Service.CreateShortcut(userCtx, req)\n\t\trequire.NoError(t, err)\n\t\trequire.NotNil(t, resp)\n\t\trequire.Equal(t, \"My Shortcut\", resp.Title)\n\t\trequire.Equal(t, \"tag in [\\\"important\\\"]\", resp.Filter)\n\t\trequire.Contains(t, resp.Name, fmt.Sprintf(\"users/%d/shortcuts/\", user.ID))\n\n\t\t// Verify the shortcut was created by listing\n\t\tlistReq := &v1pb.ListShortcutsRequest{\n\t\t\tParent: fmt.Sprintf(\"users/%d\", user.ID),\n\t\t}\n\n\t\tlistResp, err := ts.Service.ListShortcuts(userCtx, listReq)\n\t\trequire.NoError(t, err)\n\t\trequire.Len(t, listResp.Shortcuts, 1)\n\t\trequire.Equal(t, \"My Shortcut\", listResp.Shortcuts[0].Title)\n\t})\n\n\tt.Run(\"CreateShortcut permission denied for different user\", func(t *testing.T) {\n\t\tts := NewTestService(t)\n\t\tdefer ts.Cleanup()\n\n\t\t// Create two users\n\t\tuser1, err := ts.CreateRegularUser(ctx, \"user1\")\n\t\trequire.NoError(t, err)\n\t\tuser2, err := ts.CreateRegularUser(ctx, \"user2\")\n\t\trequire.NoError(t, err)\n\n\t\t// Set user1 context but try to create shortcut for user2\n\t\tuserCtx := ts.CreateUserContext(ctx, user1.ID)\n\n\t\treq := &v1pb.CreateShortcutRequest{\n\t\t\tParent: fmt.Sprintf(\"users/%d\", user2.ID),\n\t\t\tShortcut: &v1pb.Shortcut{\n\t\t\t\tTitle:  \"Forbidden Shortcut\",\n\t\t\t\tFilter: \"tag in [\\\"forbidden\\\"]\",\n\t\t\t},\n\t\t}\n\n\t\t_, err = ts.Service.CreateShortcut(userCtx, req)\n\t\trequire.Error(t, err)\n\t\trequire.Contains(t, err.Error(), \"permission denied\")\n\t})\n\n\tt.Run(\"CreateShortcut invalid parent format\", func(t *testing.T) {\n\t\tts := NewTestService(t)\n\t\tdefer ts.Cleanup()\n\n\t\t// Create user\n\t\tuser, err := ts.CreateRegularUser(ctx, \"testuser\")\n\t\trequire.NoError(t, err)\n\n\t\t// Set user context\n\t\tuserCtx := ts.CreateUserContext(ctx, user.ID)\n\n\t\treq := &v1pb.CreateShortcutRequest{\n\t\t\tParent: \"invalid-parent\",\n\t\t\tShortcut: &v1pb.Shortcut{\n\t\t\t\tTitle:  \"Test Shortcut\",\n\t\t\t\tFilter: \"tag in [\\\"test\\\"]\",\n\t\t\t},\n\t\t}\n\n\t\t_, err = ts.Service.CreateShortcut(userCtx, req)\n\t\trequire.Error(t, err)\n\t\trequire.Contains(t, err.Error(), \"invalid user name\")\n\t})\n\n\tt.Run(\"CreateShortcut invalid filter\", func(t *testing.T) {\n\t\tts := NewTestService(t)\n\t\tdefer ts.Cleanup()\n\n\t\t// Create user\n\t\tuser, err := ts.CreateRegularUser(ctx, \"testuser\")\n\t\trequire.NoError(t, err)\n\n\t\t// Set user context\n\t\tuserCtx := ts.CreateUserContext(ctx, user.ID)\n\n\t\treq := &v1pb.CreateShortcutRequest{\n\t\t\tParent: fmt.Sprintf(\"users/%d\", user.ID),\n\t\t\tShortcut: &v1pb.Shortcut{\n\t\t\t\tTitle:  \"Invalid Filter Shortcut\",\n\t\t\t\tFilter: \"invalid||filter))syntax\",\n\t\t\t},\n\t\t}\n\n\t\t_, err = ts.Service.CreateShortcut(userCtx, req)\n\t\trequire.Error(t, err)\n\t\trequire.Contains(t, err.Error(), \"invalid filter\")\n\t})\n\n\tt.Run(\"CreateShortcut missing title\", func(t *testing.T) {\n\t\tts := NewTestService(t)\n\t\tdefer ts.Cleanup()\n\n\t\t// Create user\n\t\tuser, err := ts.CreateRegularUser(ctx, \"testuser\")\n\t\trequire.NoError(t, err)\n\n\t\t// Set user context\n\t\tuserCtx := ts.CreateUserContext(ctx, user.ID)\n\n\t\treq := &v1pb.CreateShortcutRequest{\n\t\t\tParent: fmt.Sprintf(\"users/%d\", user.ID),\n\t\t\tShortcut: &v1pb.Shortcut{\n\t\t\t\tFilter: \"tag in [\\\"test\\\"]\",\n\t\t\t},\n\t\t}\n\n\t\t_, err = ts.Service.CreateShortcut(userCtx, req)\n\t\trequire.Error(t, err)\n\t\trequire.Contains(t, err.Error(), \"title is required\")\n\t})\n}\n\nfunc TestUpdateShortcut(t *testing.T) {\n\tctx := context.Background()\n\n\tt.Run(\"UpdateShortcut success\", func(t *testing.T) {\n\t\tts := NewTestService(t)\n\t\tdefer ts.Cleanup()\n\n\t\t// Create a user\n\t\tuser, err := ts.CreateRegularUser(ctx, \"testuser\")\n\t\trequire.NoError(t, err)\n\n\t\t// Set user context\n\t\tuserCtx := ts.CreateUserContext(ctx, user.ID)\n\n\t\t// Create a shortcut first\n\t\tcreateReq := &v1pb.CreateShortcutRequest{\n\t\t\tParent: fmt.Sprintf(\"users/%d\", user.ID),\n\t\t\tShortcut: &v1pb.Shortcut{\n\t\t\t\tTitle:  \"Original Title\",\n\t\t\t\tFilter: \"tag in [\\\"original\\\"]\",\n\t\t\t},\n\t\t}\n\n\t\tcreated, err := ts.Service.CreateShortcut(userCtx, createReq)\n\t\trequire.NoError(t, err)\n\n\t\t// Update the shortcut\n\t\tupdateReq := &v1pb.UpdateShortcutRequest{\n\t\t\tShortcut: &v1pb.Shortcut{\n\t\t\t\tName:   created.Name,\n\t\t\t\tTitle:  \"Updated Title\",\n\t\t\t\tFilter: \"tag in [\\\"updated\\\"]\",\n\t\t\t},\n\t\t\tUpdateMask: &fieldmaskpb.FieldMask{\n\t\t\t\tPaths: []string{\"title\", \"filter\"},\n\t\t\t},\n\t\t}\n\n\t\tupdated, err := ts.Service.UpdateShortcut(userCtx, updateReq)\n\t\trequire.NoError(t, err)\n\t\trequire.NotNil(t, updated)\n\t\trequire.Equal(t, \"Updated Title\", updated.Title)\n\t\trequire.Equal(t, \"tag in [\\\"updated\\\"]\", updated.Filter)\n\t\trequire.Equal(t, created.Name, updated.Name)\n\t})\n\n\tt.Run(\"UpdateShortcut permission denied for different user\", func(t *testing.T) {\n\t\tts := NewTestService(t)\n\t\tdefer ts.Cleanup()\n\n\t\t// Create two users\n\t\tuser1, err := ts.CreateRegularUser(ctx, \"user1\")\n\t\trequire.NoError(t, err)\n\t\tuser2, err := ts.CreateRegularUser(ctx, \"user2\")\n\t\trequire.NoError(t, err)\n\n\t\t// Create shortcut as user1\n\t\tuser1Ctx := ts.CreateUserContext(ctx, user1.ID)\n\t\tcreateReq := &v1pb.CreateShortcutRequest{\n\t\t\tParent: fmt.Sprintf(\"users/%d\", user1.ID),\n\t\t\tShortcut: &v1pb.Shortcut{\n\t\t\t\tTitle:  \"User1 Shortcut\",\n\t\t\t\tFilter: \"tag in [\\\"user1\\\"]\",\n\t\t\t},\n\t\t}\n\n\t\tcreated, err := ts.Service.CreateShortcut(user1Ctx, createReq)\n\t\trequire.NoError(t, err)\n\n\t\t// Try to update shortcut as user2\n\t\tuser2Ctx := ts.CreateUserContext(ctx, user2.ID)\n\t\tupdateReq := &v1pb.UpdateShortcutRequest{\n\t\t\tShortcut: &v1pb.Shortcut{\n\t\t\t\tName:   created.Name,\n\t\t\t\tTitle:  \"Hacked Title\",\n\t\t\t\tFilter: \"tag in [\\\"hacked\\\"]\",\n\t\t\t},\n\t\t\tUpdateMask: &fieldmaskpb.FieldMask{\n\t\t\t\tPaths: []string{\"title\", \"filter\"},\n\t\t\t},\n\t\t}\n\n\t\t_, err = ts.Service.UpdateShortcut(user2Ctx, updateReq)\n\t\trequire.Error(t, err)\n\t\trequire.Contains(t, err.Error(), \"permission denied\")\n\t})\n\n\tt.Run(\"UpdateShortcut missing update mask\", func(t *testing.T) {\n\t\tts := NewTestService(t)\n\t\tdefer ts.Cleanup()\n\n\t\t// Create a user and context for authentication\n\t\tuser, err := ts.CreateRegularUser(ctx, \"testuser\")\n\t\trequire.NoError(t, err)\n\t\tuserCtx := ts.CreateUserContext(ctx, user.ID)\n\n\t\treq := &v1pb.UpdateShortcutRequest{\n\t\t\tShortcut: &v1pb.Shortcut{\n\t\t\t\tName:  fmt.Sprintf(\"users/%d/shortcuts/test\", user.ID),\n\t\t\t\tTitle: \"Updated Title\",\n\t\t\t},\n\t\t}\n\n\t\t_, err = ts.Service.UpdateShortcut(userCtx, req)\n\t\trequire.Error(t, err)\n\t\trequire.Contains(t, err.Error(), \"update mask is required\")\n\t})\n\n\tt.Run(\"UpdateShortcut invalid name format\", func(t *testing.T) {\n\t\tts := NewTestService(t)\n\t\tdefer ts.Cleanup()\n\n\t\treq := &v1pb.UpdateShortcutRequest{\n\t\t\tShortcut: &v1pb.Shortcut{\n\t\t\t\tName:  \"invalid-shortcut-name\",\n\t\t\t\tTitle: \"Updated Title\",\n\t\t\t},\n\t\t\tUpdateMask: &fieldmaskpb.FieldMask{\n\t\t\t\tPaths: []string{\"title\"},\n\t\t\t},\n\t\t}\n\n\t\t_, err := ts.Service.UpdateShortcut(ctx, req)\n\t\trequire.Error(t, err)\n\t\trequire.Contains(t, err.Error(), \"invalid shortcut name\")\n\t})\n\n\tt.Run(\"UpdateShortcut invalid filter\", func(t *testing.T) {\n\t\tts := NewTestService(t)\n\t\tdefer ts.Cleanup()\n\n\t\t// Create user\n\t\tuser, err := ts.CreateRegularUser(ctx, \"testuser\")\n\t\trequire.NoError(t, err)\n\n\t\t// Set user context\n\t\tuserCtx := ts.CreateUserContext(ctx, user.ID)\n\n\t\t// Create a shortcut first\n\t\tcreateReq := &v1pb.CreateShortcutRequest{\n\t\t\tParent: fmt.Sprintf(\"users/%d\", user.ID),\n\t\t\tShortcut: &v1pb.Shortcut{\n\t\t\t\tTitle:  \"Test Shortcut\",\n\t\t\t\tFilter: \"tag in [\\\"test\\\"]\",\n\t\t\t},\n\t\t}\n\n\t\tcreated, err := ts.Service.CreateShortcut(userCtx, createReq)\n\t\trequire.NoError(t, err)\n\n\t\t// Try to update with invalid filter\n\t\tupdateReq := &v1pb.UpdateShortcutRequest{\n\t\t\tShortcut: &v1pb.Shortcut{\n\t\t\t\tName:   created.Name,\n\t\t\t\tFilter: \"invalid||filter))syntax\",\n\t\t\t},\n\t\t\tUpdateMask: &fieldmaskpb.FieldMask{\n\t\t\t\tPaths: []string{\"filter\"},\n\t\t\t},\n\t\t}\n\n\t\t_, err = ts.Service.UpdateShortcut(userCtx, updateReq)\n\t\trequire.Error(t, err)\n\t\trequire.Contains(t, err.Error(), \"invalid filter\")\n\t})\n}\n\nfunc TestDeleteShortcut(t *testing.T) {\n\tctx := context.Background()\n\n\tt.Run(\"DeleteShortcut success\", func(t *testing.T) {\n\t\tts := NewTestService(t)\n\t\tdefer ts.Cleanup()\n\n\t\t// Create a user\n\t\tuser, err := ts.CreateRegularUser(ctx, \"testuser\")\n\t\trequire.NoError(t, err)\n\n\t\t// Set user context\n\t\tuserCtx := ts.CreateUserContext(ctx, user.ID)\n\n\t\t// Create a shortcut first\n\t\tcreateReq := &v1pb.CreateShortcutRequest{\n\t\t\tParent: fmt.Sprintf(\"users/%d\", user.ID),\n\t\t\tShortcut: &v1pb.Shortcut{\n\t\t\t\tTitle:  \"Shortcut to Delete\",\n\t\t\t\tFilter: \"tag in [\\\"delete\\\"]\",\n\t\t\t},\n\t\t}\n\n\t\tcreated, err := ts.Service.CreateShortcut(userCtx, createReq)\n\t\trequire.NoError(t, err)\n\n\t\t// Delete the shortcut\n\t\tdeleteReq := &v1pb.DeleteShortcutRequest{\n\t\t\tName: created.Name,\n\t\t}\n\n\t\t_, err = ts.Service.DeleteShortcut(userCtx, deleteReq)\n\t\trequire.NoError(t, err)\n\n\t\t// Verify deletion by listing shortcuts\n\t\tlistReq := &v1pb.ListShortcutsRequest{\n\t\t\tParent: fmt.Sprintf(\"users/%d\", user.ID),\n\t\t}\n\n\t\tlistResp, err := ts.Service.ListShortcuts(userCtx, listReq)\n\t\trequire.NoError(t, err)\n\t\trequire.Empty(t, listResp.Shortcuts)\n\n\t\t// Also verify by trying to get the deleted shortcut\n\t\tgetReq := &v1pb.GetShortcutRequest{\n\t\t\tName: created.Name,\n\t\t}\n\n\t\t_, err = ts.Service.GetShortcut(userCtx, getReq)\n\t\trequire.Error(t, err)\n\t\trequire.Contains(t, err.Error(), \"not found\")\n\t})\n\n\tt.Run(\"DeleteShortcut permission denied for different user\", func(t *testing.T) {\n\t\tts := NewTestService(t)\n\t\tdefer ts.Cleanup()\n\n\t\t// Create two users\n\t\tuser1, err := ts.CreateRegularUser(ctx, \"user1\")\n\t\trequire.NoError(t, err)\n\t\tuser2, err := ts.CreateRegularUser(ctx, \"user2\")\n\t\trequire.NoError(t, err)\n\n\t\t// Create shortcut as user1\n\t\tuser1Ctx := ts.CreateUserContext(ctx, user1.ID)\n\t\tcreateReq := &v1pb.CreateShortcutRequest{\n\t\t\tParent: fmt.Sprintf(\"users/%d\", user1.ID),\n\t\t\tShortcut: &v1pb.Shortcut{\n\t\t\t\tTitle:  \"User1 Shortcut\",\n\t\t\t\tFilter: \"tag in [\\\"user1\\\"]\",\n\t\t\t},\n\t\t}\n\n\t\tcreated, err := ts.Service.CreateShortcut(user1Ctx, createReq)\n\t\trequire.NoError(t, err)\n\n\t\t// Try to delete shortcut as user2\n\t\tuser2Ctx := ts.CreateUserContext(ctx, user2.ID)\n\t\tdeleteReq := &v1pb.DeleteShortcutRequest{\n\t\t\tName: created.Name,\n\t\t}\n\n\t\t_, err = ts.Service.DeleteShortcut(user2Ctx, deleteReq)\n\t\trequire.Error(t, err)\n\t\trequire.Contains(t, err.Error(), \"permission denied\")\n\t})\n\n\tt.Run(\"DeleteShortcut invalid name format\", func(t *testing.T) {\n\t\tts := NewTestService(t)\n\t\tdefer ts.Cleanup()\n\n\t\treq := &v1pb.DeleteShortcutRequest{\n\t\t\tName: \"invalid-shortcut-name\",\n\t\t}\n\n\t\t_, err := ts.Service.DeleteShortcut(ctx, req)\n\t\trequire.Error(t, err)\n\t\trequire.Contains(t, err.Error(), \"invalid shortcut name\")\n\t})\n\n\tt.Run(\"DeleteShortcut not found\", func(t *testing.T) {\n\t\tts := NewTestService(t)\n\t\tdefer ts.Cleanup()\n\n\t\t// Create user\n\t\tuser, err := ts.CreateRegularUser(ctx, \"testuser\")\n\t\trequire.NoError(t, err)\n\n\t\t// Set user context\n\t\tuserCtx := ts.CreateUserContext(ctx, user.ID)\n\n\t\treq := &v1pb.DeleteShortcutRequest{\n\t\t\tName: fmt.Sprintf(\"users/%d\", user.ID) + \"/shortcuts/nonexistent\",\n\t\t}\n\n\t\t_, err = ts.Service.DeleteShortcut(userCtx, req)\n\t\trequire.Error(t, err)\n\t\trequire.Contains(t, err.Error(), \"not found\")\n\t})\n}\n\nfunc TestShortcutFiltering(t *testing.T) {\n\tctx := context.Background()\n\n\tt.Run(\"CreateShortcut with valid filters\", func(t *testing.T) {\n\t\tts := NewTestService(t)\n\t\tdefer ts.Cleanup()\n\n\t\t// Create user\n\t\tuser, err := ts.CreateRegularUser(ctx, \"testuser\")\n\t\trequire.NoError(t, err)\n\n\t\t// Set user context\n\t\tuserCtx := ts.CreateUserContext(ctx, user.ID)\n\n\t\t// Test various valid filter formats\n\t\tvalidFilters := []string{\n\t\t\t\"tag in [\\\"work\\\"]\",\n\t\t\t\"content.contains(\\\"meeting\\\")\",\n\t\t\t\"tag in [\\\"work\\\"] && content.contains(\\\"meeting\\\")\",\n\t\t\t\"tag in [\\\"work\\\"] || tag in [\\\"personal\\\"]\",\n\t\t\t\"creator_id == 1\",\n\t\t\t\"visibility == \\\"PUBLIC\\\"\",\n\t\t\t\"has_task_list == true\",\n\t\t\t\"has_task_list == false\",\n\t\t}\n\n\t\tfor i, filter := range validFilters {\n\t\t\treq := &v1pb.CreateShortcutRequest{\n\t\t\t\tParent: fmt.Sprintf(\"users/%d\", user.ID),\n\t\t\t\tShortcut: &v1pb.Shortcut{\n\t\t\t\t\tTitle:  \"Valid Filter \" + string(rune(i)),\n\t\t\t\t\tFilter: filter,\n\t\t\t\t},\n\t\t\t}\n\n\t\t\t_, err = ts.Service.CreateShortcut(userCtx, req)\n\t\t\trequire.NoError(t, err, \"Filter should be valid: %s\", filter)\n\t\t}\n\t})\n\n\tt.Run(\"CreateShortcut with invalid filters\", func(t *testing.T) {\n\t\tts := NewTestService(t)\n\t\tdefer ts.Cleanup()\n\n\t\t// Create user\n\t\tuser, err := ts.CreateRegularUser(ctx, \"testuser\")\n\t\trequire.NoError(t, err)\n\n\t\t// Set user context\n\t\tuserCtx := ts.CreateUserContext(ctx, user.ID)\n\n\t\t// Test various invalid filter formats\n\t\tinvalidFilters := []string{\n\t\t\t\"tag in \",                                   // incomplete expression\n\t\t\t\"invalid_field @in [\\\"value\\\"]\",             // unknown field\n\t\t\t\"tag in [\\\"work\\\"] &&\",                      // incomplete expression\n\t\t\t\"tag in [\\\"work\\\"] || || tag in [\\\"test\\\"]\", // double operator\n\t\t\t\"((tag in [\\\"work\\\"]\",                       // unmatched parentheses\n\t\t\t\"tag in [\\\"work\\\"] && )\",                    // mismatched parentheses\n\t\t\t\"tag == \\\"work\\\"\",                           // wrong operator (== not supported for tags)\n\t\t\t\"tag in work\",                               // missing brackets\n\t\t}\n\n\t\tfor _, filter := range invalidFilters {\n\t\t\treq := &v1pb.CreateShortcutRequest{\n\t\t\t\tParent: fmt.Sprintf(\"users/%d\", user.ID),\n\t\t\t\tShortcut: &v1pb.Shortcut{\n\t\t\t\t\tTitle:  \"Invalid Filter Test\",\n\t\t\t\t\tFilter: filter,\n\t\t\t\t},\n\t\t\t}\n\n\t\t\t_, err = ts.Service.CreateShortcut(userCtx, req)\n\t\t\trequire.Error(t, err, \"Filter should be invalid: %s\", filter)\n\t\t\trequire.Contains(t, err.Error(), \"invalid filter\", \"Error should mention invalid filter for: %s\", filter)\n\t\t}\n\t})\n}\n\nfunc TestShortcutCRUDComplete(t *testing.T) {\n\tctx := context.Background()\n\n\tt.Run(\"Complete CRUD lifecycle\", func(t *testing.T) {\n\t\tts := NewTestService(t)\n\t\tdefer ts.Cleanup()\n\n\t\t// Create user\n\t\tuser, err := ts.CreateRegularUser(ctx, \"testuser\")\n\t\trequire.NoError(t, err)\n\n\t\t// Set user context\n\t\tuserCtx := ts.CreateUserContext(ctx, user.ID)\n\n\t\t// 1. Create multiple shortcuts\n\t\tshortcut1Req := &v1pb.CreateShortcutRequest{\n\t\t\tParent: fmt.Sprintf(\"users/%d\", user.ID),\n\t\t\tShortcut: &v1pb.Shortcut{\n\t\t\t\tTitle:  \"Work Notes\",\n\t\t\t\tFilter: \"tag in [\\\"work\\\"]\",\n\t\t\t},\n\t\t}\n\n\t\tshortcut2Req := &v1pb.CreateShortcutRequest{\n\t\t\tParent: fmt.Sprintf(\"users/%d\", user.ID),\n\t\t\tShortcut: &v1pb.Shortcut{\n\t\t\t\tTitle:  \"Personal Notes\",\n\t\t\t\tFilter: \"tag in [\\\"personal\\\"]\",\n\t\t\t},\n\t\t}\n\n\t\tcreated1, err := ts.Service.CreateShortcut(userCtx, shortcut1Req)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, \"Work Notes\", created1.Title)\n\n\t\tcreated2, err := ts.Service.CreateShortcut(userCtx, shortcut2Req)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, \"Personal Notes\", created2.Title)\n\n\t\t// 2. List shortcuts and verify both exist\n\t\tlistReq := &v1pb.ListShortcutsRequest{\n\t\t\tParent: fmt.Sprintf(\"users/%d\", user.ID),\n\t\t}\n\n\t\tlistResp, err := ts.Service.ListShortcuts(userCtx, listReq)\n\t\trequire.NoError(t, err)\n\t\trequire.Len(t, listResp.Shortcuts, 2)\n\n\t\t// 3. Get individual shortcuts\n\t\tgetReq1 := &v1pb.GetShortcutRequest{Name: created1.Name}\n\t\tgetResp1, err := ts.Service.GetShortcut(userCtx, getReq1)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, created1.Name, getResp1.Name)\n\t\trequire.Equal(t, \"Work Notes\", getResp1.Title)\n\n\t\tgetReq2 := &v1pb.GetShortcutRequest{Name: created2.Name}\n\t\tgetResp2, err := ts.Service.GetShortcut(userCtx, getReq2)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, created2.Name, getResp2.Name)\n\t\trequire.Equal(t, \"Personal Notes\", getResp2.Title)\n\n\t\t// 4. Update one shortcut\n\t\tupdateReq := &v1pb.UpdateShortcutRequest{\n\t\t\tShortcut: &v1pb.Shortcut{\n\t\t\t\tName:   created1.Name,\n\t\t\t\tTitle:  \"Work & Meeting Notes\",\n\t\t\t\tFilter: \"tag in [\\\"work\\\"] || tag in [\\\"meeting\\\"]\",\n\t\t\t},\n\t\t\tUpdateMask: &fieldmaskpb.FieldMask{\n\t\t\t\tPaths: []string{\"title\", \"filter\"},\n\t\t\t},\n\t\t}\n\n\t\tupdated, err := ts.Service.UpdateShortcut(userCtx, updateReq)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, \"Work & Meeting Notes\", updated.Title)\n\t\trequire.Equal(t, \"tag in [\\\"work\\\"] || tag in [\\\"meeting\\\"]\", updated.Filter)\n\n\t\t// 5. Verify update by getting it again\n\t\tgetUpdatedReq := &v1pb.GetShortcutRequest{Name: created1.Name}\n\t\tgetUpdatedResp, err := ts.Service.GetShortcut(userCtx, getUpdatedReq)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, \"Work & Meeting Notes\", getUpdatedResp.Title)\n\t\trequire.Equal(t, \"tag in [\\\"work\\\"] || tag in [\\\"meeting\\\"]\", getUpdatedResp.Filter)\n\n\t\t// 6. Delete one shortcut\n\t\tdeleteReq := &v1pb.DeleteShortcutRequest{\n\t\t\tName: created2.Name,\n\t\t}\n\n\t\t_, err = ts.Service.DeleteShortcut(userCtx, deleteReq)\n\t\trequire.NoError(t, err)\n\n\t\t// 7. Verify deletion by listing (should only have 1 left)\n\t\tfinalListResp, err := ts.Service.ListShortcuts(userCtx, listReq)\n\t\trequire.NoError(t, err)\n\t\trequire.Len(t, finalListResp.Shortcuts, 1)\n\t\trequire.Equal(t, \"Work & Meeting Notes\", finalListResp.Shortcuts[0].Title)\n\n\t\t// 8. Verify deleted shortcut can't be accessed\n\t\tgetDeletedReq := &v1pb.GetShortcutRequest{Name: created2.Name}\n\t\t_, err = ts.Service.GetShortcut(userCtx, getDeletedReq)\n\t\trequire.Error(t, err)\n\t\trequire.Contains(t, err.Error(), \"not found\")\n\t})\n}\n"
  },
  {
    "path": "server/router/api/v1/test/sse_handler_test.go",
    "content": "package test\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/labstack/echo/v5\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/usememos/memos/server/auth\"\n\tapiv1 \"github.com/usememos/memos/server/router/api/v1\"\n)\n\nfunc TestSSEHandler_Authentication(t *testing.T) {\n\tctx := context.Background()\n\tts := NewTestService(t)\n\tdefer ts.Cleanup()\n\n\tuser, err := ts.CreateRegularUser(ctx, \"sse-user\")\n\trequire.NoError(t, err)\n\n\ttoken, _, err := auth.GenerateAccessTokenV2(\n\t\tuser.ID,\n\t\tuser.Username,\n\t\tstring(user.Role),\n\t\tstring(user.RowStatus),\n\t\t[]byte(ts.Secret),\n\t)\n\trequire.NoError(t, err)\n\n\te := echo.New()\n\tapiv1.RegisterSSERoutes(e, ts.Service.SSEHub, ts.Store, ts.Secret)\n\n\tt.Run(\"no token returns 401\", func(t *testing.T) {\n\t\treq := httptest.NewRequest(http.MethodGet, \"/api/v1/sse\", nil)\n\t\trec := httptest.NewRecorder()\n\t\te.ServeHTTP(rec, req)\n\t\trequire.Equal(t, http.StatusUnauthorized, rec.Code)\n\t})\n\n\tt.Run(\"invalid token returns 401\", func(t *testing.T) {\n\t\treq := httptest.NewRequest(http.MethodGet, \"/api/v1/sse\", nil)\n\t\treq.Header.Set(\"Authorization\", \"Bearer invalid-token\")\n\t\trec := httptest.NewRecorder()\n\t\te.ServeHTTP(rec, req)\n\t\trequire.Equal(t, http.StatusUnauthorized, rec.Code)\n\t})\n\n\tt.Run(\"valid token returns 200 and stream\", func(t *testing.T) {\n\t\t// Use a cancellable context so we can close the SSE connection after\n\t\t// confirming the headers, preventing the handler's event loop from\n\t\t// blocking the test indefinitely.\n\t\treqCtx, cancel := context.WithCancel(context.Background())\n\t\tdefer cancel()\n\t\treq := httptest.NewRequest(http.MethodGet, \"/api/v1/sse\", nil).WithContext(reqCtx)\n\t\treq.Header.Set(\"Authorization\", \"Bearer \"+token)\n\t\trec := httptest.NewRecorder()\n\t\tdone := make(chan struct{})\n\t\tgo func() {\n\t\t\tdefer close(done)\n\t\t\te.ServeHTTP(rec, req)\n\t\t}()\n\t\t// Cancel the context to signal client disconnect, which exits the SSE loop.\n\t\tcancel()\n\t\t<-done\n\t\trequire.Equal(t, http.StatusOK, rec.Code)\n\t\trequire.Equal(t, \"text/event-stream\", rec.Header().Get(\"Content-Type\"))\n\t})\n\n\tt.Run(\"token in query param returns 401\", func(t *testing.T) {\n\t\treq := httptest.NewRequest(http.MethodGet, \"/api/v1/sse?token=\"+token, nil)\n\t\trec := httptest.NewRecorder()\n\t\te.ServeHTTP(rec, req)\n\t\trequire.Equal(t, http.StatusUnauthorized, rec.Code)\n\t})\n}\n"
  },
  {
    "path": "server/router/api/v1/test/test_helper.go",
    "content": "package test\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"github.com/usememos/memos/internal/profile\"\n\t\"github.com/usememos/memos/plugin/markdown\"\n\t\"github.com/usememos/memos/server/auth\"\n\tapiv1 \"github.com/usememos/memos/server/router/api/v1\"\n\t\"github.com/usememos/memos/store\"\n\tteststore \"github.com/usememos/memos/store/test\"\n)\n\n// TestService holds the test service setup for API v1 services.\ntype TestService struct {\n\tService *apiv1.APIV1Service\n\tStore   *store.Store\n\tProfile *profile.Profile\n\tSecret  string\n}\n\n// NewTestService creates a new test service with SQLite database.\nfunc NewTestService(t *testing.T) *TestService {\n\tctx := context.Background()\n\n\t// Create a test store with SQLite\n\ttestStore := teststore.NewTestingStore(ctx, t)\n\n\t// Create a test profile with a temp directory for file storage,\n\t// so tests that create attachments don't leave artifacts in the source tree.\n\ttestProfile := &profile.Profile{\n\t\tDemo:        true,\n\t\tVersion:     \"test-1.0.0\",\n\t\tInstanceURL: \"http://localhost:8080\",\n\t\tDriver:      \"sqlite\",\n\t\tDSN:         \":memory:\",\n\t\tData:        t.TempDir(),\n\t}\n\n\t// Create APIV1Service with nil grpcServer since we're testing direct calls\n\tsecret := \"test-secret\"\n\tmarkdownService := markdown.NewService(\n\t\tmarkdown.WithTagExtension(),\n\t)\n\tservice := &apiv1.APIV1Service{\n\t\tSecret:          secret,\n\t\tProfile:         testProfile,\n\t\tStore:           testStore,\n\t\tMarkdownService: markdownService,\n\t\tSSEHub:          apiv1.NewSSEHub(),\n\t}\n\n\treturn &TestService{\n\t\tService: service,\n\t\tStore:   testStore,\n\t\tProfile: testProfile,\n\t\tSecret:  secret,\n\t}\n}\n\n// Cleanup closes resources after test.\nfunc (ts *TestService) Cleanup() {\n\tts.Store.Close()\n}\n\n// CreateHostUser creates an admin user for testing.\nfunc (ts *TestService) CreateHostUser(ctx context.Context, username string) (*store.User, error) {\n\treturn ts.Store.CreateUser(ctx, &store.User{\n\t\tUsername: username,\n\t\tRole:     store.RoleAdmin,\n\t\tEmail:    username + \"@example.com\",\n\t})\n}\n\n// CreateRegularUser creates a regular user for testing.\nfunc (ts *TestService) CreateRegularUser(ctx context.Context, username string) (*store.User, error) {\n\treturn ts.Store.CreateUser(ctx, &store.User{\n\t\tUsername: username,\n\t\tRole:     store.RoleUser,\n\t\tEmail:    username + \"@example.com\",\n\t})\n}\n\n// CreateUserContext creates a context with the given user's ID for authentication.\nfunc (*TestService) CreateUserContext(ctx context.Context, userID int32) context.Context {\n\t// Use the context key from the auth package\n\treturn context.WithValue(ctx, auth.UserIDContextKey, userID)\n}\n"
  },
  {
    "path": "server/router/api/v1/test/user_notification_test.go",
    "content": "package test\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n\n\tapiv1 \"github.com/usememos/memos/proto/gen/api/v1\"\n\tstorepb \"github.com/usememos/memos/proto/gen/store\"\n\t\"github.com/usememos/memos/store\"\n)\n\nfunc TestListUserNotificationsIncludesMemoCommentPayload(t *testing.T) {\n\tctx := context.Background()\n\tts := NewTestService(t)\n\tdefer ts.Cleanup()\n\n\towner, err := ts.CreateRegularUser(ctx, \"notification-owner\")\n\trequire.NoError(t, err)\n\townerCtx := ts.CreateUserContext(ctx, owner.ID)\n\n\tcommenter, err := ts.CreateRegularUser(ctx, \"notification-commenter\")\n\trequire.NoError(t, err)\n\tcommenterCtx := ts.CreateUserContext(ctx, commenter.ID)\n\n\tmemo, err := ts.Service.CreateMemo(ownerCtx, &apiv1.CreateMemoRequest{\n\t\tMemo: &apiv1.Memo{\n\t\t\tContent:    \"Base memo\",\n\t\t\tVisibility: apiv1.Visibility_PUBLIC,\n\t\t},\n\t})\n\trequire.NoError(t, err)\n\n\tcomment, err := ts.Service.CreateMemoComment(commenterCtx, &apiv1.CreateMemoCommentRequest{\n\t\tName: memo.Name,\n\t\tComment: &apiv1.Memo{\n\t\t\tContent:    \"Comment content\",\n\t\t\tVisibility: apiv1.Visibility_PUBLIC,\n\t\t},\n\t})\n\trequire.NoError(t, err)\n\n\tresp, err := ts.Service.ListUserNotifications(ownerCtx, &apiv1.ListUserNotificationsRequest{\n\t\tParent: fmt.Sprintf(\"users/%d\", owner.ID),\n\t})\n\trequire.NoError(t, err)\n\trequire.Len(t, resp.Notifications, 1)\n\n\tnotification := resp.Notifications[0]\n\trequire.Equal(t, apiv1.UserNotification_MEMO_COMMENT, notification.Type)\n\trequire.NotNil(t, notification.GetMemoComment())\n\trequire.Equal(t, comment.Name, notification.GetMemoComment().Memo)\n\trequire.Equal(t, memo.Name, notification.GetMemoComment().RelatedMemo)\n}\n\nfunc TestListUserNotificationsStoresMemoCommentPayloadInInbox(t *testing.T) {\n\tctx := context.Background()\n\tts := NewTestService(t)\n\tdefer ts.Cleanup()\n\n\towner, err := ts.CreateRegularUser(ctx, \"notification-owner\")\n\trequire.NoError(t, err)\n\townerCtx := ts.CreateUserContext(ctx, owner.ID)\n\n\tcommenter, err := ts.CreateRegularUser(ctx, \"notification-commenter\")\n\trequire.NoError(t, err)\n\tcommenterCtx := ts.CreateUserContext(ctx, commenter.ID)\n\n\tmemo, err := ts.Service.CreateMemo(ownerCtx, &apiv1.CreateMemoRequest{\n\t\tMemo: &apiv1.Memo{\n\t\t\tContent:    \"Base memo\",\n\t\t\tVisibility: apiv1.Visibility_PUBLIC,\n\t\t},\n\t})\n\trequire.NoError(t, err)\n\n\t_, err = ts.Service.CreateMemoComment(commenterCtx, &apiv1.CreateMemoCommentRequest{\n\t\tName: memo.Name,\n\t\tComment: &apiv1.Memo{\n\t\t\tContent:    \"Comment content\",\n\t\t\tVisibility: apiv1.Visibility_PUBLIC,\n\t\t},\n\t})\n\trequire.NoError(t, err)\n\n\tmessageType := storepb.InboxMessage_MEMO_COMMENT\n\tinboxes, err := ts.Store.ListInboxes(ctx, &store.FindInbox{\n\t\tReceiverID:  &owner.ID,\n\t\tMessageType: &messageType,\n\t})\n\trequire.NoError(t, err)\n\trequire.Len(t, inboxes, 1)\n\trequire.NotNil(t, inboxes[0].Message)\n\trequire.NotNil(t, inboxes[0].Message.GetMemoComment())\n\trequire.NotZero(t, inboxes[0].Message.GetMemoComment().MemoId)\n\trequire.NotZero(t, inboxes[0].Message.GetMemoComment().RelatedMemoId)\n}\n\nfunc TestListUserNotificationsOmitsPayloadWhenMemosDeleted(t *testing.T) {\n\tctx := context.Background()\n\tts := NewTestService(t)\n\tdefer ts.Cleanup()\n\n\towner, err := ts.CreateRegularUser(ctx, \"notification-owner\")\n\trequire.NoError(t, err)\n\townerCtx := ts.CreateUserContext(ctx, owner.ID)\n\n\tcommenter, err := ts.CreateRegularUser(ctx, \"notification-commenter\")\n\trequire.NoError(t, err)\n\tcommenterCtx := ts.CreateUserContext(ctx, commenter.ID)\n\n\tmemo, err := ts.Service.CreateMemo(ownerCtx, &apiv1.CreateMemoRequest{\n\t\tMemo: &apiv1.Memo{\n\t\t\tContent:    \"Base memo\",\n\t\t\tVisibility: apiv1.Visibility_PUBLIC,\n\t\t},\n\t})\n\trequire.NoError(t, err)\n\n\t_, err = ts.Service.CreateMemoComment(commenterCtx, &apiv1.CreateMemoCommentRequest{\n\t\tName: memo.Name,\n\t\tComment: &apiv1.Memo{\n\t\t\tContent:    \"Comment content\",\n\t\t\tVisibility: apiv1.Visibility_PUBLIC,\n\t\t},\n\t})\n\trequire.NoError(t, err)\n\n\t_, err = ts.Service.DeleteMemo(ownerCtx, &apiv1.DeleteMemoRequest{\n\t\tName: memo.Name,\n\t})\n\trequire.NoError(t, err)\n\n\tresp, err := ts.Service.ListUserNotifications(ownerCtx, &apiv1.ListUserNotificationsRequest{\n\t\tParent: fmt.Sprintf(\"users/%d\", owner.ID),\n\t})\n\trequire.NoError(t, err)\n\trequire.Len(t, resp.Notifications, 1)\n\trequire.Equal(t, apiv1.UserNotification_MEMO_COMMENT, resp.Notifications[0].Type)\n\trequire.Nil(t, resp.Notifications[0].GetMemoComment())\n}\n"
  },
  {
    "path": "server/router/api/v1/test/user_service_registration_test.go",
    "content": "package test\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n\n\tapiv1 \"github.com/usememos/memos/proto/gen/api/v1\"\n\tstorepb \"github.com/usememos/memos/proto/gen/store\"\n)\n\nfunc TestCreateUserRegistration(t *testing.T) {\n\tctx := context.Background()\n\n\tt.Run(\"CreateUser success when registration enabled\", func(t *testing.T) {\n\t\tts := NewTestService(t)\n\t\tdefer ts.Cleanup()\n\n\t\t// User registration is enabled by default, no need to set it explicitly\n\n\t\t// Create user without authentication - should succeed\n\t\t_, err := ts.Service.CreateUser(ctx, &apiv1.CreateUserRequest{\n\t\t\tUser: &apiv1.User{\n\t\t\t\tUsername: \"newuser\",\n\t\t\t\tEmail:    \"newuser@example.com\",\n\t\t\t\tPassword: \"password123\",\n\t\t\t},\n\t\t})\n\t\trequire.NoError(t, err)\n\t})\n\n\tt.Run(\"CreateUser blocked when registration disabled\", func(t *testing.T) {\n\t\tts := NewTestService(t)\n\t\tdefer ts.Cleanup()\n\n\t\t// Create a host user first so we're not in first-user setup mode\n\t\t_, err := ts.CreateHostUser(ctx, \"admin\")\n\t\trequire.NoError(t, err)\n\n\t\t// Disable user registration\n\t\t_, err = ts.Store.UpsertInstanceSetting(ctx, &storepb.InstanceSetting{\n\t\t\tKey: storepb.InstanceSettingKey_GENERAL,\n\t\t\tValue: &storepb.InstanceSetting_GeneralSetting{\n\t\t\t\tGeneralSetting: &storepb.InstanceGeneralSetting{\n\t\t\t\t\tDisallowUserRegistration: true,\n\t\t\t\t},\n\t\t\t},\n\t\t})\n\t\trequire.NoError(t, err)\n\n\t\t// Try to create user without authentication - should fail\n\t\t_, err = ts.Service.CreateUser(ctx, &apiv1.CreateUserRequest{\n\t\t\tUser: &apiv1.User{\n\t\t\t\tUsername: \"newuser\",\n\t\t\t\tEmail:    \"newuser@example.com\",\n\t\t\t\tPassword: \"password123\",\n\t\t\t},\n\t\t})\n\t\trequire.Error(t, err)\n\t\trequire.Contains(t, err.Error(), \"not allowed\")\n\t})\n\n\tt.Run(\"CreateUser succeeds for superuser even when registration disabled\", func(t *testing.T) {\n\t\tts := NewTestService(t)\n\t\tdefer ts.Cleanup()\n\n\t\t// Create host user\n\t\thostUser, err := ts.CreateHostUser(ctx, \"admin\")\n\t\trequire.NoError(t, err)\n\t\thostCtx := ts.CreateUserContext(ctx, hostUser.ID)\n\n\t\t// Disable user registration\n\t\t_, err = ts.Store.UpsertInstanceSetting(ctx, &storepb.InstanceSetting{\n\t\t\tKey: storepb.InstanceSettingKey_GENERAL,\n\t\t\tValue: &storepb.InstanceSetting_GeneralSetting{\n\t\t\t\tGeneralSetting: &storepb.InstanceGeneralSetting{\n\t\t\t\t\tDisallowUserRegistration: true,\n\t\t\t\t},\n\t\t\t},\n\t\t})\n\t\trequire.NoError(t, err)\n\n\t\t// Host user can create users even when registration is disabled - should succeed\n\t\t_, err = ts.Service.CreateUser(hostCtx, &apiv1.CreateUserRequest{\n\t\t\tUser: &apiv1.User{\n\t\t\t\tUsername: \"newuser\",\n\t\t\t\tEmail:    \"newuser@example.com\",\n\t\t\t\tPassword: \"password123\",\n\t\t\t},\n\t\t})\n\t\trequire.NoError(t, err)\n\t})\n\n\tt.Run(\"CreateUser regular user cannot create users when registration disabled\", func(t *testing.T) {\n\t\tts := NewTestService(t)\n\t\tdefer ts.Cleanup()\n\n\t\t// Create regular user\n\t\tregularUser, err := ts.CreateRegularUser(ctx, \"regularuser\")\n\t\trequire.NoError(t, err)\n\t\tregularUserCtx := ts.CreateUserContext(ctx, regularUser.ID)\n\n\t\t// Disable user registration\n\t\t_, err = ts.Store.UpsertInstanceSetting(ctx, &storepb.InstanceSetting{\n\t\t\tKey: storepb.InstanceSettingKey_GENERAL,\n\t\t\tValue: &storepb.InstanceSetting_GeneralSetting{\n\t\t\t\tGeneralSetting: &storepb.InstanceGeneralSetting{\n\t\t\t\t\tDisallowUserRegistration: true,\n\t\t\t\t},\n\t\t\t},\n\t\t})\n\t\trequire.NoError(t, err)\n\n\t\t// Regular user tries to create user when registration is disabled - should fail\n\t\t_, err = ts.Service.CreateUser(regularUserCtx, &apiv1.CreateUserRequest{\n\t\t\tUser: &apiv1.User{\n\t\t\t\tUsername: \"newuser\",\n\t\t\t\tEmail:    \"newuser@example.com\",\n\t\t\t\tPassword: \"password123\",\n\t\t\t},\n\t\t})\n\t\trequire.Error(t, err)\n\t\trequire.Contains(t, err.Error(), \"not allowed\")\n\t})\n\n\tt.Run(\"CreateUser host can assign roles\", func(t *testing.T) {\n\t\tts := NewTestService(t)\n\t\tdefer ts.Cleanup()\n\n\t\t// Create host user\n\t\thostUser, err := ts.CreateHostUser(ctx, \"admin\")\n\t\trequire.NoError(t, err)\n\t\thostCtx := ts.CreateUserContext(ctx, hostUser.ID)\n\n\t\t// Host user can create user with specific role - should succeed\n\t\tcreatedUser, err := ts.Service.CreateUser(hostCtx, &apiv1.CreateUserRequest{\n\t\t\tUser: &apiv1.User{\n\t\t\t\tUsername: \"newadmin\",\n\t\t\t\tEmail:    \"newadmin@example.com\",\n\t\t\t\tPassword: \"password123\",\n\t\t\t\tRole:     apiv1.User_ADMIN,\n\t\t\t},\n\t\t})\n\t\trequire.NoError(t, err)\n\t\trequire.NotNil(t, createdUser)\n\t\trequire.Equal(t, apiv1.User_ADMIN, createdUser.Role)\n\t})\n\n\tt.Run(\"CreateUser unauthenticated user can only create regular user\", func(t *testing.T) {\n\t\tts := NewTestService(t)\n\t\tdefer ts.Cleanup()\n\n\t\t// Create a host user first so we're not in first-user setup mode\n\t\t_, err := ts.CreateHostUser(ctx, \"admin\")\n\t\trequire.NoError(t, err)\n\n\t\t// User registration is enabled by default\n\n\t\t// Unauthenticated user tries to create admin user - role should be ignored\n\t\tcreatedUser, err := ts.Service.CreateUser(ctx, &apiv1.CreateUserRequest{\n\t\t\tUser: &apiv1.User{\n\t\t\t\tUsername: \"wannabeadmin\",\n\t\t\t\tEmail:    \"wannabeadmin@example.com\",\n\t\t\t\tPassword: \"password123\",\n\t\t\t\tRole:     apiv1.User_ADMIN, // This should be ignored\n\t\t\t},\n\t\t})\n\t\trequire.NoError(t, err)\n\t\trequire.NotNil(t, createdUser)\n\t\trequire.Equal(t, apiv1.User_USER, createdUser.Role, \"Unauthenticated users can only create USER role\")\n\t})\n}\n"
  },
  {
    "path": "server/router/api/v1/test/user_service_stats_test.go",
    "content": "package test\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n\n\tv1pb \"github.com/usememos/memos/proto/gen/api/v1\"\n\tstorepb \"github.com/usememos/memos/proto/gen/store\"\n\t\"github.com/usememos/memos/store\"\n)\n\nfunc TestGetUserStats_TagCount(t *testing.T) {\n\tctx := context.Background()\n\n\t// Create test service\n\tts := NewTestService(t)\n\tdefer ts.Cleanup()\n\n\t// Create a test host user\n\tuser, err := ts.CreateHostUser(ctx, \"test_user\")\n\trequire.NoError(t, err)\n\n\t// Create user context for authentication\n\tuserCtx := ts.CreateUserContext(ctx, user.ID)\n\n\t// Create a memo with a single tag\n\tmemo, err := ts.Store.CreateMemo(ctx, &store.Memo{\n\t\tUID:        \"test-memo-1\",\n\t\tCreatorID:  user.ID,\n\t\tContent:    \"This is a test memo with #test tag\",\n\t\tVisibility: store.Public,\n\t\tPayload: &storepb.MemoPayload{\n\t\t\tTags: []string{\"test\"},\n\t\t},\n\t})\n\trequire.NoError(t, err)\n\trequire.NotNil(t, memo)\n\n\t// Test GetUserStats\n\tuserName := fmt.Sprintf(\"users/%d\", user.ID)\n\tresponse, err := ts.Service.GetUserStats(userCtx, &v1pb.GetUserStatsRequest{\n\t\tName: userName,\n\t})\n\trequire.NoError(t, err)\n\trequire.NotNil(t, response)\n\n\t// Check that the tag count is exactly 1, not 2\n\trequire.Contains(t, response.TagCount, \"test\")\n\trequire.Equal(t, int32(1), response.TagCount[\"test\"], \"Tag count should be 1 for a single occurrence\")\n\n\t// Create another memo with the same tag\n\tmemo2, err := ts.Store.CreateMemo(ctx, &store.Memo{\n\t\tUID:        \"test-memo-2\",\n\t\tCreatorID:  user.ID,\n\t\tContent:    \"Another memo with #test tag\",\n\t\tVisibility: store.Public,\n\t\tPayload: &storepb.MemoPayload{\n\t\t\tTags: []string{\"test\"},\n\t\t},\n\t})\n\trequire.NoError(t, err)\n\trequire.NotNil(t, memo2)\n\n\t// Test GetUserStats again\n\tresponse2, err := ts.Service.GetUserStats(userCtx, &v1pb.GetUserStatsRequest{\n\t\tName: userName,\n\t})\n\trequire.NoError(t, err)\n\trequire.NotNil(t, response2)\n\n\t// Check that the tag count is exactly 2, not 3\n\trequire.Contains(t, response2.TagCount, \"test\")\n\trequire.Equal(t, int32(2), response2.TagCount[\"test\"], \"Tag count should be 2 for two occurrences\")\n\n\t// Test with a new unique tag\n\tmemo3, err := ts.Store.CreateMemo(ctx, &store.Memo{\n\t\tUID:        \"test-memo-3\",\n\t\tCreatorID:  user.ID,\n\t\tContent:    \"Memo with #unique tag\",\n\t\tVisibility: store.Public,\n\t\tPayload: &storepb.MemoPayload{\n\t\t\tTags: []string{\"unique\"},\n\t\t},\n\t})\n\trequire.NoError(t, err)\n\trequire.NotNil(t, memo3)\n\n\t// Test GetUserStats for the new tag\n\tresponse3, err := ts.Service.GetUserStats(userCtx, &v1pb.GetUserStatsRequest{\n\t\tName: userName,\n\t})\n\trequire.NoError(t, err)\n\trequire.NotNil(t, response3)\n\n\t// Check that the unique tag count is exactly 1\n\trequire.Contains(t, response3.TagCount, \"unique\")\n\trequire.Equal(t, int32(1), response3.TagCount[\"unique\"], \"New tag count should be 1 for first occurrence\")\n\n\t// The original test tag should still be 2\n\trequire.Contains(t, response3.TagCount, \"test\")\n\trequire.Equal(t, int32(2), response3.TagCount[\"test\"], \"Original tag count should remain 2\")\n}\n"
  },
  {
    "path": "server/router/api/v1/user_service.go",
    "content": "package v1\n\nimport (\n\t\"context\"\n\t\"crypto/rand\"\n\t\"encoding/hex\"\n\t\"fmt\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/google/cel-go/cel\"\n\t\"github.com/google/cel-go/common/ast\"\n\t\"github.com/pkg/errors\"\n\t\"golang.org/x/crypto/bcrypt\"\n\t\"google.golang.org/grpc/codes\"\n\t\"google.golang.org/grpc/status\"\n\t\"google.golang.org/protobuf/types/known/emptypb\"\n\t\"google.golang.org/protobuf/types/known/timestamppb\"\n\n\t\"github.com/usememos/memos/internal/base\"\n\t\"github.com/usememos/memos/internal/util\"\n\t\"github.com/usememos/memos/plugin/webhook\"\n\tv1pb \"github.com/usememos/memos/proto/gen/api/v1\"\n\tstorepb \"github.com/usememos/memos/proto/gen/store\"\n\t\"github.com/usememos/memos/server/auth\"\n\t\"github.com/usememos/memos/store\"\n)\n\nfunc (s *APIV1Service) ListUsers(ctx context.Context, request *v1pb.ListUsersRequest) (*v1pb.ListUsersResponse, error) {\n\tcurrentUser, err := s.fetchCurrentUser(ctx)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to get user: %v\", err)\n\t}\n\tif currentUser == nil {\n\t\treturn nil, status.Errorf(codes.Unauthenticated, \"user not authenticated\")\n\t}\n\tif currentUser.Role != store.RoleAdmin {\n\t\treturn nil, status.Errorf(codes.PermissionDenied, \"permission denied\")\n\t}\n\n\tuserFind := &store.FindUser{}\n\n\tif request.Filter != \"\" {\n\t\tusername, err := extractUsernameFromFilter(request.Filter)\n\t\tif err != nil {\n\t\t\treturn nil, status.Errorf(codes.InvalidArgument, \"invalid filter: %v\", err)\n\t\t}\n\t\tif username != \"\" {\n\t\t\tuserFind.Username = &username\n\t\t}\n\t}\n\n\tusers, err := s.Store.ListUsers(ctx, userFind)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to list users: %v\", err)\n\t}\n\n\t// TODO: Implement proper ordering, and pagination\n\t// For now, return all users with basic structure\n\tresponse := &v1pb.ListUsersResponse{\n\t\tUsers:     []*v1pb.User{},\n\t\tTotalSize: int32(len(users)),\n\t}\n\tfor _, user := range users {\n\t\tresponse.Users = append(response.Users, convertUserFromStore(user))\n\t}\n\treturn response, nil\n}\n\nfunc (s *APIV1Service) GetUser(ctx context.Context, request *v1pb.GetUserRequest) (*v1pb.User, error) {\n\t// Extract identifier from \"users/{id_or_username}\"\n\tidentifier := extractUserIdentifierFromName(request.Name)\n\tif identifier == \"\" {\n\t\treturn nil, status.Errorf(codes.InvalidArgument, \"invalid user name: %s\", request.Name)\n\t}\n\n\tvar user *store.User\n\tvar err error\n\n\t// Try to parse as numeric ID first\n\tif userID, parseErr := strconv.ParseInt(identifier, 10, 32); parseErr == nil {\n\t\t// It's a numeric ID\n\t\tuserID32 := int32(userID)\n\t\tuser, err = s.Store.GetUser(ctx, &store.FindUser{\n\t\t\tID: &userID32,\n\t\t})\n\t} else {\n\t\t// It's a username\n\t\tuser, err = s.Store.GetUser(ctx, &store.FindUser{\n\t\t\tUsername: &identifier,\n\t\t})\n\t}\n\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to get user: %v\", err)\n\t}\n\tif user == nil {\n\t\treturn nil, status.Errorf(codes.NotFound, \"user not found\")\n\t}\n\treturn convertUserFromStore(user), nil\n}\n\nfunc (s *APIV1Service) CreateUser(ctx context.Context, request *v1pb.CreateUserRequest) (*v1pb.User, error) {\n\t// Get current user (might be nil for unauthenticated requests)\n\tcurrentUser, _ := s.fetchCurrentUser(ctx)\n\n\t// Check if there are any existing users (for first-time setup detection)\n\tlimitOne := 1\n\tallUsers, err := s.Store.ListUsers(ctx, &store.FindUser{Limit: &limitOne})\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to list users: %v\", err)\n\t}\n\tisFirstUser := len(allUsers) == 0\n\n\t// Check registration settings FIRST (unless it's the very first user)\n\tif !isFirstUser {\n\t\t// Only allow user registration if it is enabled in the settings, or if the user is a superuser\n\t\tif currentUser == nil || !isSuperUser(currentUser) {\n\t\t\tinstanceGeneralSetting, err := s.Store.GetInstanceGeneralSetting(ctx)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, status.Errorf(codes.Internal, \"failed to get instance general setting, error: %v\", err)\n\t\t\t}\n\t\t\tif instanceGeneralSetting.DisallowUserRegistration {\n\t\t\t\treturn nil, status.Errorf(codes.PermissionDenied, \"user registration is not allowed\")\n\t\t\t}\n\t\t}\n\t}\n\n\t// Determine the role to assign\n\tvar roleToAssign store.Role\n\tif isFirstUser {\n\t\t// First-time setup: create the first user as ADMIN (no authentication required)\n\t\troleToAssign = store.RoleAdmin\n\t} else if currentUser != nil && currentUser.Role == store.RoleAdmin {\n\t\t// Authenticated ADMIN user can create users with any role specified in request\n\t\tif request.User.Role != v1pb.User_ROLE_UNSPECIFIED {\n\t\t\troleToAssign = convertUserRoleToStore(request.User.Role)\n\t\t} else {\n\t\t\troleToAssign = store.RoleUser\n\t\t}\n\t} else {\n\t\t// Unauthenticated or non-ADMIN users can only create normal users\n\t\troleToAssign = store.RoleUser\n\t}\n\n\tif !base.UIDMatcher.MatchString(strings.ToLower(request.User.Username)) {\n\t\treturn nil, status.Errorf(codes.InvalidArgument, \"invalid username: %s\", request.User.Username)\n\t}\n\n\t// If validate_only is true, just validate without creating\n\tif request.ValidateOnly {\n\t\t// Perform validation checks without actually creating the user\n\t\treturn &v1pb.User{\n\t\t\tUsername:    request.User.Username,\n\t\t\tEmail:       request.User.Email,\n\t\t\tDisplayName: request.User.DisplayName,\n\t\t\tRole:        convertUserRoleFromStore(roleToAssign),\n\t\t}, nil\n\t}\n\n\tpasswordHash, err := bcrypt.GenerateFromPassword([]byte(request.User.Password), bcrypt.DefaultCost)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to generate password hash: %v\", err)\n\t}\n\n\tuser, err := s.Store.CreateUser(ctx, &store.User{\n\t\tUsername:     request.User.Username,\n\t\tRole:         roleToAssign,\n\t\tEmail:        request.User.Email,\n\t\tNickname:     request.User.DisplayName,\n\t\tPasswordHash: string(passwordHash),\n\t})\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to create user: %v\", err)\n\t}\n\n\treturn convertUserFromStore(user), nil\n}\n\nfunc (s *APIV1Service) UpdateUser(ctx context.Context, request *v1pb.UpdateUserRequest) (*v1pb.User, error) {\n\tif request.UpdateMask == nil || len(request.UpdateMask.Paths) == 0 {\n\t\treturn nil, status.Errorf(codes.InvalidArgument, \"update mask is empty\")\n\t}\n\tuserID, err := ExtractUserIDFromName(request.User.Name)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.InvalidArgument, \"invalid user name: %v\", err)\n\t}\n\tcurrentUser, err := s.fetchCurrentUser(ctx)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to get user: %v\", err)\n\t}\n\tif currentUser == nil {\n\t\treturn nil, status.Errorf(codes.Unauthenticated, \"user not authenticated\")\n\t}\n\t// Check permission.\n\t// Only allow admin or self to update user.\n\tif currentUser.ID != userID && currentUser.Role != store.RoleAdmin {\n\t\treturn nil, status.Errorf(codes.PermissionDenied, \"permission denied\")\n\t}\n\n\tuser, err := s.Store.GetUser(ctx, &store.FindUser{ID: &userID})\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to get user: %v\", err)\n\t}\n\tif user == nil {\n\t\t// Handle allow_missing field\n\t\tif request.AllowMissing {\n\t\t\t// Could create user if missing, but for now return not found\n\t\t\treturn nil, status.Errorf(codes.NotFound, \"user not found\")\n\t\t}\n\t\treturn nil, status.Errorf(codes.NotFound, \"user not found\")\n\t}\n\n\tcurrentTs := time.Now().Unix()\n\tupdate := &store.UpdateUser{\n\t\tID:        user.ID,\n\t\tUpdatedTs: &currentTs,\n\t}\n\tinstanceGeneralSetting, err := s.Store.GetInstanceGeneralSetting(ctx)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to get instance general setting: %v\", err)\n\t}\n\tfor _, field := range request.UpdateMask.Paths {\n\t\tswitch field {\n\t\tcase \"username\":\n\t\t\tif instanceGeneralSetting.DisallowChangeUsername {\n\t\t\t\treturn nil, status.Errorf(codes.PermissionDenied, \"permission denied: disallow change username\")\n\t\t\t}\n\t\t\tif !base.UIDMatcher.MatchString(strings.ToLower(request.User.Username)) {\n\t\t\t\treturn nil, status.Errorf(codes.InvalidArgument, \"invalid username: %s\", request.User.Username)\n\t\t\t}\n\t\t\tupdate.Username = &request.User.Username\n\t\tcase \"display_name\":\n\t\t\tif instanceGeneralSetting.DisallowChangeNickname {\n\t\t\t\treturn nil, status.Errorf(codes.PermissionDenied, \"permission denied: disallow change nickname\")\n\t\t\t}\n\t\t\tupdate.Nickname = &request.User.DisplayName\n\t\tcase \"email\":\n\t\t\tupdate.Email = &request.User.Email\n\t\tcase \"avatar_url\":\n\t\t\t// Validate avatar MIME type to prevent XSS during upload\n\t\t\tif request.User.AvatarUrl != \"\" {\n\t\t\t\timageType, _, err := extractImageInfo(request.User.AvatarUrl)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, status.Errorf(codes.InvalidArgument, \"invalid avatar format: %v\", err)\n\t\t\t\t}\n\t\t\t\t// Only allow safe image formats for avatars\n\t\t\t\tallowedAvatarTypes := map[string]bool{\n\t\t\t\t\t\"image/png\":  true,\n\t\t\t\t\t\"image/jpeg\": true,\n\t\t\t\t\t\"image/jpg\":  true,\n\t\t\t\t\t\"image/gif\":  true,\n\t\t\t\t\t\"image/webp\": true,\n\t\t\t\t}\n\t\t\t\tif !allowedAvatarTypes[imageType] {\n\t\t\t\t\treturn nil, status.Errorf(codes.InvalidArgument, \"invalid avatar image type: %s. Only PNG, JPEG, GIF, and WebP are allowed\", imageType)\n\t\t\t\t}\n\t\t\t}\n\t\t\tupdate.AvatarURL = &request.User.AvatarUrl\n\t\tcase \"description\":\n\t\t\tupdate.Description = &request.User.Description\n\t\tcase \"role\":\n\t\t\t// Only allow admin to update role.\n\t\t\tif currentUser.Role != store.RoleAdmin {\n\t\t\t\treturn nil, status.Errorf(codes.PermissionDenied, \"permission denied\")\n\t\t\t}\n\t\t\trole := convertUserRoleToStore(request.User.Role)\n\t\t\tupdate.Role = &role\n\t\tcase \"password\":\n\t\t\tpasswordHash, err := bcrypt.GenerateFromPassword([]byte(request.User.Password), bcrypt.DefaultCost)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, status.Errorf(codes.Internal, \"failed to generate password hash: %v\", err)\n\t\t\t}\n\t\t\tpasswordHashStr := string(passwordHash)\n\t\t\tupdate.PasswordHash = &passwordHashStr\n\t\tcase \"state\":\n\t\t\trowStatus := convertStateToStore(request.User.State)\n\t\t\tupdate.RowStatus = &rowStatus\n\t\tdefault:\n\t\t\treturn nil, status.Errorf(codes.InvalidArgument, \"invalid update path: %s\", field)\n\t\t}\n\t}\n\n\tupdatedUser, err := s.Store.UpdateUser(ctx, update)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to update user: %v\", err)\n\t}\n\n\treturn convertUserFromStore(updatedUser), nil\n}\n\nfunc (s *APIV1Service) DeleteUser(ctx context.Context, request *v1pb.DeleteUserRequest) (*emptypb.Empty, error) {\n\tuserID, err := ExtractUserIDFromName(request.Name)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.InvalidArgument, \"invalid user name: %v\", err)\n\t}\n\tcurrentUser, err := s.fetchCurrentUser(ctx)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to get user: %v\", err)\n\t}\n\tif currentUser == nil {\n\t\treturn nil, status.Errorf(codes.Unauthenticated, \"user not authenticated\")\n\t}\n\tif currentUser.ID != userID && currentUser.Role != store.RoleAdmin {\n\t\treturn nil, status.Errorf(codes.PermissionDenied, \"permission denied\")\n\t}\n\n\tuser, err := s.Store.GetUser(ctx, &store.FindUser{ID: &userID})\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to get user: %v\", err)\n\t}\n\tif user == nil {\n\t\treturn nil, status.Errorf(codes.NotFound, \"user not found\")\n\t}\n\n\tif err := s.Store.DeleteUser(ctx, &store.DeleteUser{\n\t\tID: user.ID,\n\t}); err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to delete user: %v\", err)\n\t}\n\n\treturn &emptypb.Empty{}, nil\n}\n\nfunc getDefaultUserGeneralSetting() *v1pb.UserSetting_GeneralSetting {\n\treturn &v1pb.UserSetting_GeneralSetting{\n\t\tLocale:         \"en\",\n\t\tMemoVisibility: \"PRIVATE\",\n\t\tTheme:          \"\",\n\t}\n}\n\nfunc (s *APIV1Service) GetUserSetting(ctx context.Context, request *v1pb.GetUserSettingRequest) (*v1pb.UserSetting, error) {\n\t// Parse resource name: users/{user}/settings/{setting}\n\tuserID, settingKey, err := ExtractUserIDAndSettingKeyFromName(request.Name)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.InvalidArgument, \"invalid resource name: %v\", err)\n\t}\n\n\tcurrentUser, err := s.fetchCurrentUser(ctx)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to get current user: %v\", err)\n\t}\n\tif currentUser == nil {\n\t\treturn nil, status.Errorf(codes.Unauthenticated, \"user not authenticated\")\n\t}\n\n\t// Only allow user to get their own settings\n\tif currentUser.ID != userID {\n\t\treturn nil, status.Errorf(codes.PermissionDenied, \"permission denied\")\n\t}\n\n\t// Convert setting key string to store enum\n\tstoreKey, err := convertSettingKeyToStore(settingKey)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.InvalidArgument, \"invalid setting key: %v\", err)\n\t}\n\n\tuserSetting, err := s.Store.GetUserSetting(ctx, &store.FindUserSetting{\n\t\tUserID: &userID,\n\t\tKey:    storeKey,\n\t})\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to get user setting: %v\", err)\n\t}\n\n\treturn convertUserSettingFromStore(userSetting, userID, storeKey), nil\n}\n\nfunc (s *APIV1Service) UpdateUserSetting(ctx context.Context, request *v1pb.UpdateUserSettingRequest) (*v1pb.UserSetting, error) {\n\t// Parse resource name: users/{user}/settings/{setting}\n\tuserID, settingKey, err := ExtractUserIDAndSettingKeyFromName(request.Setting.Name)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.InvalidArgument, \"invalid resource name: %v\", err)\n\t}\n\n\tcurrentUser, err := s.fetchCurrentUser(ctx)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to get current user: %v\", err)\n\t}\n\tif currentUser == nil {\n\t\treturn nil, status.Errorf(codes.Unauthenticated, \"user not authenticated\")\n\t}\n\n\t// Only allow user to update their own settings\n\tif currentUser.ID != userID {\n\t\treturn nil, status.Errorf(codes.PermissionDenied, \"permission denied\")\n\t}\n\n\tif request.UpdateMask == nil || len(request.UpdateMask.Paths) == 0 {\n\t\treturn nil, status.Errorf(codes.InvalidArgument, \"update mask is empty\")\n\t}\n\n\t// Convert setting key string to store enum\n\tstoreKey, err := convertSettingKeyToStore(settingKey)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.InvalidArgument, \"invalid setting key: %v\", err)\n\t}\n\n\tvar updatedSetting *v1pb.UserSetting\n\tswitch storeKey {\n\tcase storepb.UserSetting_GENERAL:\n\t\texistingUserSetting, _ := s.Store.GetUserSetting(ctx, &store.FindUserSetting{\n\t\t\tUserID: &userID,\n\t\t\tKey:    storeKey,\n\t\t})\n\n\t\tgeneralSetting := &storepb.GeneralUserSetting{}\n\t\tif existingUserSetting != nil {\n\t\t\t// Start with existing general setting values.\n\t\t\tgeneralSetting = existingUserSetting.GetGeneral()\n\t\t}\n\n\t\tupdatedGeneral := &v1pb.UserSetting_GeneralSetting{\n\t\t\tMemoVisibility: generalSetting.GetMemoVisibility(),\n\t\t\tLocale:         generalSetting.GetLocale(),\n\t\t\tTheme:          generalSetting.GetTheme(),\n\t\t}\n\n\t\tincomingGeneral := request.Setting.GetGeneralSetting()\n\t\tif incomingGeneral == nil {\n\t\t\treturn nil, status.Errorf(codes.InvalidArgument, \"general setting is required\")\n\t\t}\n\t\tfor _, field := range request.UpdateMask.Paths {\n\t\t\tswitch field {\n\t\t\tcase \"memo_visibility\":\n\t\t\t\tupdatedGeneral.MemoVisibility = incomingGeneral.MemoVisibility\n\t\t\tcase \"theme\":\n\t\t\t\tupdatedGeneral.Theme = incomingGeneral.Theme\n\t\t\tcase \"locale\":\n\t\t\t\tupdatedGeneral.Locale = incomingGeneral.Locale\n\t\t\tdefault:\n\t\t\t\t// Ignore unsupported fields.\n\t\t\t}\n\t\t}\n\n\t\tupdatedSetting = &v1pb.UserSetting{\n\t\t\tName: request.Setting.Name,\n\t\t\tValue: &v1pb.UserSetting_GeneralSetting_{\n\t\t\t\tGeneralSetting: updatedGeneral,\n\t\t\t},\n\t\t}\n\tdefault:\n\t\treturn nil, status.Errorf(codes.InvalidArgument, \"setting type %s should not be updated via UpdateUserSetting\", storeKey.String())\n\t}\n\n\t// Convert API setting to store setting\n\tstoreSetting, err := convertUserSettingToStore(updatedSetting, userID, storeKey)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.InvalidArgument, \"failed to convert setting: %v\", err)\n\t}\n\n\t// Upsert the setting\n\tif _, err := s.Store.UpsertUserSetting(ctx, storeSetting); err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to upsert user setting: %v\", err)\n\t}\n\n\treturn s.GetUserSetting(ctx, &v1pb.GetUserSettingRequest{Name: request.Setting.Name})\n}\n\nfunc (s *APIV1Service) ListUserSettings(ctx context.Context, request *v1pb.ListUserSettingsRequest) (*v1pb.ListUserSettingsResponse, error) {\n\tuserID, err := ExtractUserIDFromName(request.Parent)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.InvalidArgument, \"invalid parent name: %v\", err)\n\t}\n\n\tcurrentUser, err := s.fetchCurrentUser(ctx)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to get current user: %v\", err)\n\t}\n\tif currentUser == nil {\n\t\treturn nil, status.Errorf(codes.Unauthenticated, \"user not authenticated\")\n\t}\n\n\t// Only allow user to list their own settings\n\tif currentUser.ID != userID {\n\t\treturn nil, status.Errorf(codes.PermissionDenied, \"permission denied\")\n\t}\n\n\tuserSettings, err := s.Store.ListUserSettings(ctx, &store.FindUserSetting{\n\t\tUserID: &userID,\n\t})\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to list user settings: %v\", err)\n\t}\n\n\tsettings := make([]*v1pb.UserSetting, 0, len(userSettings))\n\tfor _, storeSetting := range userSettings {\n\t\tapiSetting := convertUserSettingFromStore(storeSetting, userID, storeSetting.Key)\n\t\tif apiSetting != nil {\n\t\t\tsettings = append(settings, apiSetting)\n\t\t}\n\t}\n\n\thasGeneral := false\n\tfor _, setting := range settings {\n\t\tif setting.GetGeneralSetting() != nil {\n\t\t\thasGeneral = true\n\t\t}\n\t}\n\tif !hasGeneral {\n\t\tdefaultGeneral := &v1pb.UserSetting{\n\t\t\tName: fmt.Sprintf(\"users/%d/settings/%s\", userID, convertSettingKeyFromStore(storepb.UserSetting_GENERAL)),\n\t\t\tValue: &v1pb.UserSetting_GeneralSetting_{\n\t\t\t\tGeneralSetting: getDefaultUserGeneralSetting(),\n\t\t\t},\n\t\t}\n\t\tsettings = append([]*v1pb.UserSetting{defaultGeneral}, settings...)\n\t}\n\tresponse := &v1pb.ListUserSettingsResponse{\n\t\tSettings:  settings,\n\t\tTotalSize: int32(len(settings)),\n\t}\n\n\treturn response, nil\n}\n\n// ListPersonalAccessTokens retrieves all Personal Access Tokens (PATs) for a user.\n//\n// Personal Access Tokens are used for:\n// - Mobile app authentication\n// - CLI tool authentication\n// - API client authentication\n// - Any programmatic access requiring Bearer token auth\n//\n// Security:\n// - Only the token owner can list their tokens\n// - Returns token metadata only (not the actual token value)\n// - Invalid or expired tokens are filtered out\n//\n// Authentication: Required (session cookie or access token)\n// Authorization: User can only list their own tokens.\nfunc (s *APIV1Service) ListPersonalAccessTokens(ctx context.Context, request *v1pb.ListPersonalAccessTokensRequest) (*v1pb.ListPersonalAccessTokensResponse, error) {\n\tuserID, err := ExtractUserIDFromName(request.Parent)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.InvalidArgument, \"invalid user name: %v\", err)\n\t}\n\n\t// Verify permission\n\tclaims := auth.GetUserClaims(ctx)\n\tif claims == nil || claims.UserID != userID {\n\t\tcurrentUser, _ := s.fetchCurrentUser(ctx)\n\t\tif currentUser == nil || (currentUser.ID != userID && currentUser.Role != store.RoleAdmin) {\n\t\t\treturn nil, status.Errorf(codes.PermissionDenied, \"permission denied\")\n\t\t}\n\t}\n\n\ttokens, err := s.Store.GetUserPersonalAccessTokens(ctx, userID)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to get access tokens: %v\", err)\n\t}\n\n\tpersonalAccessTokens := make([]*v1pb.PersonalAccessToken, len(tokens))\n\tfor i, token := range tokens {\n\t\tpersonalAccessTokens[i] = &v1pb.PersonalAccessToken{\n\t\t\tName:        fmt.Sprintf(\"%s/personalAccessTokens/%s\", request.Parent, token.TokenId),\n\t\t\tDescription: token.Description,\n\t\t\tExpiresAt:   token.ExpiresAt,\n\t\t\tCreatedAt:   token.CreatedAt,\n\t\t\tLastUsedAt:  token.LastUsedAt,\n\t\t}\n\t}\n\n\treturn &v1pb.ListPersonalAccessTokensResponse{PersonalAccessTokens: personalAccessTokens}, nil\n}\n\n// CreatePersonalAccessToken creates a new Personal Access Token (PAT) for a user.\n//\n// Use cases:\n// - User manually creates token in settings for mobile app\n// - User creates token for CLI tool\n// - User creates token for third-party integration\n//\n// Token properties:\n// - Random string with memos_pat_ prefix\n// - SHA-256 hash stored in database\n// - Optional expiration time (can be never-expiring)\n// - User-provided description for identification\n//\n// Security considerations:\n// - Full token is only shown ONCE (in this response)\n// - User should copy and store it securely\n// - Token can be revoked by deleting it from settings\n//\n// Authentication: Required (session cookie or access token)\n// Authorization: User can only create tokens for themselves.\nfunc (s *APIV1Service) CreatePersonalAccessToken(ctx context.Context, request *v1pb.CreatePersonalAccessTokenRequest) (*v1pb.CreatePersonalAccessTokenResponse, error) {\n\tuserID, err := ExtractUserIDFromName(request.Parent)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.InvalidArgument, \"invalid user name: %v\", err)\n\t}\n\n\t// Verify permission\n\tclaims := auth.GetUserClaims(ctx)\n\tif claims == nil || claims.UserID != userID {\n\t\tcurrentUser, _ := s.fetchCurrentUser(ctx)\n\t\tif currentUser == nil || currentUser.ID != userID {\n\t\t\treturn nil, status.Errorf(codes.PermissionDenied, \"permission denied\")\n\t\t}\n\t}\n\n\t// Generate PAT\n\ttokenID := util.GenUUID()\n\ttoken := auth.GeneratePersonalAccessToken()\n\ttokenHash := auth.HashPersonalAccessToken(token)\n\n\tvar expiresAt *timestamppb.Timestamp\n\tif request.ExpiresInDays > 0 {\n\t\texpiresAt = timestamppb.New(time.Now().AddDate(0, 0, int(request.ExpiresInDays)))\n\t}\n\n\tpatRecord := &storepb.PersonalAccessTokensUserSetting_PersonalAccessToken{\n\t\tTokenId:     tokenID,\n\t\tTokenHash:   tokenHash,\n\t\tDescription: request.Description,\n\t\tExpiresAt:   expiresAt,\n\t\tCreatedAt:   timestamppb.Now(),\n\t}\n\n\tif err := s.Store.AddUserPersonalAccessToken(ctx, userID, patRecord); err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to create access token: %v\", err)\n\t}\n\n\treturn &v1pb.CreatePersonalAccessTokenResponse{\n\t\tPersonalAccessToken: &v1pb.PersonalAccessToken{\n\t\t\tName:        fmt.Sprintf(\"%s/personalAccessTokens/%s\", request.Parent, tokenID),\n\t\t\tDescription: request.Description,\n\t\t\tExpiresAt:   expiresAt,\n\t\t\tCreatedAt:   patRecord.CreatedAt,\n\t\t},\n\t\tToken: token, // Only returned on creation\n\t}, nil\n}\n\n// DeletePersonalAccessToken revokes a Personal Access Token.\n//\n// This endpoint:\n// 1. Removes the token from the user's access tokens list\n// 2. Immediately invalidates the token (subsequent API calls with it will fail)\n//\n// Use cases:\n// - User revokes a compromised token\n// - User removes token for unused app/device\n// - User cleans up old tokens\n//\n// Authentication: Required (session cookie or access token)\n// Authorization: User can only delete their own tokens.\nfunc (s *APIV1Service) DeletePersonalAccessToken(ctx context.Context, request *v1pb.DeletePersonalAccessTokenRequest) (*emptypb.Empty, error) {\n\t// Parse name: users/{user_id}/personalAccessTokens/{token_id}\n\tparts := strings.Split(request.Name, \"/\")\n\tif len(parts) != 4 || parts[0] != \"users\" || parts[2] != \"personalAccessTokens\" {\n\t\treturn nil, status.Errorf(codes.InvalidArgument, \"invalid personal access token name\")\n\t}\n\n\tuserID, err := util.ConvertStringToInt32(parts[1])\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.InvalidArgument, \"invalid user ID: %v\", err)\n\t}\n\ttokenID := parts[3]\n\n\t// Verify permission\n\tclaims := auth.GetUserClaims(ctx)\n\tif claims == nil || claims.UserID != userID {\n\t\tcurrentUser, _ := s.fetchCurrentUser(ctx)\n\t\tif currentUser == nil || currentUser.ID != userID {\n\t\t\treturn nil, status.Errorf(codes.PermissionDenied, \"permission denied\")\n\t\t}\n\t}\n\n\tif err := s.Store.RemoveUserPersonalAccessToken(ctx, userID, tokenID); err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to delete access token: %v\", err)\n\t}\n\n\treturn &emptypb.Empty{}, nil\n}\n\nfunc (s *APIV1Service) ListUserWebhooks(ctx context.Context, request *v1pb.ListUserWebhooksRequest) (*v1pb.ListUserWebhooksResponse, error) {\n\tuserID, err := ExtractUserIDFromName(request.Parent)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.InvalidArgument, \"invalid parent: %v\", err)\n\t}\n\n\tcurrentUser, err := s.fetchCurrentUser(ctx)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to get current user: %v\", err)\n\t}\n\tif currentUser == nil {\n\t\treturn nil, status.Errorf(codes.Unauthenticated, \"user not authenticated\")\n\t}\n\tif currentUser.ID != userID && currentUser.Role != store.RoleAdmin {\n\t\treturn nil, status.Errorf(codes.PermissionDenied, \"permission denied\")\n\t}\n\n\twebhooks, err := s.Store.GetUserWebhooks(ctx, userID)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to get user webhooks: %v\", err)\n\t}\n\n\tuserWebhooks := make([]*v1pb.UserWebhook, 0, len(webhooks))\n\tfor _, webhook := range webhooks {\n\t\tuserWebhooks = append(userWebhooks, convertUserWebhookFromUserSetting(webhook, userID))\n\t}\n\n\treturn &v1pb.ListUserWebhooksResponse{\n\t\tWebhooks: userWebhooks,\n\t}, nil\n}\n\nfunc (s *APIV1Service) CreateUserWebhook(ctx context.Context, request *v1pb.CreateUserWebhookRequest) (*v1pb.UserWebhook, error) {\n\tuserID, err := ExtractUserIDFromName(request.Parent)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.InvalidArgument, \"invalid parent: %v\", err)\n\t}\n\n\tcurrentUser, err := s.fetchCurrentUser(ctx)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to get current user: %v\", err)\n\t}\n\tif currentUser == nil {\n\t\treturn nil, status.Errorf(codes.Unauthenticated, \"user not authenticated\")\n\t}\n\tif currentUser.ID != userID && currentUser.Role != store.RoleAdmin {\n\t\treturn nil, status.Errorf(codes.PermissionDenied, \"permission denied\")\n\t}\n\n\tif request.Webhook.Url == \"\" {\n\t\treturn nil, status.Errorf(codes.InvalidArgument, \"webhook URL is required\")\n\t}\n\tif err := webhook.ValidateURL(strings.TrimSpace(request.Webhook.Url)); err != nil {\n\t\treturn nil, err\n\t}\n\n\twebhookID := generateUserWebhookID()\n\twebhook := &storepb.WebhooksUserSetting_Webhook{\n\t\tId:    webhookID,\n\t\tTitle: request.Webhook.DisplayName,\n\t\tUrl:   strings.TrimSpace(request.Webhook.Url),\n\t}\n\n\terr = s.Store.AddUserWebhook(ctx, userID, webhook)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to create webhook: %v\", err)\n\t}\n\n\treturn convertUserWebhookFromUserSetting(webhook, userID), nil\n}\n\nfunc (s *APIV1Service) UpdateUserWebhook(ctx context.Context, request *v1pb.UpdateUserWebhookRequest) (*v1pb.UserWebhook, error) {\n\tif request.Webhook == nil {\n\t\treturn nil, status.Errorf(codes.InvalidArgument, \"webhook is required\")\n\t}\n\n\twebhookID, userID, err := parseUserWebhookName(request.Webhook.Name)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.InvalidArgument, \"invalid webhook name: %v\", err)\n\t}\n\n\tcurrentUser, err := s.fetchCurrentUser(ctx)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to get current user: %v\", err)\n\t}\n\tif currentUser == nil {\n\t\treturn nil, status.Errorf(codes.Unauthenticated, \"user not authenticated\")\n\t}\n\tif currentUser.ID != userID && currentUser.Role != store.RoleAdmin {\n\t\treturn nil, status.Errorf(codes.PermissionDenied, \"permission denied\")\n\t}\n\n\t// Get existing webhooks\n\twebhooks, err := s.Store.GetUserWebhooks(ctx, userID)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to get user webhooks: %v\", err)\n\t}\n\n\t// Find the webhook to update\n\tvar targetWebhook *storepb.WebhooksUserSetting_Webhook\n\tfor _, webhook := range webhooks {\n\t\tif webhook.Id == webhookID {\n\t\t\ttargetWebhook = webhook\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif targetWebhook == nil {\n\t\treturn nil, status.Errorf(codes.NotFound, \"webhook not found\")\n\t}\n\n\t// Update the webhook\n\tupdatedWebhook := &storepb.WebhooksUserSetting_Webhook{\n\t\tId:    webhookID,\n\t\tTitle: targetWebhook.Title,\n\t\tUrl:   targetWebhook.Url,\n\t}\n\n\tif request.UpdateMask != nil {\n\t\tfor _, path := range request.UpdateMask.Paths {\n\t\t\tswitch path {\n\t\t\tcase \"url\":\n\t\t\t\tif request.Webhook.Url != \"\" {\n\t\t\t\t\ttrimmed := strings.TrimSpace(request.Webhook.Url)\n\t\t\t\t\tif err := webhook.ValidateURL(trimmed); err != nil {\n\t\t\t\t\t\treturn nil, err\n\t\t\t\t\t}\n\t\t\t\t\tupdatedWebhook.Url = trimmed\n\t\t\t\t}\n\t\t\tcase \"display_name\":\n\t\t\t\tupdatedWebhook.Title = request.Webhook.DisplayName\n\t\t\tdefault:\n\t\t\t\t// Ignore unsupported fields\n\t\t\t}\n\t\t}\n\t} else {\n\t\t// If no update mask is provided, update all fields\n\t\tif request.Webhook.Url != \"\" {\n\t\t\ttrimmed := strings.TrimSpace(request.Webhook.Url)\n\t\t\tif err := webhook.ValidateURL(trimmed); err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tupdatedWebhook.Url = trimmed\n\t\t}\n\t\tupdatedWebhook.Title = request.Webhook.DisplayName\n\t}\n\n\terr = s.Store.UpdateUserWebhook(ctx, userID, updatedWebhook)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to update webhook: %v\", err)\n\t}\n\n\treturn convertUserWebhookFromUserSetting(updatedWebhook, userID), nil\n}\n\nfunc (s *APIV1Service) DeleteUserWebhook(ctx context.Context, request *v1pb.DeleteUserWebhookRequest) (*emptypb.Empty, error) {\n\twebhookID, userID, err := parseUserWebhookName(request.Name)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.InvalidArgument, \"invalid webhook name: %v\", err)\n\t}\n\n\tcurrentUser, err := s.fetchCurrentUser(ctx)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to get current user: %v\", err)\n\t}\n\tif currentUser == nil {\n\t\treturn nil, status.Errorf(codes.Unauthenticated, \"user not authenticated\")\n\t}\n\tif currentUser.ID != userID && currentUser.Role != store.RoleAdmin {\n\t\treturn nil, status.Errorf(codes.PermissionDenied, \"permission denied\")\n\t}\n\n\t// Get existing webhooks to verify the webhook exists\n\twebhooks, err := s.Store.GetUserWebhooks(ctx, userID)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to get user webhooks: %v\", err)\n\t}\n\n\t// Check if webhook exists\n\tfound := false\n\tfor _, webhook := range webhooks {\n\t\tif webhook.Id == webhookID {\n\t\t\tfound = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif !found {\n\t\treturn nil, status.Errorf(codes.NotFound, \"webhook not found\")\n\t}\n\n\terr = s.Store.RemoveUserWebhook(ctx, userID, webhookID)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to delete webhook: %v\", err)\n\t}\n\n\treturn &emptypb.Empty{}, nil\n}\n\n// Helper functions for webhook operations\n\n// generateUserWebhookID generates a unique ID for user webhooks.\nfunc generateUserWebhookID() string {\n\tb := make([]byte, 8)\n\trand.Read(b)\n\treturn hex.EncodeToString(b)\n}\n\n// parseUserWebhookName parses a webhook name and returns the webhook ID and user ID.\n// Format: users/{user}/webhooks/{webhook}.\nfunc parseUserWebhookName(name string) (string, int32, error) {\n\tparts := strings.Split(name, \"/\")\n\tif len(parts) != 4 || parts[0] != \"users\" || parts[2] != \"webhooks\" {\n\t\treturn \"\", 0, errors.New(\"invalid webhook name format\")\n\t}\n\n\tuserID, err := strconv.ParseInt(parts[1], 10, 32)\n\tif err != nil {\n\t\treturn \"\", 0, errors.New(\"invalid user ID in webhook name\")\n\t}\n\n\treturn parts[3], int32(userID), nil\n}\n\n// convertUserWebhookFromUserSetting converts a storepb webhook to a v1pb UserWebhook.\nfunc convertUserWebhookFromUserSetting(webhook *storepb.WebhooksUserSetting_Webhook, userID int32) *v1pb.UserWebhook {\n\treturn &v1pb.UserWebhook{\n\t\tName:        fmt.Sprintf(\"users/%d/webhooks/%s\", userID, webhook.Id),\n\t\tUrl:         webhook.Url,\n\t\tDisplayName: webhook.Title,\n\t\t// Note: create_time and update_time are not available in the user setting webhook structure\n\t\t// This is a limitation of storing webhooks in user settings vs the dedicated webhook table\n\t}\n}\n\nfunc convertUserFromStore(user *store.User) *v1pb.User {\n\tuserpb := &v1pb.User{\n\t\tName:        fmt.Sprintf(\"%s%d\", UserNamePrefix, user.ID),\n\t\tState:       convertStateFromStore(user.RowStatus),\n\t\tCreateTime:  timestamppb.New(time.Unix(user.CreatedTs, 0)),\n\t\tUpdateTime:  timestamppb.New(time.Unix(user.UpdatedTs, 0)),\n\t\tRole:        convertUserRoleFromStore(user.Role),\n\t\tUsername:    user.Username,\n\t\tEmail:       user.Email,\n\t\tDisplayName: user.Nickname,\n\t\tAvatarUrl:   user.AvatarURL,\n\t\tDescription: user.Description,\n\t}\n\t// Use the avatar URL instead of raw base64 image data to reduce the response size.\n\tif user.AvatarURL != \"\" {\n\t\t// Check if avatar url is base64 format.\n\t\t_, _, err := extractImageInfo(user.AvatarURL)\n\t\tif err == nil {\n\t\t\tuserpb.AvatarUrl = fmt.Sprintf(\"/file/%s/avatar\", userpb.Name)\n\t\t} else {\n\t\t\tuserpb.AvatarUrl = user.AvatarURL\n\t\t}\n\t}\n\treturn userpb\n}\n\nfunc convertUserRoleFromStore(role store.Role) v1pb.User_Role {\n\tswitch role {\n\tcase store.RoleAdmin:\n\t\treturn v1pb.User_ADMIN\n\tcase store.RoleUser:\n\t\treturn v1pb.User_USER\n\tdefault:\n\t\treturn v1pb.User_ROLE_UNSPECIFIED\n\t}\n}\n\nfunc convertUserRoleToStore(role v1pb.User_Role) store.Role {\n\tswitch role {\n\tcase v1pb.User_ADMIN:\n\t\treturn store.RoleAdmin\n\tdefault:\n\t\treturn store.RoleUser\n\t}\n}\n\n// extractImageInfo extracts image type and base64 data from a data URI.\n// Data URI format: data:image/png;base64,iVBORw0KGgo...\nfunc extractImageInfo(dataURI string) (string, string, error) {\n\tdataURIRegex := regexp.MustCompile(`^data:(?P<type>.+);base64,(?P<base64>.+)`)\n\tmatches := dataURIRegex.FindStringSubmatch(dataURI)\n\tif len(matches) != 3 {\n\t\treturn \"\", \"\", errors.New(\"invalid data URI format\")\n\t}\n\timageType := matches[1]\n\tbase64Data := matches[2]\n\treturn imageType, base64Data, nil\n}\n\n// Helper functions for user settings\n\n// ExtractUserIDAndSettingKeyFromName extracts user ID and setting key from resource name.\n// e.g., \"users/123/settings/general\" -> 123, \"general\".\nfunc ExtractUserIDAndSettingKeyFromName(name string) (int32, string, error) {\n\t// Expected format: users/{user}/settings/{setting}\n\tparts := strings.Split(name, \"/\")\n\tif len(parts) != 4 || parts[0] != \"users\" || parts[2] != \"settings\" {\n\t\treturn 0, \"\", errors.Errorf(\"invalid resource name format: %s\", name)\n\t}\n\n\tuserID, err := util.ConvertStringToInt32(parts[1])\n\tif err != nil {\n\t\treturn 0, \"\", errors.Errorf(\"invalid user ID: %s\", parts[1])\n\t}\n\n\tsettingKey := parts[3]\n\treturn userID, settingKey, nil\n}\n\n// convertSettingKeyToStore converts API setting key to store enum.\nfunc convertSettingKeyToStore(key string) (storepb.UserSetting_Key, error) {\n\tswitch key {\n\tcase v1pb.UserSetting_Key_name[int32(v1pb.UserSetting_GENERAL)]:\n\t\treturn storepb.UserSetting_GENERAL, nil\n\tcase v1pb.UserSetting_Key_name[int32(v1pb.UserSetting_WEBHOOKS)]:\n\t\treturn storepb.UserSetting_WEBHOOKS, nil\n\tdefault:\n\t\treturn storepb.UserSetting_KEY_UNSPECIFIED, errors.Errorf(\"unknown setting key: %s\", key)\n\t}\n}\n\n// convertSettingKeyFromStore converts store enum to API setting key.\nfunc convertSettingKeyFromStore(key storepb.UserSetting_Key) string {\n\tswitch key {\n\tcase storepb.UserSetting_GENERAL:\n\t\treturn v1pb.UserSetting_Key_name[int32(v1pb.UserSetting_GENERAL)]\n\tcase storepb.UserSetting_SHORTCUTS:\n\t\treturn \"SHORTCUTS\" // Not defined in API proto\n\tcase storepb.UserSetting_WEBHOOKS:\n\t\treturn v1pb.UserSetting_Key_name[int32(v1pb.UserSetting_WEBHOOKS)]\n\tdefault:\n\t\treturn \"unknown\"\n\t}\n}\n\n// convertUserSettingFromStore converts store UserSetting to API UserSetting.\nfunc convertUserSettingFromStore(storeSetting *storepb.UserSetting, userID int32, key storepb.UserSetting_Key) *v1pb.UserSetting {\n\tif storeSetting == nil {\n\t\t// Return default setting if none exists\n\t\tsettingKey := convertSettingKeyFromStore(key)\n\t\tsetting := &v1pb.UserSetting{\n\t\t\tName: fmt.Sprintf(\"users/%d/settings/%s\", userID, settingKey),\n\t\t}\n\n\t\tswitch key {\n\t\tcase storepb.UserSetting_WEBHOOKS:\n\t\t\tsetting.Value = &v1pb.UserSetting_WebhooksSetting_{\n\t\t\t\tWebhooksSetting: &v1pb.UserSetting_WebhooksSetting{\n\t\t\t\t\tWebhooks: []*v1pb.UserWebhook{},\n\t\t\t\t},\n\t\t\t}\n\t\tdefault:\n\t\t\t// Default to general setting\n\t\t\tsetting.Value = &v1pb.UserSetting_GeneralSetting_{\n\t\t\t\tGeneralSetting: getDefaultUserGeneralSetting(),\n\t\t\t}\n\t\t}\n\t\treturn setting\n\t}\n\n\tsettingKey := convertSettingKeyFromStore(storeSetting.Key)\n\tsetting := &v1pb.UserSetting{\n\t\tName: fmt.Sprintf(\"users/%d/settings/%s\", userID, settingKey),\n\t}\n\n\tswitch storeSetting.Key {\n\tcase storepb.UserSetting_GENERAL:\n\t\tif general := storeSetting.GetGeneral(); general != nil {\n\t\t\tsetting.Value = &v1pb.UserSetting_GeneralSetting_{\n\t\t\t\tGeneralSetting: &v1pb.UserSetting_GeneralSetting{\n\t\t\t\t\tLocale:         general.Locale,\n\t\t\t\t\tMemoVisibility: general.MemoVisibility,\n\t\t\t\t\tTheme:          general.Theme,\n\t\t\t\t},\n\t\t\t}\n\t\t} else {\n\t\t\tsetting.Value = &v1pb.UserSetting_GeneralSetting_{\n\t\t\t\tGeneralSetting: getDefaultUserGeneralSetting(),\n\t\t\t}\n\t\t}\n\tcase storepb.UserSetting_WEBHOOKS:\n\t\twebhooks := storeSetting.GetWebhooks()\n\t\tapiWebhooks := make([]*v1pb.UserWebhook, 0, len(webhooks.Webhooks))\n\t\tfor _, webhook := range webhooks.Webhooks {\n\t\t\tapiWebhook := &v1pb.UserWebhook{\n\t\t\t\tName:        fmt.Sprintf(\"users/%d/webhooks/%s\", userID, webhook.Id),\n\t\t\t\tUrl:         webhook.Url,\n\t\t\t\tDisplayName: webhook.Title,\n\t\t\t}\n\t\t\tapiWebhooks = append(apiWebhooks, apiWebhook)\n\t\t}\n\t\tsetting.Value = &v1pb.UserSetting_WebhooksSetting_{\n\t\t\tWebhooksSetting: &v1pb.UserSetting_WebhooksSetting{\n\t\t\t\tWebhooks: apiWebhooks,\n\t\t\t},\n\t\t}\n\tdefault:\n\t\t// Default to general setting if unknown key\n\t\tsetting.Value = &v1pb.UserSetting_GeneralSetting_{\n\t\t\tGeneralSetting: getDefaultUserGeneralSetting(),\n\t\t}\n\t}\n\n\treturn setting\n}\n\n// convertUserSettingToStore converts API UserSetting to store UserSetting.\nfunc convertUserSettingToStore(apiSetting *v1pb.UserSetting, userID int32, key storepb.UserSetting_Key) (*storepb.UserSetting, error) {\n\tstoreSetting := &storepb.UserSetting{\n\t\tUserId: userID,\n\t\tKey:    key,\n\t}\n\n\tswitch key {\n\tcase storepb.UserSetting_GENERAL:\n\t\tif general := apiSetting.GetGeneralSetting(); general != nil {\n\t\t\tstoreSetting.Value = &storepb.UserSetting_General{\n\t\t\t\tGeneral: &storepb.GeneralUserSetting{\n\t\t\t\t\tLocale:         general.Locale,\n\t\t\t\t\tMemoVisibility: general.MemoVisibility,\n\t\t\t\t\tTheme:          general.Theme,\n\t\t\t\t},\n\t\t\t}\n\t\t} else {\n\t\t\treturn nil, errors.Errorf(\"general setting is required\")\n\t\t}\n\tcase storepb.UserSetting_WEBHOOKS:\n\t\tif webhooks := apiSetting.GetWebhooksSetting(); webhooks != nil {\n\t\t\tstoreWebhooks := make([]*storepb.WebhooksUserSetting_Webhook, 0, len(webhooks.Webhooks))\n\t\t\tfor _, webhook := range webhooks.Webhooks {\n\t\t\t\tstoreWebhook := &storepb.WebhooksUserSetting_Webhook{\n\t\t\t\t\tId:    extractWebhookIDFromName(webhook.Name),\n\t\t\t\t\tTitle: webhook.DisplayName,\n\t\t\t\t\tUrl:   webhook.Url,\n\t\t\t\t}\n\t\t\t\tstoreWebhooks = append(storeWebhooks, storeWebhook)\n\t\t\t}\n\t\t\tstoreSetting.Value = &storepb.UserSetting_Webhooks{\n\t\t\t\tWebhooks: &storepb.WebhooksUserSetting{\n\t\t\t\t\tWebhooks: storeWebhooks,\n\t\t\t\t},\n\t\t\t}\n\t\t} else {\n\t\t\treturn nil, errors.Errorf(\"webhooks setting is required\")\n\t\t}\n\tdefault:\n\t\treturn nil, errors.Errorf(\"unsupported setting key: %v\", key)\n\t}\n\n\treturn storeSetting, nil\n}\n\n// extractWebhookIDFromName extracts webhook ID from resource name.\n// e.g., \"users/123/webhooks/webhook-id\" -> \"webhook-id\".\nfunc extractWebhookIDFromName(name string) string {\n\tparts := strings.Split(name, \"/\")\n\tif len(parts) >= 4 && parts[0] == \"users\" && parts[2] == \"webhooks\" {\n\t\treturn parts[3]\n\t}\n\treturn \"\"\n}\n\n// extractUsernameFromFilter extracts username from the filter string using CEL.\n// Supported filter format: \"username == 'steven'\"\n// Returns the username value and an error if the filter format is invalid.\nfunc extractUsernameFromFilter(filterStr string) (string, error) {\n\tfilterStr = strings.TrimSpace(filterStr)\n\tif filterStr == \"\" {\n\t\treturn \"\", nil\n\t}\n\n\t// Create CEL environment with username variable\n\tenv, err := cel.NewEnv(\n\t\tcel.Variable(\"username\", cel.StringType),\n\t)\n\tif err != nil {\n\t\treturn \"\", errors.Wrap(err, \"failed to create CEL environment\")\n\t}\n\n\t// Parse and check the filter expression\n\tcelAST, issues := env.Compile(filterStr)\n\tif issues != nil && issues.Err() != nil {\n\t\treturn \"\", errors.Wrapf(issues.Err(), \"invalid filter expression: %s\", filterStr)\n\t}\n\n\t// Extract username from the AST\n\tusername, err := extractUsernameFromAST(celAST.NativeRep().Expr())\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn username, nil\n}\n\n// extractUsernameFromAST extracts the username value from a CEL AST expression.\nfunc extractUsernameFromAST(expr ast.Expr) (string, error) {\n\tif expr == nil {\n\t\treturn \"\", errors.New(\"empty expression\")\n\t}\n\n\t// Check if this is a call expression (for ==, !=, etc.)\n\tif expr.Kind() != ast.CallKind {\n\t\treturn \"\", errors.New(\"filter must be a comparison expression (e.g., username == 'value')\")\n\t}\n\n\tcall := expr.AsCall()\n\n\t// We only support == operator\n\tif call.FunctionName() != \"_==_\" {\n\t\treturn \"\", errors.Errorf(\"unsupported operator: %s (only '==' is supported)\", call.FunctionName())\n\t}\n\n\t// The call should have exactly 2 arguments\n\targs := call.Args()\n\tif len(args) != 2 {\n\t\treturn \"\", errors.New(\"invalid comparison expression\")\n\t}\n\n\t// Try to extract username from either left or right side\n\tif username, ok := extractUsernameFromComparison(args[0], args[1]); ok {\n\t\treturn username, nil\n\t}\n\tif username, ok := extractUsernameFromComparison(args[1], args[0]); ok {\n\t\treturn username, nil\n\t}\n\n\treturn \"\", errors.New(\"filter must compare 'username' field with a string constant\")\n}\n\n// extractUsernameFromComparison tries to extract username value if left is 'username' ident and right is a string constant.\nfunc extractUsernameFromComparison(left, right ast.Expr) (string, bool) {\n\t// Check if left side is 'username' identifier\n\tif left.Kind() != ast.IdentKind {\n\t\treturn \"\", false\n\t}\n\tident := left.AsIdent()\n\tif ident != \"username\" {\n\t\treturn \"\", false\n\t}\n\n\t// Right side should be a constant string\n\tif right.Kind() != ast.LiteralKind {\n\t\treturn \"\", false\n\t}\n\tliteral := right.AsLiteral()\n\n\t// literal is a ref.Val, we need to get the Go value\n\tstr, ok := literal.Value().(string)\n\tif !ok || str == \"\" {\n\t\treturn \"\", false\n\t}\n\n\treturn str, true\n}\n\n// ListUserNotifications lists all notifications for a user.\n// Notifications are backed by the inbox storage layer and represent activities\n// that require user attention (e.g., memo comments).\nfunc (s *APIV1Service) ListUserNotifications(ctx context.Context, request *v1pb.ListUserNotificationsRequest) (*v1pb.ListUserNotificationsResponse, error) {\n\tuserID, err := ExtractUserIDFromName(request.Parent)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.InvalidArgument, \"invalid user name: %v\", err)\n\t}\n\n\t// Verify the requesting user has permission to view these notifications\n\tcurrentUser, err := s.fetchCurrentUser(ctx)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to get current user: %v\", err)\n\t}\n\tif currentUser == nil {\n\t\treturn nil, status.Errorf(codes.Unauthenticated, \"user not authenticated\")\n\t}\n\tif currentUser.ID != userID {\n\t\treturn nil, status.Errorf(codes.PermissionDenied, \"permission denied\")\n\t}\n\n\t// Fetch inbox items from storage\n\t// Filter at database level to only include MEMO_COMMENT notifications (ignore legacy VERSION_UPDATE entries)\n\tmemoCommentType := storepb.InboxMessage_MEMO_COMMENT\n\tinboxes, err := s.Store.ListInboxes(ctx, &store.FindInbox{\n\t\tReceiverID:  &userID,\n\t\tMessageType: &memoCommentType,\n\t})\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to list inboxes: %v\", err)\n\t}\n\n\t// Convert storage layer inboxes to API notifications\n\tnotifications := []*v1pb.UserNotification{}\n\tfor _, inbox := range inboxes {\n\t\tnotification, err := s.convertInboxToUserNotification(ctx, inbox)\n\t\tif err != nil {\n\t\t\treturn nil, status.Errorf(codes.Internal, \"failed to convert inbox: %v\", err)\n\t\t}\n\t\tnotifications = append(notifications, notification)\n\t}\n\n\treturn &v1pb.ListUserNotificationsResponse{\n\t\tNotifications: notifications,\n\t}, nil\n}\n\n// UpdateUserNotification updates a notification's status (e.g., marking as read/archived).\n// Only the notification owner can update their notifications.\nfunc (s *APIV1Service) UpdateUserNotification(ctx context.Context, request *v1pb.UpdateUserNotificationRequest) (*v1pb.UserNotification, error) {\n\tif request.Notification == nil {\n\t\treturn nil, status.Errorf(codes.InvalidArgument, \"notification is required\")\n\t}\n\n\tnotificationID, err := ExtractNotificationIDFromName(request.Notification.Name)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.InvalidArgument, \"invalid notification name: %v\", err)\n\t}\n\n\tcurrentUser, err := s.fetchCurrentUser(ctx)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to get current user: %v\", err)\n\t}\n\n\tif currentUser == nil {\n\t\treturn nil, status.Errorf(codes.Unauthenticated, \"user not authenticated\")\n\t}\n\t// Verify ownership before updating\n\tinboxes, err := s.Store.ListInboxes(ctx, &store.FindInbox{\n\t\tID: &notificationID,\n\t})\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to get inbox: %v\", err)\n\t}\n\tif len(inboxes) == 0 {\n\t\treturn nil, status.Errorf(codes.NotFound, \"notification not found\")\n\t}\n\tinbox := inboxes[0]\n\tif inbox.ReceiverID != currentUser.ID {\n\t\treturn nil, status.Errorf(codes.PermissionDenied, \"permission denied\")\n\t}\n\n\t// Build update request based on field mask\n\tupdate := &store.UpdateInbox{\n\t\tID: notificationID,\n\t}\n\n\tfor _, path := range request.UpdateMask.Paths {\n\t\tswitch path {\n\t\tcase \"status\":\n\t\t\t// Convert API status enum to storage enum\n\t\t\tvar inboxStatus store.InboxStatus\n\t\t\tswitch request.Notification.Status {\n\t\t\tcase v1pb.UserNotification_UNREAD:\n\t\t\t\tinboxStatus = store.UNREAD\n\t\t\tcase v1pb.UserNotification_ARCHIVED:\n\t\t\t\tinboxStatus = store.ARCHIVED\n\t\t\tdefault:\n\t\t\t\treturn nil, status.Errorf(codes.InvalidArgument, \"invalid status\")\n\t\t\t}\n\t\t\tupdate.Status = inboxStatus\n\t\tdefault:\n\t\t\treturn nil, status.Errorf(codes.InvalidArgument, \"invalid update path: %s\", path)\n\t\t}\n\t}\n\n\tupdatedInbox, err := s.Store.UpdateInbox(ctx, update)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to update inbox: %v\", err)\n\t}\n\n\tnotification, err := s.convertInboxToUserNotification(ctx, updatedInbox)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to convert inbox: %v\", err)\n\t}\n\n\treturn notification, nil\n}\n\n// DeleteUserNotification permanently deletes a notification.\n// Only the notification owner can delete their notifications.\nfunc (s *APIV1Service) DeleteUserNotification(ctx context.Context, request *v1pb.DeleteUserNotificationRequest) (*emptypb.Empty, error) {\n\tnotificationID, err := ExtractNotificationIDFromName(request.Name)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.InvalidArgument, \"invalid notification name: %v\", err)\n\t}\n\n\tcurrentUser, err := s.fetchCurrentUser(ctx)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to get current user: %v\", err)\n\t}\n\n\tif currentUser == nil {\n\t\treturn nil, status.Errorf(codes.Unauthenticated, \"user not authenticated\")\n\t}\n\t// Verify ownership before deletion\n\tinboxes, err := s.Store.ListInboxes(ctx, &store.FindInbox{\n\t\tID: &notificationID,\n\t})\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to get inbox: %v\", err)\n\t}\n\tif len(inboxes) == 0 {\n\t\treturn nil, status.Errorf(codes.NotFound, \"notification not found\")\n\t}\n\tinbox := inboxes[0]\n\tif inbox.ReceiverID != currentUser.ID {\n\t\treturn nil, status.Errorf(codes.PermissionDenied, \"permission denied\")\n\t}\n\n\tif err := s.Store.DeleteInbox(ctx, &store.DeleteInbox{\n\t\tID: notificationID,\n\t}); err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to delete inbox: %v\", err)\n\t}\n\n\treturn &emptypb.Empty{}, nil\n}\n\n// convertInboxToUserNotification converts a storage-layer inbox to an API notification.\n// This handles the mapping between the internal inbox representation and the public API.\nfunc (s *APIV1Service) convertInboxToUserNotification(ctx context.Context, inbox *store.Inbox) (*v1pb.UserNotification, error) {\n\tnotification := &v1pb.UserNotification{\n\t\tName:       fmt.Sprintf(\"users/%d/notifications/%d\", inbox.ReceiverID, inbox.ID),\n\t\tSender:     fmt.Sprintf(\"%s%d\", UserNamePrefix, inbox.SenderID),\n\t\tCreateTime: timestamppb.New(time.Unix(inbox.CreatedTs, 0)),\n\t}\n\n\t// Convert status from storage enum to API enum\n\tswitch inbox.Status {\n\tcase store.UNREAD:\n\t\tnotification.Status = v1pb.UserNotification_UNREAD\n\tcase store.ARCHIVED:\n\t\tnotification.Status = v1pb.UserNotification_ARCHIVED\n\tdefault:\n\t\tnotification.Status = v1pb.UserNotification_STATUS_UNSPECIFIED\n\t}\n\n\t// Extract notification type and payload from the inbox message.\n\tif inbox.Message != nil {\n\t\tswitch inbox.Message.Type {\n\t\tcase storepb.InboxMessage_MEMO_COMMENT:\n\t\t\tnotification.Type = v1pb.UserNotification_MEMO_COMMENT\n\t\tdefault:\n\t\t\tnotification.Type = v1pb.UserNotification_TYPE_UNSPECIFIED\n\t\t}\n\n\t\tpayload, err := s.convertUserNotificationPayload(ctx, inbox.Message)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif payload != nil {\n\t\t\tnotification.Payload = &v1pb.UserNotification_MemoComment{\n\t\t\t\tMemoComment: payload,\n\t\t\t}\n\t\t}\n\t}\n\n\treturn notification, nil\n}\n\nfunc (s *APIV1Service) convertUserNotificationPayload(ctx context.Context, message *storepb.InboxMessage) (*v1pb.UserNotification_MemoCommentPayload, error) {\n\tmemoComment := message.GetMemoComment()\n\tif message == nil || message.Type != storepb.InboxMessage_MEMO_COMMENT || memoComment == nil {\n\t\treturn nil, nil\n\t}\n\n\tcommentMemo, err := s.Store.GetMemo(ctx, &store.FindMemo{\n\t\tID:             &memoComment.MemoId,\n\t\tExcludeContent: true,\n\t})\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"failed to get comment memo\")\n\t}\n\tif commentMemo == nil {\n\t\treturn nil, nil\n\t}\n\n\trelatedMemo, err := s.Store.GetMemo(ctx, &store.FindMemo{\n\t\tID:             &memoComment.RelatedMemoId,\n\t\tExcludeContent: true,\n\t})\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"failed to get related memo\")\n\t}\n\tif relatedMemo == nil {\n\t\treturn nil, nil\n\t}\n\n\treturn &v1pb.UserNotification_MemoCommentPayload{\n\t\tMemo:        fmt.Sprintf(\"%s%s\", MemoNamePrefix, commentMemo.UID),\n\t\tRelatedMemo: fmt.Sprintf(\"%s%s\", MemoNamePrefix, relatedMemo.UID),\n\t}, nil\n}\n\n// ExtractNotificationIDFromName extracts the notification ID from a resource name.\n// Expected format: users/{user_id}/notifications/{notification_id}.\nfunc ExtractNotificationIDFromName(name string) (int32, error) {\n\tpattern := regexp.MustCompile(`^users/(\\d+)/notifications/(\\d+)$`)\n\tmatches := pattern.FindStringSubmatch(name)\n\tif len(matches) != 3 {\n\t\treturn 0, errors.Errorf(\"invalid notification name: %s\", name)\n\t}\n\n\tid, err := strconv.Atoi(matches[2])\n\tif err != nil {\n\t\treturn 0, errors.Errorf(\"invalid notification id: %s\", matches[2])\n\t}\n\n\treturn int32(id), nil\n}\n"
  },
  {
    "path": "server/router/api/v1/user_service_stats.go",
    "content": "package v1\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/pkg/errors\"\n\t\"google.golang.org/grpc/codes\"\n\t\"google.golang.org/grpc/status\"\n\t\"google.golang.org/protobuf/types/known/timestamppb\"\n\n\tv1pb \"github.com/usememos/memos/proto/gen/api/v1\"\n\t\"github.com/usememos/memos/store\"\n)\n\nfunc (s *APIV1Service) ListAllUserStats(ctx context.Context, _ *v1pb.ListAllUserStatsRequest) (*v1pb.ListAllUserStatsResponse, error) {\n\tinstanceMemoRelatedSetting, err := s.Store.GetInstanceMemoRelatedSetting(ctx)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"failed to get instance memo related setting\")\n\t}\n\n\tnormalStatus := store.Normal\n\tmemoFind := &store.FindMemo{\n\t\t// Exclude comments by default.\n\t\tExcludeComments: true,\n\t\tExcludeContent:  true,\n\t\tRowStatus:       &normalStatus,\n\t}\n\n\tcurrentUser, err := s.fetchCurrentUser(ctx)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to get user: %v\", err)\n\t}\n\tif currentUser == nil {\n\t\tmemoFind.VisibilityList = []store.Visibility{store.Public}\n\t} else {\n\t\tif memoFind.CreatorID == nil {\n\t\t\tfilter := fmt.Sprintf(`creator_id == %d || visibility in [\"PUBLIC\", \"PROTECTED\"]`, currentUser.ID)\n\t\t\tmemoFind.Filters = append(memoFind.Filters, filter)\n\t\t} else if *memoFind.CreatorID != currentUser.ID {\n\t\t\tmemoFind.VisibilityList = []store.Visibility{store.Public, store.Protected}\n\t\t}\n\t}\n\n\tuserMemoStatMap := make(map[int32]*v1pb.UserStats)\n\tlimit := 1000\n\toffset := 0\n\tmemoFind.Limit = &limit\n\tmemoFind.Offset = &offset\n\n\tfor {\n\t\tmemos, err := s.Store.ListMemos(ctx, memoFind)\n\t\tif err != nil {\n\t\t\treturn nil, status.Errorf(codes.Internal, \"failed to list memos: %v\", err)\n\t\t}\n\t\tif len(memos) == 0 {\n\t\t\tbreak\n\t\t}\n\n\t\tfor _, memo := range memos {\n\t\t\t// Initialize user stats if not exists\n\t\t\tif _, exists := userMemoStatMap[memo.CreatorID]; !exists {\n\t\t\t\tuserMemoStatMap[memo.CreatorID] = &v1pb.UserStats{\n\t\t\t\t\tName:                  fmt.Sprintf(\"users/%d/stats\", memo.CreatorID),\n\t\t\t\t\tTagCount:              make(map[string]int32),\n\t\t\t\t\tMemoDisplayTimestamps: []*timestamppb.Timestamp{},\n\t\t\t\t\tPinnedMemos:           []string{},\n\t\t\t\t\tMemoTypeStats: &v1pb.UserStats_MemoTypeStats{\n\t\t\t\t\t\tLinkCount: 0,\n\t\t\t\t\t\tCodeCount: 0,\n\t\t\t\t\t\tTodoCount: 0,\n\t\t\t\t\t\tUndoCount: 0,\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tstats := userMemoStatMap[memo.CreatorID]\n\n\t\t\t// Add display timestamp\n\t\t\tdisplayTs := memo.CreatedTs\n\t\t\tif instanceMemoRelatedSetting.DisplayWithUpdateTime {\n\t\t\t\tdisplayTs = memo.UpdatedTs\n\t\t\t}\n\t\t\tstats.MemoDisplayTimestamps = append(stats.MemoDisplayTimestamps, timestamppb.New(time.Unix(displayTs, 0)))\n\n\t\t\t// Count memo stats\n\t\t\tstats.TotalMemoCount++\n\n\t\t\t// Count tags and other properties\n\t\t\tif memo.Payload != nil {\n\t\t\t\tfor _, tag := range memo.Payload.Tags {\n\t\t\t\t\tstats.TagCount[tag]++\n\t\t\t\t}\n\t\t\t\tif memo.Payload.Property != nil {\n\t\t\t\t\tif memo.Payload.Property.HasLink {\n\t\t\t\t\t\tstats.MemoTypeStats.LinkCount++\n\t\t\t\t\t}\n\t\t\t\t\tif memo.Payload.Property.HasCode {\n\t\t\t\t\t\tstats.MemoTypeStats.CodeCount++\n\t\t\t\t\t}\n\t\t\t\t\tif memo.Payload.Property.HasTaskList {\n\t\t\t\t\t\tstats.MemoTypeStats.TodoCount++\n\t\t\t\t\t}\n\t\t\t\t\tif memo.Payload.Property.HasIncompleteTasks {\n\t\t\t\t\t\tstats.MemoTypeStats.UndoCount++\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Track pinned memos\n\t\t\tif memo.Pinned {\n\t\t\t\tstats.PinnedMemos = append(stats.PinnedMemos, fmt.Sprintf(\"users/%d/memos/%d\", memo.CreatorID, memo.ID))\n\t\t\t}\n\t\t}\n\n\t\toffset += limit\n\t}\n\n\tuserMemoStats := []*v1pb.UserStats{}\n\tfor _, userMemoStat := range userMemoStatMap {\n\t\tuserMemoStats = append(userMemoStats, userMemoStat)\n\t}\n\n\tresponse := &v1pb.ListAllUserStatsResponse{\n\t\tStats: userMemoStats,\n\t}\n\treturn response, nil\n}\n\nfunc (s *APIV1Service) GetUserStats(ctx context.Context, request *v1pb.GetUserStatsRequest) (*v1pb.UserStats, error) {\n\tuserID, err := ExtractUserIDFromName(request.Name)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.InvalidArgument, \"invalid user name: %v\", err)\n\t}\n\n\tcurrentUser, err := s.fetchCurrentUser(ctx)\n\tif err != nil {\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to get user: %v\", err)\n\t}\n\n\tnormalStatus := store.Normal\n\tmemoFind := &store.FindMemo{\n\t\tCreatorID: &userID,\n\t\t// Exclude comments by default.\n\t\tExcludeComments: true,\n\t\tExcludeContent:  true,\n\t\tRowStatus:       &normalStatus,\n\t}\n\n\tif currentUser == nil {\n\t\tmemoFind.VisibilityList = []store.Visibility{store.Public}\n\t} else if currentUser.ID != userID {\n\t\tmemoFind.VisibilityList = []store.Visibility{store.Public, store.Protected}\n\t}\n\n\tinstanceMemoRelatedSetting, err := s.Store.GetInstanceMemoRelatedSetting(ctx)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"failed to get instance memo related setting\")\n\t}\n\n\tdisplayTimestamps := []*timestamppb.Timestamp{}\n\ttagCount := make(map[string]int32)\n\tlinkCount := int32(0)\n\tcodeCount := int32(0)\n\ttodoCount := int32(0)\n\tundoCount := int32(0)\n\tpinnedMemos := []string{}\n\ttotalMemoCount := int32(0)\n\n\tlimit := 1000\n\toffset := 0\n\tmemoFind.Limit = &limit\n\tmemoFind.Offset = &offset\n\n\tfor {\n\t\tmemos, err := s.Store.ListMemos(ctx, memoFind)\n\t\tif err != nil {\n\t\t\treturn nil, status.Errorf(codes.Internal, \"failed to list memos: %v\", err)\n\t\t}\n\t\tif len(memos) == 0 {\n\t\t\tbreak\n\t\t}\n\n\t\ttotalMemoCount += int32(len(memos))\n\n\t\tfor _, memo := range memos {\n\t\t\tdisplayTs := memo.CreatedTs\n\t\t\tif instanceMemoRelatedSetting.DisplayWithUpdateTime {\n\t\t\t\tdisplayTs = memo.UpdatedTs\n\t\t\t}\n\t\t\tdisplayTimestamps = append(displayTimestamps, timestamppb.New(time.Unix(displayTs, 0)))\n\t\t\t// Count different memo types based on content.\n\t\t\tif memo.Payload != nil {\n\t\t\t\tfor _, tag := range memo.Payload.Tags {\n\t\t\t\t\ttagCount[tag]++\n\t\t\t\t}\n\t\t\t\tif memo.Payload.Property != nil {\n\t\t\t\t\tif memo.Payload.Property.HasLink {\n\t\t\t\t\t\tlinkCount++\n\t\t\t\t\t}\n\t\t\t\t\tif memo.Payload.Property.HasCode {\n\t\t\t\t\t\tcodeCount++\n\t\t\t\t\t}\n\t\t\t\t\tif memo.Payload.Property.HasTaskList {\n\t\t\t\t\t\ttodoCount++\n\t\t\t\t\t}\n\t\t\t\t\tif memo.Payload.Property.HasIncompleteTasks {\n\t\t\t\t\t\tundoCount++\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tif memo.Pinned {\n\t\t\t\tpinnedMemos = append(pinnedMemos, fmt.Sprintf(\"users/%d/memos/%d\", userID, memo.ID))\n\t\t\t}\n\t\t}\n\n\t\toffset += limit\n\t}\n\n\tuserStats := &v1pb.UserStats{\n\t\tName:                  fmt.Sprintf(\"users/%d/stats\", userID),\n\t\tMemoDisplayTimestamps: displayTimestamps,\n\t\tTagCount:              tagCount,\n\t\tPinnedMemos:           pinnedMemos,\n\t\tTotalMemoCount:        totalMemoCount,\n\t\tMemoTypeStats: &v1pb.UserStats_MemoTypeStats{\n\t\t\tLinkCount: linkCount,\n\t\t\tCodeCount: codeCount,\n\t\t\tTodoCount: todoCount,\n\t\t\tUndoCount: undoCount,\n\t\t},\n\t}\n\n\treturn userStats, nil\n}\n"
  },
  {
    "path": "server/router/api/v1/v1.go",
    "content": "package v1\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\n\t\"connectrpc.com/connect\"\n\t\"github.com/grpc-ecosystem/grpc-gateway/v2/runtime\"\n\t\"github.com/labstack/echo/v5\"\n\t\"github.com/labstack/echo/v5/middleware\"\n\t\"golang.org/x/sync/semaphore\"\n\n\t\"github.com/usememos/memos/internal/profile\"\n\t\"github.com/usememos/memos/plugin/markdown\"\n\tv1pb \"github.com/usememos/memos/proto/gen/api/v1\"\n\t\"github.com/usememos/memos/server/auth\"\n\t\"github.com/usememos/memos/store\"\n)\n\ntype APIV1Service struct {\n\tv1pb.UnimplementedInstanceServiceServer\n\tv1pb.UnimplementedAuthServiceServer\n\tv1pb.UnimplementedUserServiceServer\n\tv1pb.UnimplementedMemoServiceServer\n\tv1pb.UnimplementedAttachmentServiceServer\n\tv1pb.UnimplementedShortcutServiceServer\n\tv1pb.UnimplementedIdentityProviderServiceServer\n\n\tSecret          string\n\tProfile         *profile.Profile\n\tStore           *store.Store\n\tMarkdownService markdown.Service\n\tSSEHub          *SSEHub\n\n\t// thumbnailSemaphore limits concurrent thumbnail generation to prevent memory exhaustion\n\tthumbnailSemaphore *semaphore.Weighted\n}\n\nfunc NewAPIV1Service(secret string, profile *profile.Profile, store *store.Store) *APIV1Service {\n\tmarkdownService := markdown.NewService(\n\t\tmarkdown.WithTagExtension(),\n\t)\n\treturn &APIV1Service{\n\t\tSecret:             secret,\n\t\tProfile:            profile,\n\t\tStore:              store,\n\t\tMarkdownService:    markdownService,\n\t\tSSEHub:             NewSSEHub(),\n\t\tthumbnailSemaphore: semaphore.NewWeighted(3), // Limit to 3 concurrent thumbnail generations\n\t}\n}\n\n// RegisterGateway registers the gRPC-Gateway and Connect handlers with the given Echo instance.\nfunc (s *APIV1Service) RegisterGateway(ctx context.Context, echoServer *echo.Echo) error {\n\t// Auth middleware for gRPC-Gateway - runs after routing, has access to method name.\n\t// Uses the same PublicMethods config as the Connect AuthInterceptor.\n\tauthenticator := auth.NewAuthenticator(s.Store, s.Secret)\n\tgatewayAuthMiddleware := func(next runtime.HandlerFunc) runtime.HandlerFunc {\n\t\treturn func(w http.ResponseWriter, r *http.Request, pathParams map[string]string) {\n\t\t\tctx := r.Context()\n\n\t\t\t// Get the RPC method name from context (set by grpc-gateway after routing)\n\t\t\trpcMethod, ok := runtime.RPCMethod(ctx)\n\n\t\t\t// Extract credentials from HTTP headers\n\t\t\tauthHeader := r.Header.Get(\"Authorization\")\n\n\t\t\tresult := authenticator.Authenticate(ctx, authHeader)\n\n\t\t\t// Enforce authentication for non-public methods\n\t\t\t// If rpcMethod cannot be determined, allow through, service layer will handle visibility checks\n\t\t\tif result == nil && ok && !IsPublicMethod(rpcMethod) {\n\t\t\t\thttp.Error(w, `{\"code\": 16, \"message\": \"authentication required\"}`, http.StatusUnauthorized)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t// Apply auth result to context (no-op when result is nil for public endpoints)\n\t\t\tif result != nil {\n\t\t\t\tctx = auth.ApplyToContext(ctx, result)\n\t\t\t\tr = r.WithContext(ctx)\n\t\t\t}\n\n\t\t\tnext(w, r, pathParams)\n\t\t}\n\t}\n\n\t// Create gRPC-Gateway mux with auth middleware.\n\tgwMux := runtime.NewServeMux(\n\t\truntime.WithMiddlewares(gatewayAuthMiddleware),\n\t)\n\tif err := v1pb.RegisterInstanceServiceHandlerServer(ctx, gwMux, s); err != nil {\n\t\treturn err\n\t}\n\tif err := v1pb.RegisterAuthServiceHandlerServer(ctx, gwMux, s); err != nil {\n\t\treturn err\n\t}\n\tif err := v1pb.RegisterUserServiceHandlerServer(ctx, gwMux, s); err != nil {\n\t\treturn err\n\t}\n\tif err := v1pb.RegisterMemoServiceHandlerServer(ctx, gwMux, s); err != nil {\n\t\treturn err\n\t}\n\tif err := v1pb.RegisterAttachmentServiceHandlerServer(ctx, gwMux, s); err != nil {\n\t\treturn err\n\t}\n\tif err := v1pb.RegisterShortcutServiceHandlerServer(ctx, gwMux, s); err != nil {\n\t\treturn err\n\t}\n\tif err := v1pb.RegisterIdentityProviderServiceHandlerServer(ctx, gwMux, s); err != nil {\n\t\treturn err\n\t}\n\tgwGroup := echoServer.Group(\"\")\n\tgwGroup.Use(middleware.CORSWithConfig(middleware.CORSConfig{\n\t\tAllowOrigins: []string{\"*\"},\n\t}))\n\t// Register SSE endpoint with same CORS as rest of /api/v1.\n\tgwGroup.GET(\"/api/v1/sse\", func(c *echo.Context) error {\n\t\treturn handleSSE(c, s.SSEHub, auth.NewAuthenticator(s.Store, s.Secret))\n\t})\n\thandler := echo.WrapHandler(gwMux)\n\n\tgwGroup.Any(\"/api/v1/*\", handler)\n\tgwGroup.Any(\"/file/*\", handler)\n\n\t// Connect handlers for browser clients (replaces grpc-web).\n\tlogStacktraces := s.Profile.Demo\n\tconnectInterceptors := connect.WithInterceptors(\n\t\tNewMetadataInterceptor(), // Convert HTTP headers to gRPC metadata first\n\t\tNewLoggingInterceptor(logStacktraces),\n\t\tNewRecoveryInterceptor(logStacktraces),\n\t\tNewAuthInterceptor(s.Store, s.Secret),\n\t)\n\tconnectMux := http.NewServeMux()\n\tconnectHandler := NewConnectServiceHandler(s)\n\tconnectHandler.RegisterConnectHandlers(connectMux, connectInterceptors)\n\n\t// Wrap with CORS for browser access\n\tcorsHandler := middleware.CORSWithConfig(middleware.CORSConfig{\n\t\tUnsafeAllowOriginFunc: func(_ *echo.Context, origin string) (string, bool, error) {\n\t\t\treturn origin, true, nil\n\t\t},\n\t\tAllowMethods:     []string{http.MethodGet, http.MethodPost, http.MethodOptions},\n\t\tAllowHeaders:     []string{\"*\"},\n\t\tAllowCredentials: true,\n\t})\n\tconnectGroup := echoServer.Group(\"\", corsHandler)\n\tconnectGroup.Any(\"/memos.api.v1.*\", echo.WrapHandler(connectMux))\n\n\treturn nil\n}\n"
  },
  {
    "path": "server/router/fileserver/README.md",
    "content": "# Fileserver Package\n\n## Overview\n\nThe `fileserver` package handles all binary file serving for Memos using native HTTP handlers. It was created to replace gRPC-based binary serving, which had limitations with HTTP range requests (required for Safari video/audio playback).\n\n## Responsibilities\n\n- Serve attachment binary files (images, videos, audio, documents)\n- Serve user avatar images\n- Handle HTTP range requests for video/audio streaming\n- Authenticate requests using JWT tokens or Personal Access Tokens\n- Check permissions for private content\n- Generate and serve image thumbnails\n- Prevent XSS attacks on uploaded content\n- Support S3 external storage\n\n## Architecture\n\n### Design Principles\n\n1. **Separation of Concerns**: Binary files via HTTP, metadata via gRPC\n2. **DRY**: Imports auth constants from `api/v1` package (single source of truth)\n3. **Security First**: Authentication, authorization, and XSS prevention\n4. **Performance**: Native HTTP streaming with proper caching headers\n\n### Package Structure\n\n```\nfileserver/\n├── fileserver.go           # Main service and HTTP handlers\n├── README.md              # This file\n└── fileserver_test.go     # Tests (to be added)\n```\n\n## API Endpoints\n\n### 1. Attachment Binary\n```\nGET /file/attachments/:uid/:filename[?thumbnail=true]\n```\n\n**Parameters:**\n- `uid` - Attachment unique identifier\n- `filename` - Original filename\n- `thumbnail` (optional) - Return thumbnail for images\n\n**Authentication:** Required for non-public memos\n\n**Response:**\n- `200 OK` - File content with proper Content-Type\n- `206 Partial Content` - For range requests (video/audio)\n- `401 Unauthorized` - Authentication required\n- `403 Forbidden` - User not authorized\n- `404 Not Found` - Attachment not found\n\n**Headers:**\n- `Content-Type` - MIME type of the file\n- `Cache-Control: public, max-age=3600`\n- `Accept-Ranges: bytes` - For video/audio\n- `Content-Range` - For partial responses (206)\n\n### 2. User Avatar\n```\nGET /file/users/:identifier/avatar\n```\n\n**Parameters:**\n- `identifier` - User ID (e.g., `1`) or username (e.g., `steven`)\n\n**Authentication:** Not required (avatars are public)\n\n**Response:**\n- `200 OK` - Avatar image (PNG/JPEG)\n- `404 Not Found` - User not found or no avatar set\n\n**Headers:**\n- `Content-Type` - image/png or image/jpeg\n- `Cache-Control: public, max-age=3600`\n\n## Authentication\n\n### Supported Methods\n\nThe fileserver supports the following authentication methods:\n\n1. **JWT Access Token** (`Authorization: Bearer {token}`)\n   - Short-lived tokens (15 minutes) for API access\n   - Stateless validation using JWT signature\n   - Extracts user ID from token claims\n\n2. **Personal Access Token (PAT)** (`Authorization: Bearer {pat}`)\n   - Long-lived tokens for programmatic access\n   - Validates against database for revocation\n   - Prefixed with specific identifier\n\n### Authentication Flow\n\n```\nRequest → getCurrentUser()\n    ├─→ Try Session Cookie\n    │   ├─→ Parse cookie value\n    │   ├─→ Get user from DB\n    │   ├─→ Validate session\n    │   └─→ Return user (if valid)\n    │\n    └─→ Try JWT Token\n        ├─→ Parse Authorization header\n        ├─→ Verify JWT signature\n        ├─→ Get user from DB\n        ├─→ Validate token in access tokens list\n        └─→ Return user (if valid)\n```\n\n### Permission Model\n\n**Attachments:**\n- Unlinked: Public (no auth required)\n- Public memo: Public (no auth required)\n- Protected memo: Requires authentication\n- Private memo: Creator only\n\n**Avatars:**\n- Always public (no auth required)\n\n## Key Functions\n\n### HTTP Handlers\n\n#### `serveAttachmentFile(c echo.Context) error`\nMain handler for attachment binary serving.\n\n**Flow:**\n1. Extract UID from URL parameter\n2. Fetch attachment from database\n3. Check permissions (memo visibility)\n4. Get binary blob (local file, S3, or database)\n5. Handle thumbnail request (if applicable)\n6. Set security headers (XSS prevention)\n7. Serve with range request support (video/audio)\n\n#### `serveUserAvatar(c echo.Context) error`\nMain handler for user avatar serving.\n\n**Flow:**\n1. Extract identifier (ID or username) from URL\n2. Lookup user in database\n3. Check if avatar exists\n4. Decode base64 data URI\n5. Serve with proper content type and caching\n\n### Authentication\n\n#### `getCurrentUser(ctx, c) (*store.User, error)`\nAuthenticates request using session cookie or JWT token.\n\n#### `authenticateBySession(ctx, cookie) (*store.User, error)`\nValidates session cookie and returns authenticated user.\n\n#### `authenticateByJWT(ctx, token) (*store.User, error)`\nValidates JWT access token and returns authenticated user.\n\n### Permission Checks\n\n#### `checkAttachmentPermission(ctx, c, attachment) error`\nValidates user has permission to access attachment based on memo visibility.\n\n### File Operations\n\n#### `getAttachmentBlob(attachment) ([]byte, error)`\nRetrieves binary content from local storage, S3, or database.\n\n#### `getOrGenerateThumbnail(ctx, attachment) ([]byte, error)`\nReturns cached thumbnail or generates new one (with semaphore limiting).\n\n### Utilities\n\n#### `getUserByIdentifier(ctx, identifier) (*store.User, error)`\nFinds user by ID (int) or username (string).\n\n#### `extractImageInfo(dataURI) (type, base64, error)`\nParses data URI to extract MIME type and base64 data.\n\n## Dependencies\n\n### External Packages\n- `github.com/labstack/echo/v5` - HTTP router and middleware\n- `github.com/golang-jwt/jwt/v5` - JWT parsing and validation\n- `github.com/disintegration/imaging` - Image thumbnail generation\n- `golang.org/x/sync/semaphore` - Concurrency control for thumbnails\n\n### Internal Packages\n- `server/auth` - Authentication utilities\n- `store` - Database operations\n- `internal/profile` - Server configuration\n- `plugin/storage/s3` - S3 storage client\n\n## Configuration\n\n### Constants\n\nAuth-related constants are imported from `server/auth`:\n- `auth.RefreshTokenCookieName` - \"memos_refresh\"\n- `auth.PersonalAccessTokenPrefix` - PAT identifier prefix\n\nPackage-specific constants:\n- `ThumbnailCacheFolder` - \".thumbnail_cache\"\n- `thumbnailMaxSize` - 600px\n- `SupportedThumbnailMimeTypes` - [\"image/png\", \"image/jpeg\"]\n\n## Error Handling\n\nAll handlers return Echo HTTP errors with appropriate status codes:\n\n```go\n// Bad request\necho.NewHTTPError(http.StatusBadRequest, \"message\")\n\n// Unauthorized (no auth)\necho.NewHTTPError(http.StatusUnauthorized, \"message\")\n\n// Forbidden (auth but no permission)\necho.NewHTTPError(http.StatusForbidden, \"message\")\n\n// Not found\necho.NewHTTPError(http.StatusNotFound, \"message\")\n\n// Internal error\necho.NewHTTPError(http.StatusInternalServerError, \"message\").SetInternal(err)\n```\n\n## Security Considerations\n\n### 1. XSS Prevention\nSVG and HTML files are served as `application/octet-stream` to prevent script execution:\n\n```go\nif contentType == \"image/svg+xml\" ||\n   contentType == \"text/html\" ||\n   contentType == \"application/xhtml+xml\" {\n    contentType = \"application/octet-stream\"\n}\n```\n\n### 2. Authentication\nPrivate content requires valid JWT access token or Personal Access Token.\n\n### 3. Authorization\nMemo visibility rules enforced before serving attachments.\n\n### 4. Input Validation\n- Attachment UID validated from database\n- User identifier validated (ID or username)\n- Range requests validated before processing\n\n## Performance Optimizations\n\n### 1. Thumbnail Caching\nThumbnails cached on disk to avoid regeneration:\n- Cache location: `{data_dir}/.thumbnail_cache/`\n- Filename: `{attachment_id}{extension}`\n- Semaphore limits concurrent generation (max 3)\n\n### 2. HTTP Range Requests\nVideo/audio files use `http.ServeContent()` for efficient streaming:\n- Automatic range parsing\n- Efficient memory usage (streaming, not loading full file)\n- Safari-compatible partial content responses\n\n### 3. Caching Headers\nAll responses include cache headers:\n```\nCache-Control: public, max-age=3600\n```\n\n### 4. S3 External Links\nS3 files served via presigned URLs (no server download).\n\n## Testing\n\n### Unit Tests (To Add)\nSee SAFARI_FIX.md for recommended test coverage.\n\n### Manual Testing\n```bash\n# Test attachment\ncurl \"http://localhost:8081/file/attachments/{uid}/file.jpg\"\n\n# Test avatar by ID\ncurl \"http://localhost:8081/file/users/1/avatar\"\n\n# Test avatar by username\ncurl \"http://localhost:8081/file/users/steven/avatar\"\n\n# Test range request\ncurl -H \"Range: bytes=0-999\" \"http://localhost:8081/file/attachments/{uid}/video.mp4\"\n```\n\n## Future Improvements\n\nSee SAFARI_FIX.md section \"Future Improvements\" for planned enhancements.\n\n## Related Documentation\n\n- [SAFARI_FIX.md](../../../SAFARI_FIX.md) - Full migration guide\n- [server/router/api/v1/auth.go](../api/v1/auth.go) - Auth constants source of truth\n- [RFC 7233](https://tools.ietf.org/html/rfc7233) - HTTP Range Requests spec\n"
  },
  {
    "path": "server/router/fileserver/fileserver.go",
    "content": "package fileserver\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/base64\"\n\t\"fmt\"\n\t\"io\"\n\t\"log/slog\"\n\t\"net/http\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/disintegration/imaging\"\n\t\"github.com/labstack/echo/v5\"\n\t\"github.com/pkg/errors\"\n\t\"golang.org/x/sync/semaphore\"\n\n\t\"github.com/usememos/memos/internal/profile\"\n\t\"github.com/usememos/memos/internal/util\"\n\t\"github.com/usememos/memos/plugin/storage/s3\"\n\tstorepb \"github.com/usememos/memos/proto/gen/store\"\n\t\"github.com/usememos/memos/server/auth\"\n\t\"github.com/usememos/memos/store\"\n)\n\n// Constants for file serving configuration.\nconst (\n\t// ThumbnailCacheFolder is the folder name where thumbnail images are stored.\n\tThumbnailCacheFolder = \".thumbnail_cache\"\n\n\t// thumbnailMaxSize is the maximum dimension (width or height) for thumbnails.\n\tthumbnailMaxSize = 600\n\n\t// maxConcurrentThumbnails limits concurrent thumbnail generation to prevent memory exhaustion.\n\tmaxConcurrentThumbnails = 3\n\n\t// cacheMaxAge is the max-age value for Cache-Control headers (1 hour).\n\tcacheMaxAge = \"public, max-age=3600\"\n)\n\n// xssUnsafeTypes contains MIME types that could execute scripts if served directly.\n// These are served as application/octet-stream to prevent XSS attacks.\nvar xssUnsafeTypes = map[string]bool{\n\t\"text/html\":                true,\n\t\"text/javascript\":          true,\n\t\"application/javascript\":   true,\n\t\"application/x-javascript\": true,\n\t\"text/xml\":                 true,\n\t\"application/xml\":          true,\n\t\"application/xhtml+xml\":    true,\n\t\"image/svg+xml\":            true,\n}\n\n// thumbnailSupportedTypes contains image MIME types that support thumbnail generation.\nvar thumbnailSupportedTypes = map[string]bool{\n\t\"image/png\":  true,\n\t\"image/jpeg\": true,\n\t\"image/heic\": true,\n\t\"image/heif\": true,\n\t\"image/webp\": true,\n}\n\n// avatarAllowedTypes contains MIME types allowed for user avatars.\nvar avatarAllowedTypes = map[string]bool{\n\t\"image/png\":  true,\n\t\"image/jpeg\": true,\n\t\"image/jpg\":  true,\n\t\"image/gif\":  true,\n\t\"image/webp\": true,\n\t\"image/heic\": true,\n\t\"image/heif\": true,\n}\n\n// SupportedThumbnailMimeTypes is the exported list of thumbnail-supported MIME types.\nvar SupportedThumbnailMimeTypes = []string{\n\t\"image/png\",\n\t\"image/jpeg\",\n\t\"image/heic\",\n\t\"image/heif\",\n\t\"image/webp\",\n}\n\n// dataURIRegex parses data URI format: data:image/png;base64,iVBORw0KGgo...\nvar dataURIRegex = regexp.MustCompile(`^data:(?P<type>[^;]+);base64,(?P<base64>.+)`)\n\n// FileServerService handles HTTP file serving with proper range request support.\ntype FileServerService struct {\n\tProfile       *profile.Profile\n\tStore         *store.Store\n\tauthenticator *auth.Authenticator\n\n\t// thumbnailSemaphore limits concurrent thumbnail generation.\n\tthumbnailSemaphore *semaphore.Weighted\n}\n\n// NewFileServerService creates a new file server service.\nfunc NewFileServerService(profile *profile.Profile, store *store.Store, secret string) *FileServerService {\n\treturn &FileServerService{\n\t\tProfile:            profile,\n\t\tStore:              store,\n\t\tauthenticator:      auth.NewAuthenticator(store, secret),\n\t\tthumbnailSemaphore: semaphore.NewWeighted(maxConcurrentThumbnails),\n\t}\n}\n\n// RegisterRoutes registers HTTP file serving routes.\nfunc (s *FileServerService) RegisterRoutes(echoServer *echo.Echo) {\n\tfileGroup := echoServer.Group(\"/file\")\n\tfileGroup.GET(\"/attachments/:uid/:filename\", s.serveAttachmentFile)\n\tfileGroup.GET(\"/users/:identifier/avatar\", s.serveUserAvatar)\n}\n\n// =============================================================================\n// HTTP Handlers\n// =============================================================================\n\n// serveAttachmentFile serves attachment binary content using native HTTP.\nfunc (s *FileServerService) serveAttachmentFile(c *echo.Context) error {\n\tctx := c.Request().Context()\n\tuid := c.Param(\"uid\")\n\twantThumbnail := c.QueryParam(\"thumbnail\") == \"true\"\n\n\tattachment, err := s.Store.GetAttachment(ctx, &store.FindAttachment{\n\t\tUID:     &uid,\n\t\tGetBlob: true,\n\t})\n\tif err != nil {\n\t\treturn echo.NewHTTPError(http.StatusInternalServerError, \"failed to get attachment\").Wrap(err)\n\t}\n\tif attachment == nil {\n\t\treturn echo.NewHTTPError(http.StatusNotFound, \"attachment not found\")\n\t}\n\n\tif err := s.checkAttachmentPermission(ctx, c, attachment); err != nil {\n\t\treturn err\n\t}\n\n\tcontentType := s.sanitizeContentType(attachment.Type)\n\n\t// Stream video/audio to avoid loading entire file into memory.\n\tif isMediaType(attachment.Type) {\n\t\treturn s.serveMediaStream(c, attachment, contentType)\n\t}\n\n\treturn s.serveStaticFile(c, attachment, contentType, wantThumbnail)\n}\n\n// serveUserAvatar serves user avatar images.\nfunc (s *FileServerService) serveUserAvatar(c *echo.Context) error {\n\tctx := c.Request().Context()\n\tidentifier := c.Param(\"identifier\")\n\n\tuser, err := s.getUserByIdentifier(ctx, identifier)\n\tif err != nil {\n\t\treturn echo.NewHTTPError(http.StatusInternalServerError, \"failed to get user\").Wrap(err)\n\t}\n\tif user == nil {\n\t\treturn echo.NewHTTPError(http.StatusNotFound, \"user not found\")\n\t}\n\tif user.AvatarURL == \"\" {\n\t\treturn echo.NewHTTPError(http.StatusNotFound, \"avatar not found\")\n\t}\n\n\timageType, imageData, err := s.parseDataURI(user.AvatarURL)\n\tif err != nil {\n\t\treturn echo.NewHTTPError(http.StatusInternalServerError, \"failed to parse avatar data\").Wrap(err)\n\t}\n\n\tif !avatarAllowedTypes[imageType] {\n\t\treturn echo.NewHTTPError(http.StatusBadRequest, \"invalid avatar image type\")\n\t}\n\n\tsetSecurityHeaders(c)\n\tc.Response().Header().Set(echo.HeaderContentType, imageType)\n\tc.Response().Header().Set(echo.HeaderCacheControl, cacheMaxAge)\n\n\treturn c.Blob(http.StatusOK, imageType, imageData)\n}\n\n// =============================================================================\n// File Serving Methods\n// =============================================================================\n\n// serveMediaStream serves video/audio files using streaming to avoid memory exhaustion.\nfunc (s *FileServerService) serveMediaStream(c *echo.Context, attachment *store.Attachment, contentType string) error {\n\tsetSecurityHeaders(c)\n\tsetMediaHeaders(c, contentType, attachment.Type)\n\n\tswitch attachment.StorageType {\n\tcase storepb.AttachmentStorageType_LOCAL:\n\t\tfilePath, err := s.resolveLocalPath(attachment.Reference)\n\t\tif err != nil {\n\t\t\treturn echo.NewHTTPError(http.StatusInternalServerError, \"failed to resolve file path\").Wrap(err)\n\t\t}\n\t\thttp.ServeFile(c.Response(), c.Request(), filePath)\n\t\treturn nil\n\n\tcase storepb.AttachmentStorageType_S3:\n\t\tpresignURL, err := s.getS3PresignedURL(c.Request().Context(), attachment)\n\t\tif err != nil {\n\t\t\treturn echo.NewHTTPError(http.StatusInternalServerError, \"failed to generate presigned URL\").Wrap(err)\n\t\t}\n\t\treturn c.Redirect(http.StatusTemporaryRedirect, presignURL)\n\n\tdefault:\n\t\t// Database storage fallback.\n\t\tmodTime := time.Unix(attachment.UpdatedTs, 0)\n\t\thttp.ServeContent(c.Response(), c.Request(), attachment.Filename, modTime, bytes.NewReader(attachment.Blob))\n\t\treturn nil\n\t}\n}\n\n// serveStaticFile serves non-streaming files (images, documents, etc.).\nfunc (s *FileServerService) serveStaticFile(c *echo.Context, attachment *store.Attachment, contentType string, wantThumbnail bool) error {\n\tblob, err := s.getAttachmentBlob(attachment)\n\tif err != nil {\n\t\treturn echo.NewHTTPError(http.StatusInternalServerError, \"failed to get attachment blob\").Wrap(err)\n\t}\n\n\t// Generate thumbnail for supported image types.\n\tif wantThumbnail && thumbnailSupportedTypes[attachment.Type] {\n\t\tif thumbnailBlob, err := s.getOrGenerateThumbnail(c.Request().Context(), attachment); err != nil {\n\t\t\tslog.Warn(\"failed to get thumbnail\", \"error\", err)\n\t\t} else {\n\t\t\tblob = thumbnailBlob\n\t\t}\n\t}\n\n\tsetSecurityHeaders(c)\n\tsetMediaHeaders(c, contentType, attachment.Type)\n\n\t// Force download for non-media files to prevent XSS execution.\n\tif !strings.HasPrefix(contentType, \"image/\") && contentType != \"application/pdf\" {\n\t\tc.Response().Header().Set(echo.HeaderContentDisposition, fmt.Sprintf(\"attachment; filename=%q\", attachment.Filename))\n\t}\n\n\treturn c.Blob(http.StatusOK, contentType, blob)\n}\n\n// =============================================================================\n// Storage Operations\n// =============================================================================\n\n// getAttachmentBlob retrieves the binary content of an attachment from storage.\nfunc (s *FileServerService) getAttachmentBlob(attachment *store.Attachment) ([]byte, error) {\n\tswitch attachment.StorageType {\n\tcase storepb.AttachmentStorageType_LOCAL:\n\t\treturn s.readLocalFile(attachment.Reference)\n\n\tcase storepb.AttachmentStorageType_S3:\n\t\treturn s.downloadFromS3(attachment)\n\n\tdefault:\n\t\treturn attachment.Blob, nil\n\t}\n}\n\n// getAttachmentReader returns a reader for streaming attachment content.\nfunc (s *FileServerService) getAttachmentReader(attachment *store.Attachment) (io.ReadCloser, error) {\n\tswitch attachment.StorageType {\n\tcase storepb.AttachmentStorageType_LOCAL:\n\t\tfilePath, err := s.resolveLocalPath(attachment.Reference)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tfile, err := os.Open(filePath)\n\t\tif err != nil {\n\t\t\tif os.IsNotExist(err) {\n\t\t\t\treturn nil, errors.Wrap(err, \"file not found\")\n\t\t\t}\n\t\t\treturn nil, errors.Wrap(err, \"failed to open file\")\n\t\t}\n\t\treturn file, nil\n\n\tcase storepb.AttachmentStorageType_S3:\n\t\ts3Client, s3Object, err := s.createS3Client(attachment)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treader, err := s3Client.GetObjectStream(context.Background(), s3Object.Key)\n\t\tif err != nil {\n\t\t\treturn nil, errors.Wrap(err, \"failed to stream from S3\")\n\t\t}\n\t\treturn reader, nil\n\n\tdefault:\n\t\treturn io.NopCloser(bytes.NewReader(attachment.Blob)), nil\n\t}\n}\n\n// resolveLocalPath converts a storage reference to an absolute file path.\nfunc (s *FileServerService) resolveLocalPath(reference string) (string, error) {\n\tfilePath := filepath.FromSlash(reference)\n\tif !filepath.IsAbs(filePath) {\n\t\tfilePath = filepath.Join(s.Profile.Data, filePath)\n\t}\n\treturn filePath, nil\n}\n\n// readLocalFile reads the entire contents of a local file.\nfunc (s *FileServerService) readLocalFile(reference string) ([]byte, error) {\n\tfilePath, err := s.resolveLocalPath(reference)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfile, err := os.Open(filePath)\n\tif err != nil {\n\t\tif os.IsNotExist(err) {\n\t\t\treturn nil, errors.Wrap(err, \"file not found\")\n\t\t}\n\t\treturn nil, errors.Wrap(err, \"failed to open file\")\n\t}\n\tdefer file.Close()\n\n\tblob, err := io.ReadAll(file)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"failed to read file\")\n\t}\n\treturn blob, nil\n}\n\n// createS3Client creates an S3 client from attachment payload.\nfunc (*FileServerService) createS3Client(attachment *store.Attachment) (*s3.Client, *storepb.AttachmentPayload_S3Object, error) {\n\tif attachment.Payload == nil {\n\t\treturn nil, nil, errors.New(\"attachment payload is missing\")\n\t}\n\ts3Object := attachment.Payload.GetS3Object()\n\tif s3Object == nil {\n\t\treturn nil, nil, errors.New(\"S3 object payload is missing\")\n\t}\n\tif s3Object.S3Config == nil {\n\t\treturn nil, nil, errors.New(\"S3 config is missing\")\n\t}\n\tif s3Object.Key == \"\" {\n\t\treturn nil, nil, errors.New(\"S3 object key is missing\")\n\t}\n\n\tclient, err := s3.NewClient(context.Background(), s3Object.S3Config)\n\tif err != nil {\n\t\treturn nil, nil, errors.Wrap(err, \"failed to create S3 client\")\n\t}\n\treturn client, s3Object, nil\n}\n\n// downloadFromS3 downloads the entire object from S3.\nfunc (s *FileServerService) downloadFromS3(attachment *store.Attachment) ([]byte, error) {\n\tclient, s3Object, err := s.createS3Client(attachment)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tblob, err := client.GetObject(context.Background(), s3Object.Key)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"failed to download from S3\")\n\t}\n\treturn blob, nil\n}\n\n// getS3PresignedURL generates a presigned URL for direct S3 access.\nfunc (s *FileServerService) getS3PresignedURL(ctx context.Context, attachment *store.Attachment) (string, error) {\n\tclient, s3Object, err := s.createS3Client(attachment)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\turl, err := client.PresignGetObject(ctx, s3Object.Key)\n\tif err != nil {\n\t\treturn \"\", errors.Wrap(err, \"failed to presign URL\")\n\t}\n\treturn url, nil\n}\n\n// =============================================================================\n// Thumbnail Generation\n// =============================================================================\n\n// getOrGenerateThumbnail returns the thumbnail image of the attachment.\n// Uses semaphore to limit concurrent thumbnail generation and prevent memory exhaustion.\nfunc (s *FileServerService) getOrGenerateThumbnail(ctx context.Context, attachment *store.Attachment) ([]byte, error) {\n\tthumbnailPath, err := s.getThumbnailPath(attachment)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Fast path: return cached thumbnail if exists.\n\tif blob, err := s.readCachedThumbnail(thumbnailPath); err == nil {\n\t\treturn blob, nil\n\t}\n\n\t// Acquire semaphore to limit concurrent generation.\n\tif err := s.thumbnailSemaphore.Acquire(ctx, 1); err != nil {\n\t\treturn nil, errors.Wrap(err, \"failed to acquire semaphore\")\n\t}\n\tdefer s.thumbnailSemaphore.Release(1)\n\n\t// Double-check after acquiring semaphore (another goroutine may have generated it).\n\tif blob, err := s.readCachedThumbnail(thumbnailPath); err == nil {\n\t\treturn blob, nil\n\t}\n\n\treturn s.generateThumbnail(attachment, thumbnailPath)\n}\n\n// getThumbnailPath returns the file path for a cached thumbnail.\nfunc (s *FileServerService) getThumbnailPath(attachment *store.Attachment) (string, error) {\n\tcacheFolder := filepath.Join(s.Profile.Data, ThumbnailCacheFolder)\n\tif err := os.MkdirAll(cacheFolder, os.ModePerm); err != nil {\n\t\treturn \"\", errors.Wrap(err, \"failed to create thumbnail cache folder\")\n\t}\n\tfilename := fmt.Sprintf(\"%d%s\", attachment.ID, filepath.Ext(attachment.Filename))\n\treturn filepath.Join(cacheFolder, filename), nil\n}\n\n// readCachedThumbnail reads a thumbnail from the cache directory.\nfunc (*FileServerService) readCachedThumbnail(path string) ([]byte, error) {\n\tfile, err := os.Open(path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer file.Close()\n\treturn io.ReadAll(file)\n}\n\n// generateThumbnail creates a new thumbnail and saves it to disk.\nfunc (s *FileServerService) generateThumbnail(attachment *store.Attachment, thumbnailPath string) ([]byte, error) {\n\treader, err := s.getAttachmentReader(attachment)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"failed to get attachment reader\")\n\t}\n\tdefer reader.Close()\n\n\timg, err := imaging.Decode(reader, imaging.AutoOrientation(true))\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"failed to decode image\")\n\t}\n\n\twidth, height := img.Bounds().Dx(), img.Bounds().Dy()\n\tthumbnailWidth, thumbnailHeight := calculateThumbnailDimensions(width, height)\n\n\tthumbnailImage := imaging.Resize(img, thumbnailWidth, thumbnailHeight, imaging.Lanczos)\n\n\tif err := imaging.Save(thumbnailImage, thumbnailPath); err != nil {\n\t\treturn nil, errors.Wrap(err, \"failed to save thumbnail\")\n\t}\n\n\treturn s.readCachedThumbnail(thumbnailPath)\n}\n\n// calculateThumbnailDimensions calculates the target dimensions for a thumbnail.\n// The largest dimension is constrained to thumbnailMaxSize while maintaining aspect ratio.\n// Small images are not enlarged.\nfunc calculateThumbnailDimensions(width, height int) (int, int) {\n\tif max(width, height) <= thumbnailMaxSize {\n\t\treturn width, height\n\t}\n\tif width >= height {\n\t\treturn thumbnailMaxSize, 0 // Landscape: constrain width.\n\t}\n\treturn 0, thumbnailMaxSize // Portrait: constrain height.\n}\n\n// =============================================================================\n// Authentication & Authorization\n// =============================================================================\n\n// checkAttachmentPermission verifies the user has permission to access the attachment.\nfunc (s *FileServerService) checkAttachmentPermission(ctx context.Context, c *echo.Context, attachment *store.Attachment) error {\n\t// For unlinked attachments, only the creator can access.\n\tif attachment.MemoID == nil {\n\t\tuser, err := s.getCurrentUser(ctx, c)\n\t\tif err != nil {\n\t\t\treturn echo.NewHTTPError(http.StatusInternalServerError, \"failed to get current user\").Wrap(err)\n\t\t}\n\t\tif user == nil {\n\t\t\treturn echo.NewHTTPError(http.StatusUnauthorized, \"unauthorized access\")\n\t\t}\n\t\tif user.ID != attachment.CreatorID && user.Role != store.RoleAdmin {\n\t\t\treturn echo.NewHTTPError(http.StatusForbidden, \"forbidden access\")\n\t\t}\n\t\treturn nil\n\t}\n\n\tmemo, err := s.Store.GetMemo(ctx, &store.FindMemo{ID: attachment.MemoID})\n\tif err != nil {\n\t\treturn echo.NewHTTPError(http.StatusInternalServerError, \"failed to find memo\").Wrap(err)\n\t}\n\tif memo == nil {\n\t\treturn echo.NewHTTPError(http.StatusNotFound, \"memo not found\")\n\t}\n\n\tif memo.Visibility == store.Public {\n\t\treturn nil\n\t}\n\n\t// Check share token fallback: allow access if request carries a valid, non-expired share token\n\t// that was issued for this specific memo. This covers attachment requests made from the shared\n\t// memo page for private or protected memos.\n\tif shareToken := (*c).QueryParam(\"share_token\"); shareToken != \"\" {\n\t\tms, err := s.Store.GetMemoShare(ctx, &store.FindMemoShare{UID: &shareToken})\n\t\tif err == nil && ms != nil && !isMemoShareExpired(ms) && ms.MemoID == memo.ID {\n\t\t\treturn nil\n\t\t}\n\t}\n\n\tuser, err := s.getCurrentUser(ctx, c)\n\tif err != nil {\n\t\treturn echo.NewHTTPError(http.StatusInternalServerError, \"failed to get current user\").Wrap(err)\n\t}\n\tif user == nil {\n\t\treturn echo.NewHTTPError(http.StatusUnauthorized, \"unauthorized access\")\n\t}\n\n\tif memo.Visibility == store.Private && user.ID != memo.CreatorID && user.Role != store.RoleAdmin {\n\t\treturn echo.NewHTTPError(http.StatusForbidden, \"forbidden access\")\n\t}\n\n\treturn nil\n}\n\n// getCurrentUser retrieves the current authenticated user from the request.\n// Authentication priority: Bearer token (Access Token V2 or PAT) > Refresh token cookie.\nfunc (s *FileServerService) getCurrentUser(ctx context.Context, c *echo.Context) (*store.User, error) {\n\tauthHeader := c.Request().Header.Get(echo.HeaderAuthorization)\n\tcookieHeader := c.Request().Header.Get(\"Cookie\")\n\treturn s.authenticator.AuthenticateToUser(ctx, authHeader, cookieHeader)\n}\n\n// getUserByIdentifier finds a user by either ID or username.\nfunc (s *FileServerService) getUserByIdentifier(ctx context.Context, identifier string) (*store.User, error) {\n\tif userID, err := util.ConvertStringToInt32(identifier); err == nil {\n\t\treturn s.Store.GetUser(ctx, &store.FindUser{ID: &userID})\n\t}\n\treturn s.Store.GetUser(ctx, &store.FindUser{Username: &identifier})\n}\n\n// =============================================================================\n// Helper Functions\n// =============================================================================\n\n// sanitizeContentType converts potentially dangerous MIME types to safe alternatives.\nfunc (*FileServerService) sanitizeContentType(mimeType string) string {\n\tcontentType := mimeType\n\tif strings.HasPrefix(contentType, \"text/\") {\n\t\tcontentType += \"; charset=utf-8\"\n\t}\n\t// Normalize for case-insensitive lookup.\n\tif xssUnsafeTypes[strings.ToLower(mimeType)] {\n\t\treturn \"application/octet-stream\"\n\t}\n\treturn contentType\n}\n\n// parseDataURI extracts MIME type and decoded data from a data URI.\nfunc (*FileServerService) parseDataURI(dataURI string) (string, []byte, error) {\n\tmatches := dataURIRegex.FindStringSubmatch(dataURI)\n\tif len(matches) != 3 {\n\t\treturn \"\", nil, errors.New(\"invalid data URI format\")\n\t}\n\n\timageType := matches[1]\n\timageData, err := base64.StdEncoding.DecodeString(matches[2])\n\tif err != nil {\n\t\treturn \"\", nil, errors.Wrap(err, \"failed to decode base64 data\")\n\t}\n\n\treturn imageType, imageData, nil\n}\n\n// isMediaType checks if the MIME type is video or audio.\nfunc isMediaType(mimeType string) bool {\n\treturn strings.HasPrefix(mimeType, \"video/\") || strings.HasPrefix(mimeType, \"audio/\")\n}\n\n// setSecurityHeaders sets common security headers for all responses.\nfunc setSecurityHeaders(c *echo.Context) {\n\th := c.Response().Header()\n\th.Set(\"X-Content-Type-Options\", \"nosniff\")\n\th.Set(\"X-Frame-Options\", \"DENY\")\n\th.Set(\"Content-Security-Policy\", \"default-src 'none'; style-src 'unsafe-inline';\")\n}\n\n// setMediaHeaders sets headers for media file responses.\nfunc setMediaHeaders(c *echo.Context, contentType, originalType string) {\n\th := c.Response().Header()\n\th.Set(echo.HeaderContentType, contentType)\n\th.Set(echo.HeaderCacheControl, cacheMaxAge)\n\n\t// Support HDR/wide color gamut for images and videos.\n\tif strings.HasPrefix(originalType, \"image/\") || strings.HasPrefix(originalType, \"video/\") {\n\t\th.Set(\"Color-Gamut\", \"srgb, p3, rec2020\")\n\t}\n}\n\n// isMemoShareExpired returns true if the share has a defined expiry that has already passed.\nfunc isMemoShareExpired(ms *store.MemoShare) bool {\n\treturn ms.ExpiresTs != nil && time.Now().Unix() > *ms.ExpiresTs\n}\n"
  },
  {
    "path": "server/router/fileserver/fileserver_test.go",
    "content": "package fileserver\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/labstack/echo/v5\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/usememos/memos/internal/profile\"\n\t\"github.com/usememos/memos/plugin/markdown\"\n\tapiv1 \"github.com/usememos/memos/proto/gen/api/v1\"\n\t\"github.com/usememos/memos/server/auth\"\n\tapiv1service \"github.com/usememos/memos/server/router/api/v1\"\n\t\"github.com/usememos/memos/store\"\n\tteststore \"github.com/usememos/memos/store/test\"\n)\n\nfunc TestServeAttachmentFile_ShareTokenAllowsDirectMemoAttachment(t *testing.T) {\n\tctx := context.Background()\n\tsvc, fs, _, cleanup := newShareAttachmentTestServices(ctx, t)\n\tdefer cleanup()\n\n\tcreator, err := svc.Store.CreateUser(ctx, &store.User{\n\t\tUsername: \"share-parent-owner\",\n\t\tRole:     store.RoleUser,\n\t\tEmail:    \"share-parent-owner@example.com\",\n\t})\n\trequire.NoError(t, err)\n\n\tcreatorCtx := context.WithValue(ctx, auth.UserIDContextKey, creator.ID)\n\n\tattachment, err := svc.CreateAttachment(creatorCtx, &apiv1.CreateAttachmentRequest{\n\t\tAttachment: &apiv1.Attachment{\n\t\t\tFilename: \"memo.txt\",\n\t\t\tType:     \"text/plain\",\n\t\t\tContent:  []byte(\"memo attachment\"),\n\t\t},\n\t})\n\trequire.NoError(t, err)\n\n\tparentMemo, err := svc.CreateMemo(creatorCtx, &apiv1.CreateMemoRequest{\n\t\tMemo: &apiv1.Memo{\n\t\t\tContent:    \"shared parent\",\n\t\t\tVisibility: apiv1.Visibility_PROTECTED,\n\t\t\tAttachments: []*apiv1.Attachment{\n\t\t\t\t{Name: attachment.Name},\n\t\t\t},\n\t\t},\n\t})\n\trequire.NoError(t, err)\n\n\tshare, err := svc.CreateMemoShare(creatorCtx, &apiv1.CreateMemoShareRequest{\n\t\tParent:    parentMemo.Name,\n\t\tMemoShare: &apiv1.MemoShare{},\n\t})\n\trequire.NoError(t, err)\n\tshareToken := share.Name[strings.LastIndex(share.Name, \"/\")+1:]\n\n\te := echo.New()\n\tfs.RegisterRoutes(e)\n\n\treq := httptest.NewRequest(http.MethodGet, fmt.Sprintf(\"/file/%s/%s?share_token=%s\", attachment.Name, attachment.Filename, shareToken), nil)\n\trec := httptest.NewRecorder()\n\te.ServeHTTP(rec, req)\n\n\trequire.Equal(t, http.StatusOK, rec.Code)\n\trequire.Equal(t, \"memo attachment\", rec.Body.String())\n}\n\nfunc TestServeAttachmentFile_ShareTokenRejectsCommentAttachment(t *testing.T) {\n\tctx := context.Background()\n\tsvc, fs, _, cleanup := newShareAttachmentTestServices(ctx, t)\n\tdefer cleanup()\n\n\tcreator, err := svc.Store.CreateUser(ctx, &store.User{\n\t\tUsername: \"private-parent-owner\",\n\t\tRole:     store.RoleUser,\n\t\tEmail:    \"private-parent-owner@example.com\",\n\t})\n\trequire.NoError(t, err)\n\n\tcreatorCtx := context.WithValue(ctx, auth.UserIDContextKey, creator.ID)\n\tcommenter, err := svc.Store.CreateUser(ctx, &store.User{\n\t\tUsername: \"share-commenter\",\n\t\tRole:     store.RoleUser,\n\t\tEmail:    \"share-commenter@example.com\",\n\t})\n\trequire.NoError(t, err)\n\tcommenterCtx := context.WithValue(ctx, auth.UserIDContextKey, commenter.ID)\n\n\tparentMemo, err := svc.CreateMemo(creatorCtx, &apiv1.CreateMemoRequest{\n\t\tMemo: &apiv1.Memo{\n\t\t\tContent:    \"shared parent\",\n\t\t\tVisibility: apiv1.Visibility_PROTECTED,\n\t\t},\n\t})\n\trequire.NoError(t, err)\n\n\tcommentAttachment, err := svc.CreateAttachment(commenterCtx, &apiv1.CreateAttachmentRequest{\n\t\tAttachment: &apiv1.Attachment{\n\t\t\tFilename: \"comment.txt\",\n\t\t\tType:     \"text/plain\",\n\t\t\tContent:  []byte(\"comment attachment\"),\n\t\t},\n\t})\n\trequire.NoError(t, err)\n\n\t_, err = svc.CreateMemoComment(commenterCtx, &apiv1.CreateMemoCommentRequest{\n\t\tName: parentMemo.Name,\n\t\tComment: &apiv1.Memo{\n\t\t\tContent:    \"comment with attachment\",\n\t\t\tVisibility: apiv1.Visibility_PROTECTED,\n\t\t\tAttachments: []*apiv1.Attachment{\n\t\t\t\t{Name: commentAttachment.Name},\n\t\t\t},\n\t\t},\n\t})\n\trequire.NoError(t, err)\n\n\tshare, err := svc.CreateMemoShare(creatorCtx, &apiv1.CreateMemoShareRequest{\n\t\tParent:    parentMemo.Name,\n\t\tMemoShare: &apiv1.MemoShare{},\n\t})\n\trequire.NoError(t, err)\n\tshareToken := share.Name[strings.LastIndex(share.Name, \"/\")+1:]\n\n\te := echo.New()\n\tfs.RegisterRoutes(e)\n\n\treq := httptest.NewRequest(http.MethodGet, fmt.Sprintf(\"/file/%s/%s?share_token=%s\", commentAttachment.Name, commentAttachment.Filename, shareToken), nil)\n\trec := httptest.NewRecorder()\n\te.ServeHTTP(rec, req)\n\n\trequire.Equal(t, http.StatusUnauthorized, rec.Code)\n}\n\nfunc newShareAttachmentTestServices(ctx context.Context, t *testing.T) (*apiv1service.APIV1Service, *FileServerService, *store.Store, func()) {\n\tt.Helper()\n\n\ttestStore := teststore.NewTestingStore(ctx, t)\n\ttestProfile := &profile.Profile{\n\t\tDemo:        true,\n\t\tVersion:     \"test-1.0.0\",\n\t\tInstanceURL: \"http://localhost:8080\",\n\t\tDriver:      \"sqlite\",\n\t\tDSN:         \":memory:\",\n\t\tData:        t.TempDir(),\n\t}\n\tsecret := \"test-secret\"\n\tmarkdownService := markdown.NewService(markdown.WithTagExtension())\n\tapiService := &apiv1service.APIV1Service{\n\t\tSecret:          secret,\n\t\tProfile:         testProfile,\n\t\tStore:           testStore,\n\t\tMarkdownService: markdownService,\n\t\tSSEHub:          apiv1service.NewSSEHub(),\n\t}\n\tfileService := NewFileServerService(testProfile, testStore, secret)\n\n\treturn apiService, fileService, testStore, func() {\n\t\ttestStore.Close()\n\t}\n}\n"
  },
  {
    "path": "server/router/frontend/frontend.go",
    "content": "package frontend\n\nimport (\n\t\"context\"\n\t\"embed\"\n\t\"io/fs\"\n\n\t\"github.com/labstack/echo/v5\"\n\t\"github.com/labstack/echo/v5/middleware\"\n\n\t\"github.com/usememos/memos/internal/profile\"\n\t\"github.com/usememos/memos/internal/util\"\n\t\"github.com/usememos/memos/store\"\n)\n\n//go:embed dist/*\nvar embeddedFiles embed.FS\n\ntype FrontendService struct {\n\tProfile *profile.Profile\n\tStore   *store.Store\n}\n\nfunc NewFrontendService(profile *profile.Profile, store *store.Store) *FrontendService {\n\treturn &FrontendService{\n\t\tProfile: profile,\n\t\tStore:   store,\n\t}\n}\n\nfunc (*FrontendService) Serve(_ context.Context, e *echo.Echo) {\n\tskipper := func(c *echo.Context) bool {\n\t\t// Skip API routes.\n\t\tif util.HasPrefixes(c.Path(), \"/api\", \"/memos.api.v1\") {\n\t\t\treturn true\n\t\t}\n\t\t// For index.html and root path, set no-cache headers to prevent browser caching\n\t\t// This prevents sensitive data from being accessible via browser back button after logout\n\t\tif c.Path() == \"/\" || c.Path() == \"/index.html\" {\n\t\t\tc.Response().Header().Set(echo.HeaderCacheControl, \"no-cache, no-store, must-revalidate\")\n\t\t\tc.Response().Header().Set(\"Pragma\", \"no-cache\")\n\t\t\tc.Response().Header().Set(\"Expires\", \"0\")\n\t\t\treturn false\n\t\t}\n\t\t// Set Cache-Control header for static assets.\n\t\t// Since Vite generates content-hashed filenames (e.g., index-BtVjejZf.js),\n\t\t// we can cache aggressively but use immutable to prevent revalidation checks.\n\t\t// For frequently redeployed instances, use shorter max-age (1 hour) to avoid\n\t\t// serving stale assets after redeployment.\n\t\tc.Response().Header().Set(echo.HeaderCacheControl, \"public, max-age=3600, immutable\") // 1 hour\n\t\treturn false\n\t}\n\n\t// Route to serve the main app with HTML5 fallback for SPA behavior.\n\te.Use(middleware.StaticWithConfig(middleware.StaticConfig{\n\t\tFilesystem: getFileSystem(\"dist\"),\n\t\tHTML5:      true, // Enable fallback to index.html\n\t\tSkipper:    skipper,\n\t}))\n}\n\nfunc getFileSystem(path string) fs.FS {\n\tsub, err := fs.Sub(embeddedFiles, path)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn sub\n}\n"
  },
  {
    "path": "server/router/mcp/README.md",
    "content": "# MCP Server\n\nThis package implements a [Model Context Protocol (MCP)](https://modelcontextprotocol.io) server embedded in the Memos HTTP process. It exposes memo operations as MCP tools, making Memos accessible to any MCP-compatible AI client (Claude Desktop, Cursor, Zed, etc.).\n\n## Endpoint\n\n```\nPOST /mcp   (tool calls, initialize)\nGET  /mcp   (optional SSE stream for server-to-client messages)\n```\n\nTransport: [Streamable HTTP](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports) (single endpoint, MCP spec 2025-03-26).\n\n## Capabilities\n\nThe server advertises the following MCP capabilities:\n\n| Capability | Enabled | Details |\n|---|---|---|\n| Tools | Yes | List changed notifications supported |\n| Resources | Yes | Subscribe + list changed supported |\n| Prompts | Yes | List changed notifications supported |\n| Logging | Yes | Structured log events |\n\n## Authentication\n\nEvery request must include a Personal Access Token (PAT):\n\n```\nAuthorization: Bearer <your-PAT>\n```\n\nPATs are long-lived tokens created in Settings → My Account → Access Tokens. Short-lived JWT session tokens are also accepted. Requests without a valid token receive `HTTP 401`.\n\n## Tools\n\n### Memo Tools\n\n| Tool | Description | Required params | Optional params |\n|---|---|---|---|\n| `list_memos` | List memos | — | `page_size`, `page`, `state`, `order_by_pinned`, `filter` (CEL) |\n| `get_memo` | Get a single memo | `name` | — |\n| `search_memos` | Full-text search | `query` | — |\n| `create_memo` | Create a memo | `content` | `visibility` |\n| `update_memo` | Update a memo | `name` | `content`, `visibility`, `pinned`, `state` |\n| `delete_memo` | Delete a memo | `name` | — |\n| `list_memo_comments` | List comments | `name` | — |\n| `create_memo_comment` | Add a comment | `name`, `content` | — |\n\n### Tag Tools\n\n| Tool | Description | Required params |\n|---|---|---|\n| `list_tags` | List all tags with counts | — |\n\n### Attachment Tools\n\n| Tool | Description | Required params | Optional params |\n|---|---|---|---|\n| `list_attachments` | List user's attachments | — | `page_size`, `page`, `memo` |\n| `get_attachment` | Get attachment metadata | `name` | — |\n| `delete_attachment` | Delete an attachment | `name` | — |\n| `link_attachment_to_memo` | Link attachment to memo | `name`, `memo` | — |\n\n### Relation Tools\n\n| Tool | Description | Required params | Optional params |\n|---|---|---|---|\n| `list_memo_relations` | List relations (refs + comments) | `name` | `type` |\n| `create_memo_relation` | Create a reference relation | `name`, `related_memo` | — |\n| `delete_memo_relation` | Delete a reference relation | `name`, `related_memo` | — |\n\n### Reaction Tools\n\n| Tool | Description | Required params |\n|---|---|---|\n| `list_reactions` | List reactions on a memo | `name` |\n| `upsert_reaction` | Add a reaction emoji | `name`, `reaction_type` |\n| `delete_reaction` | Remove a reaction | `id` |\n\n## Resources\n\n| URI Template | Description | MIME Type |\n|---|---|---|\n| `memo://memos/{uid}` | Memo content with YAML frontmatter | `text/markdown` |\n\n## Prompts\n\n| Prompt | Description | Arguments |\n|---|---|---|\n| `capture` | Quick-save a thought as a memo | `content` (required), `tags`, `visibility` |\n| `review` | Search and summarize memos on a topic | `topic` (required) |\n| `daily_digest` | Summarize recent memo activity | `days` |\n| `organize` | Suggest tags/relations for unorganized memos | `scope` |\n\n## Resource Names\n\n- Memos: `memos/<uid>` (e.g. `memos/abc123`)\n- Attachments: `attachments/<uid>` (e.g. `attachments/def456`)\n\n## Connecting Claude Code\n\n```bash\nclaude mcp add --transport http memos http://localhost:5230/mcp \\\n  --header \"Authorization: Bearer <your-PAT>\"\n```\n\nUse `--scope user` to make it available across all projects:\n\n```bash\nclaude mcp add --scope user --transport http memos http://localhost:5230/mcp \\\n  --header \"Authorization: Bearer <your-PAT>\"\n```\n\n## Package Structure\n\n| File | Responsibility |\n|---|---|\n| `mcp.go` | `MCPService` struct, constructor, route registration, auth middleware |\n| `tools_memo.go` | Memo CRUD tools + helpers (JSON types, visibility/access checks) |\n| `tools_tag.go` | Tag listing tool |\n| `tools_attachment.go` | Attachment listing, metadata, deletion, linking tools |\n| `tools_relation.go` | Memo relation (reference) tools |\n| `tools_reaction.go` | Reaction (emoji) tools |\n| `resources_memo.go` | Memo resource template handler |\n| `prompts.go` | Prompt handlers (capture, review, daily_digest, organize) |\n"
  },
  {
    "path": "server/router/mcp/mcp.go",
    "content": "package mcp\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/labstack/echo/v5\"\n\t\"github.com/labstack/echo/v5/middleware\"\n\tmcpserver \"github.com/mark3labs/mcp-go/server\"\n\n\t\"github.com/usememos/memos/internal/profile\"\n\t\"github.com/usememos/memos/server/auth\"\n\t\"github.com/usememos/memos/store\"\n)\n\ntype MCPService struct {\n\tprofile       *profile.Profile\n\tstore         *store.Store\n\tauthenticator *auth.Authenticator\n}\n\nfunc NewMCPService(profile *profile.Profile, store *store.Store, secret string) *MCPService {\n\treturn &MCPService{\n\t\tprofile:       profile,\n\t\tstore:         store,\n\t\tauthenticator: auth.NewAuthenticator(store, secret),\n\t}\n}\n\nfunc (s *MCPService) RegisterRoutes(echoServer *echo.Echo) {\n\tmcpSrv := mcpserver.NewMCPServer(\"Memos\", \"1.0.0\",\n\t\tmcpserver.WithToolCapabilities(true),\n\t\tmcpserver.WithResourceCapabilities(true, true),\n\t\tmcpserver.WithPromptCapabilities(true),\n\t\tmcpserver.WithLogging(),\n\t)\n\ts.registerMemoTools(mcpSrv)\n\ts.registerTagTools(mcpSrv)\n\ts.registerAttachmentTools(mcpSrv)\n\ts.registerRelationTools(mcpSrv)\n\ts.registerReactionTools(mcpSrv)\n\ts.registerMemoResources(mcpSrv)\n\ts.registerPrompts(mcpSrv)\n\n\thttpHandler := mcpserver.NewStreamableHTTPServer(mcpSrv)\n\n\tmcpGroup := echoServer.Group(\"\")\n\tmcpGroup.Use(middleware.CORSWithConfig(middleware.CORSConfig{\n\t\tAllowOrigins: []string{\"*\"},\n\t}))\n\tmcpGroup.Use(func(next echo.HandlerFunc) echo.HandlerFunc {\n\t\treturn func(c *echo.Context) error {\n\t\t\tauthHeader := c.Request().Header.Get(\"Authorization\")\n\t\t\tif authHeader != \"\" {\n\t\t\t\tresult := s.authenticator.Authenticate(c.Request().Context(), authHeader)\n\t\t\t\tif result == nil {\n\t\t\t\t\treturn c.JSON(http.StatusUnauthorized, map[string]string{\"message\": \"invalid or expired token\"})\n\t\t\t\t}\n\t\t\t\tctx := auth.ApplyToContext(c.Request().Context(), result)\n\t\t\t\tc.SetRequest(c.Request().WithContext(ctx))\n\t\t\t}\n\t\t\treturn next(c)\n\t\t}\n\t})\n\tmcpGroup.Any(\"/mcp\", echo.WrapHandler(httpHandler))\n}\n"
  },
  {
    "path": "server/router/mcp/prompts.go",
    "content": "package mcp\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/mark3labs/mcp-go/mcp\"\n\tmcpserver \"github.com/mark3labs/mcp-go/server\"\n)\n\nfunc (s *MCPService) registerPrompts(mcpSrv *mcpserver.MCPServer) {\n\t// capture — turns free-form user input into a structured create_memo call.\n\tmcpSrv.AddPrompt(\n\t\tmcp.NewPrompt(\"capture\",\n\t\t\tmcp.WithPromptDescription(\"Capture a thought, idea, or note as a new memo. \"+\n\t\t\t\t\"Use this prompt when the user wants to quickly save something. \"+\n\t\t\t\t\"The assistant will call create_memo with the provided content.\"),\n\t\t\tmcp.WithArgument(\"content\",\n\t\t\t\tmcp.ArgumentDescription(\"The text to save as a memo\"),\n\t\t\t\tmcp.RequiredArgument(),\n\t\t\t),\n\t\t\tmcp.WithArgument(\"tags\",\n\t\t\t\tmcp.ArgumentDescription(\"Comma-separated tags to apply, e.g. \\\"work,project\\\"\"),\n\t\t\t),\n\t\t\tmcp.WithArgument(\"visibility\",\n\t\t\t\tmcp.ArgumentDescription(\"Memo visibility: PRIVATE (default), PROTECTED, or PUBLIC\"),\n\t\t\t),\n\t\t),\n\t\ts.handleCapturePrompt,\n\t)\n\n\t// review — surfaces existing memos on a topic for summarisation.\n\tmcpSrv.AddPrompt(\n\t\tmcp.NewPrompt(\"review\",\n\t\t\tmcp.WithPromptDescription(\"Search and review memos on a given topic. \"+\n\t\t\t\t\"The assistant will call search_memos and summarise the results, \"+\n\t\t\t\t\"including memo resource URIs for easy reference.\"),\n\t\t\tmcp.WithArgument(\"topic\",\n\t\t\t\tmcp.ArgumentDescription(\"Topic or keyword to search for\"),\n\t\t\t\tmcp.RequiredArgument(),\n\t\t\t),\n\t\t),\n\t\ts.handleReviewPrompt,\n\t)\n\n\t// daily_digest — summarise recent activity.\n\tmcpSrv.AddPrompt(\n\t\tmcp.NewPrompt(\"daily_digest\",\n\t\t\tmcp.WithPromptDescription(\"Get a summary of recent memo activity. \"+\n\t\t\t\t\"The assistant will list recent memos, group them by tags, and highlight \"+\n\t\t\t\t\"any incomplete tasks or pinned items.\"),\n\t\t\tmcp.WithArgument(\"days\",\n\t\t\t\tmcp.ArgumentDescription(\"Number of days to look back (default: 1)\"),\n\t\t\t),\n\t\t),\n\t\ts.handleDailyDigestPrompt,\n\t)\n\n\t// organize — suggest tags and relations for untagged memos.\n\tmcpSrv.AddPrompt(\n\t\tmcp.NewPrompt(\"organize\",\n\t\t\tmcp.WithPromptDescription(\"Analyze untagged or loosely organized memos and suggest \"+\n\t\t\t\t\"tags, relations, and groupings to improve discoverability.\"),\n\t\t\tmcp.WithArgument(\"scope\",\n\t\t\t\tmcp.ArgumentDescription(\"Scope of analysis: \\\"untagged\\\" (default) for memos without tags, \\\"all\\\" for all recent memos\"),\n\t\t\t),\n\t\t),\n\t\ts.handleOrganizePrompt,\n\t)\n}\n\nfunc (*MCPService) handleCapturePrompt(_ context.Context, req mcp.GetPromptRequest) (*mcp.GetPromptResult, error) {\n\tcontent := req.Params.Arguments[\"content\"]\n\tif content == \"\" {\n\t\treturn nil, errors.New(\"content argument is required\")\n\t}\n\n\ttags := req.Params.Arguments[\"tags\"]\n\tvisibility := req.Params.Arguments[\"visibility\"]\n\tif visibility == \"\" {\n\t\tvisibility = \"PRIVATE\"\n\t}\n\n\tvar sb strings.Builder\n\tsb.WriteString(\"Save the following as a new memo using the create_memo tool.\\n\\n\")\n\tfmt.Fprintf(&sb, \"Visibility: %s\\n\\n\", visibility)\n\tsb.WriteString(\"Content:\\n\")\n\tsb.WriteString(content)\n\tif tags != \"\" {\n\t\tfmt.Fprintf(&sb, \"\\n\\nAppend these tags inline using #tag syntax: %s\", tags)\n\t}\n\tsb.WriteString(\"\\n\\nAfter creating the memo, confirm by showing the memo resource name (e.g. memo://memos/<uid>) so it can be referenced later.\")\n\n\treturn &mcp.GetPromptResult{\n\t\tDescription: \"Capture a memo\",\n\t\tMessages: []mcp.PromptMessage{\n\t\t\tmcp.NewPromptMessage(mcp.RoleUser, mcp.NewTextContent(sb.String())),\n\t\t},\n\t}, nil\n}\n\nfunc (*MCPService) handleReviewPrompt(_ context.Context, req mcp.GetPromptRequest) (*mcp.GetPromptResult, error) {\n\ttopic := req.Params.Arguments[\"topic\"]\n\tif topic == \"\" {\n\t\treturn nil, errors.New(\"topic argument is required\")\n\t}\n\n\tinstruction := fmt.Sprintf(\n\t\t`Use the search_memos tool to find memos about %q, then:\n\n1. Group results by theme or tag\n2. For each memo, include its resource reference (memo://memos/<uid>) so the user can access it directly\n3. Provide a concise summary of what has been written on this topic\n4. Highlight any memos with incomplete tasks (has_incomplete_tasks)\n5. Note the most recent update times to show currency of the information`,\n\t\ttopic,\n\t)\n\n\treturn &mcp.GetPromptResult{\n\t\tDescription: fmt.Sprintf(\"Review memos about %q\", topic),\n\t\tMessages: []mcp.PromptMessage{\n\t\t\tmcp.NewPromptMessage(mcp.RoleUser, mcp.NewTextContent(instruction)),\n\t\t},\n\t}, nil\n}\n\nfunc (*MCPService) handleDailyDigestPrompt(_ context.Context, req mcp.GetPromptRequest) (*mcp.GetPromptResult, error) {\n\tdays := req.Params.Arguments[\"days\"]\n\tif days == \"\" {\n\t\tdays = \"1\"\n\t}\n\n\tinstruction := fmt.Sprintf(\n\t\t`Generate a daily digest of memo activity from the last %s day(s):\n\n1. Use list_memos to fetch recent memos (order by update time, check multiple pages if needed)\n2. Use list_tags to get the current tag landscape\n3. Group memos by tags and summarize each group\n4. Highlight:\n   - Pinned memos (important items)\n   - Memos with incomplete tasks (action items)\n   - New memos created vs. memos updated\n5. Include memo resource references (memo://memos/<uid>) for each item\n6. End with a brief \"action items\" section listing incomplete tasks across all memos`, days,\n\t)\n\n\treturn &mcp.GetPromptResult{\n\t\tDescription: \"Daily memo digest\",\n\t\tMessages: []mcp.PromptMessage{\n\t\t\tmcp.NewPromptMessage(mcp.RoleUser, mcp.NewTextContent(instruction)),\n\t\t},\n\t}, nil\n}\n\nfunc (*MCPService) handleOrganizePrompt(_ context.Context, req mcp.GetPromptRequest) (*mcp.GetPromptResult, error) {\n\tscope := req.Params.Arguments[\"scope\"]\n\tif scope == \"\" {\n\t\tscope = \"untagged\"\n\t}\n\n\tvar filter string\n\tif scope == \"untagged\" {\n\t\tfilter = `Focus on memos that have no tags. Use list_memos and identify those with empty tag arrays.`\n\t} else {\n\t\tfilter = `Analyze all recent memos regardless of tagging status.`\n\t}\n\n\tinstruction := fmt.Sprintf(\n\t\t`Analyze memos and suggest organizational improvements:\n\n1. %s\n2. Use list_tags to understand the existing tag taxonomy\n3. For each unorganized memo, suggest:\n   - Appropriate tags from the existing taxonomy, or new tags if needed\n   - Potential relations (references) to other memos on similar topics\n4. Present suggestions as a structured list:\n   - Memo: memo://memos/<uid> (first line of content as preview)\n   - Suggested tags: #tag1, #tag2\n   - Related to: memo://memos/<other-uid> (brief reason)\n5. After presenting suggestions, ask the user which changes to apply\n6. Apply approved changes using update_memo (for tags in content) and create_memo_relation (for references)`, filter,\n\t)\n\n\treturn &mcp.GetPromptResult{\n\t\tDescription: fmt.Sprintf(\"Organize memos (scope: %s)\", scope),\n\t\tMessages: []mcp.PromptMessage{\n\t\t\tmcp.NewPromptMessage(mcp.RoleUser, mcp.NewTextContent(instruction)),\n\t\t},\n\t}, nil\n}\n"
  },
  {
    "path": "server/router/mcp/resources_memo.go",
    "content": "package mcp\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/mark3labs/mcp-go/mcp\"\n\tmcpserver \"github.com/mark3labs/mcp-go/server\"\n\t\"github.com/pkg/errors\"\n\n\t\"github.com/usememos/memos/server/auth\"\n\t\"github.com/usememos/memos/store\"\n)\n\n// Memo resource URI scheme: memo://memos/{uid}\n// Clients can read any memo they have access to by URI without calling a tool.\n\nfunc (s *MCPService) registerMemoResources(mcpSrv *mcpserver.MCPServer) {\n\tmcpSrv.AddResourceTemplate(\n\t\tmcp.NewResourceTemplate(\n\t\t\t\"memo://memos/{uid}\",\n\t\t\t\"Memo\",\n\t\t\tmcp.WithTemplateDescription(\"A single Memos note identified by its UID. Returns the memo content as Markdown with a YAML frontmatter header containing metadata.\"),\n\t\t\tmcp.WithTemplateMIMEType(\"text/markdown\"),\n\t\t),\n\t\ts.handleReadMemoResource,\n\t)\n}\n\nfunc (s *MCPService) handleReadMemoResource(ctx context.Context, req mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) {\n\tuserID := auth.GetUserID(ctx)\n\n\t// URI format: memo://memos/{uid}\n\tuid := strings.TrimPrefix(req.Params.URI, \"memo://memos/\")\n\tif uid == req.Params.URI || uid == \"\" {\n\t\treturn nil, errors.Errorf(\"invalid memo URI %q: expected memo://memos/<uid>\", req.Params.URI)\n\t}\n\n\tmemo, err := s.store.GetMemo(ctx, &store.FindMemo{UID: &uid})\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"failed to get memo\")\n\t}\n\tif memo == nil {\n\t\treturn nil, errors.Errorf(\"memo not found: %s\", uid)\n\t}\n\tif err := checkMemoAccess(memo, userID); err != nil {\n\t\treturn nil, err\n\t}\n\n\tj := storeMemoToJSON(memo)\n\ttext := formatMemoMarkdown(j)\n\n\treturn []mcp.ResourceContents{\n\t\tmcp.TextResourceContents{\n\t\t\tURI:      req.Params.URI,\n\t\t\tMIMEType: \"text/markdown\",\n\t\t\tText:     text,\n\t\t},\n\t}, nil\n}\n\n// formatMemoMarkdown renders a memo as Markdown with a YAML frontmatter header.\nfunc formatMemoMarkdown(j memoJSON) string {\n\tvar sb strings.Builder\n\n\tsb.WriteString(\"---\\n\")\n\tfmt.Fprintf(&sb, \"name: %s\\n\", j.Name)\n\tfmt.Fprintf(&sb, \"creator: %s\\n\", j.Creator)\n\tfmt.Fprintf(&sb, \"visibility: %s\\n\", j.Visibility)\n\tfmt.Fprintf(&sb, \"state: %s\\n\", j.State)\n\tfmt.Fprintf(&sb, \"pinned: %v\\n\", j.Pinned)\n\tif len(j.Tags) > 0 {\n\t\tfmt.Fprintf(&sb, \"tags: [%s]\\n\", strings.Join(j.Tags, \", \"))\n\t}\n\tfmt.Fprintf(&sb, \"create_time: %d\\n\", j.CreateTime)\n\tfmt.Fprintf(&sb, \"update_time: %d\\n\", j.UpdateTime)\n\tif j.Parent != \"\" {\n\t\tfmt.Fprintf(&sb, \"parent: %s\\n\", j.Parent)\n\t}\n\tsb.WriteString(\"---\\n\\n\")\n\tsb.WriteString(j.Content)\n\n\treturn sb.String()\n}\n"
  },
  {
    "path": "server/router/mcp/tools_attachment.go",
    "content": "package mcp\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/mark3labs/mcp-go/mcp\"\n\tmcpserver \"github.com/mark3labs/mcp-go/server\"\n\t\"github.com/pkg/errors\"\n\n\tstorepb \"github.com/usememos/memos/proto/gen/store\"\n\t\"github.com/usememos/memos/server/auth\"\n\t\"github.com/usememos/memos/store\"\n)\n\ntype attachmentJSON struct {\n\tName         string `json:\"name\"`\n\tCreator      string `json:\"creator\"`\n\tCreateTime   int64  `json:\"create_time\"`\n\tFilename     string `json:\"filename\"`\n\tType         string `json:\"type\"`\n\tSize         int64  `json:\"size\"`\n\tStorageType  string `json:\"storage_type\"`\n\tExternalLink string `json:\"external_link,omitempty\"`\n\tMemo         string `json:\"memo,omitempty\"`\n}\n\nfunc storeAttachmentToJSON(a *store.Attachment) attachmentJSON {\n\tj := attachmentJSON{\n\t\tName:       \"attachments/\" + a.UID,\n\t\tCreator:    fmt.Sprintf(\"users/%d\", a.CreatorID),\n\t\tCreateTime: a.CreatedTs,\n\t\tFilename:   a.Filename,\n\t\tType:       a.Type,\n\t\tSize:       a.Size,\n\t}\n\tswitch a.StorageType {\n\tcase storepb.AttachmentStorageType_LOCAL:\n\t\tj.StorageType = \"LOCAL\"\n\tcase storepb.AttachmentStorageType_S3:\n\t\tj.StorageType = \"S3\"\n\t\tj.ExternalLink = a.Reference\n\tcase storepb.AttachmentStorageType_EXTERNAL:\n\t\tj.StorageType = \"EXTERNAL\"\n\t\tj.ExternalLink = a.Reference\n\tdefault:\n\t\tj.StorageType = \"DATABASE\"\n\t}\n\tif a.MemoUID != nil && *a.MemoUID != \"\" {\n\t\tj.Memo = \"memos/\" + *a.MemoUID\n\t}\n\treturn j\n}\n\nfunc parseAttachmentUID(name string) (string, error) {\n\tuid, ok := strings.CutPrefix(name, \"attachments/\")\n\tif !ok || uid == \"\" {\n\t\treturn \"\", errors.Errorf(`attachment name must be \"attachments/<uid>\", got %q`, name)\n\t}\n\treturn uid, nil\n}\n\nfunc (s *MCPService) registerAttachmentTools(mcpSrv *mcpserver.MCPServer) {\n\tmcpSrv.AddTool(mcp.NewTool(\"list_attachments\",\n\t\tmcp.WithDescription(\"List attachments owned by the authenticated user. Supports pagination and optional filtering by linked memo.\"),\n\t\tmcp.WithNumber(\"page_size\", mcp.Description(\"Maximum attachments to return (1–100, default 20)\")),\n\t\tmcp.WithNumber(\"page\", mcp.Description(\"Zero-based page index (default 0)\")),\n\t\tmcp.WithString(\"memo\", mcp.Description(`Filter by linked memo resource name, e.g. \"memos/abc123\"`)),\n\t), s.handleListAttachments)\n\n\tmcpSrv.AddTool(mcp.NewTool(\"get_attachment\",\n\t\tmcp.WithDescription(\"Get a single attachment's metadata by resource name. Requires authentication.\"),\n\t\tmcp.WithString(\"name\", mcp.Required(), mcp.Description(`Attachment resource name, e.g. \"attachments/abc123\"`)),\n\t), s.handleGetAttachment)\n\n\tmcpSrv.AddTool(mcp.NewTool(\"delete_attachment\",\n\t\tmcp.WithDescription(\"Permanently delete an attachment and its stored file. Requires authentication and ownership.\"),\n\t\tmcp.WithString(\"name\", mcp.Required(), mcp.Description(`Attachment resource name, e.g. \"attachments/abc123\"`)),\n\t), s.handleDeleteAttachment)\n\n\tmcpSrv.AddTool(mcp.NewTool(\"link_attachment_to_memo\",\n\t\tmcp.WithDescription(\"Link an existing attachment to a memo. Requires authentication and ownership of the attachment.\"),\n\t\tmcp.WithString(\"name\", mcp.Required(), mcp.Description(`Attachment resource name, e.g. \"attachments/abc123\"`)),\n\t\tmcp.WithString(\"memo\", mcp.Required(), mcp.Description(`Memo resource name, e.g. \"memos/abc123\"`)),\n\t), s.handleLinkAttachmentToMemo)\n}\n\nfunc (s *MCPService) handleListAttachments(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {\n\tuserID, err := extractUserID(ctx)\n\tif err != nil {\n\t\treturn mcp.NewToolResultError(err.Error()), nil\n\t}\n\n\tpageSize := req.GetInt(\"page_size\", 20)\n\tif pageSize <= 0 {\n\t\tpageSize = 20\n\t}\n\tif pageSize > 100 {\n\t\tpageSize = 100\n\t}\n\tpage := req.GetInt(\"page\", 0)\n\tif page < 0 {\n\t\tpage = 0\n\t}\n\n\tlimit := pageSize + 1\n\toffset := page * pageSize\n\tfind := &store.FindAttachment{\n\t\tCreatorID: &userID,\n\t\tLimit:     &limit,\n\t\tOffset:    &offset,\n\t}\n\n\tif memoName := req.GetString(\"memo\", \"\"); memoName != \"\" {\n\t\tmemoUID, err := parseMemoUID(memoName)\n\t\tif err != nil {\n\t\t\treturn mcp.NewToolResultError(err.Error()), nil\n\t\t}\n\t\tmemo, err := s.store.GetMemo(ctx, &store.FindMemo{UID: &memoUID})\n\t\tif err != nil {\n\t\t\treturn mcp.NewToolResultError(fmt.Sprintf(\"failed to find memo: %v\", err)), nil\n\t\t}\n\t\tif memo == nil {\n\t\t\treturn mcp.NewToolResultError(\"memo not found\"), nil\n\t\t}\n\t\tfind.MemoID = &memo.ID\n\t}\n\n\tattachments, err := s.store.ListAttachments(ctx, find)\n\tif err != nil {\n\t\treturn mcp.NewToolResultError(fmt.Sprintf(\"failed to list attachments: %v\", err)), nil\n\t}\n\n\thasMore := len(attachments) > pageSize\n\tif hasMore {\n\t\tattachments = attachments[:pageSize]\n\t}\n\n\tresults := make([]attachmentJSON, len(attachments))\n\tfor i, a := range attachments {\n\t\tresults[i] = storeAttachmentToJSON(a)\n\t}\n\n\ttype listResponse struct {\n\t\tAttachments []attachmentJSON `json:\"attachments\"`\n\t\tHasMore     bool             `json:\"has_more\"`\n\t}\n\tout, err := marshalJSON(listResponse{Attachments: results, HasMore: hasMore})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn mcp.NewToolResultText(out), nil\n}\n\nfunc (s *MCPService) handleGetAttachment(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {\n\tuserID := auth.GetUserID(ctx)\n\n\tuid, err := parseAttachmentUID(req.GetString(\"name\", \"\"))\n\tif err != nil {\n\t\treturn mcp.NewToolResultError(err.Error()), nil\n\t}\n\n\tattachment, err := s.store.GetAttachment(ctx, &store.FindAttachment{UID: &uid})\n\tif err != nil {\n\t\treturn mcp.NewToolResultError(fmt.Sprintf(\"failed to get attachment: %v\", err)), nil\n\t}\n\tif attachment == nil {\n\t\treturn mcp.NewToolResultError(\"attachment not found\"), nil\n\t}\n\n\t// Check access: creator can always access; linked memo visibility applies otherwise.\n\tif attachment.CreatorID != userID {\n\t\tif attachment.MemoID != nil {\n\t\t\tmemo, err := s.store.GetMemo(ctx, &store.FindMemo{ID: attachment.MemoID})\n\t\t\tif err != nil {\n\t\t\t\treturn mcp.NewToolResultError(fmt.Sprintf(\"failed to get linked memo: %v\", err)), nil\n\t\t\t}\n\t\t\tif memo != nil {\n\t\t\t\tif err := checkMemoAccess(memo, userID); err != nil {\n\t\t\t\t\treturn mcp.NewToolResultError(err.Error()), nil\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\treturn mcp.NewToolResultError(\"permission denied\"), nil\n\t\t}\n\t}\n\n\tout, err := marshalJSON(storeAttachmentToJSON(attachment))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn mcp.NewToolResultText(out), nil\n}\n\nfunc (s *MCPService) handleDeleteAttachment(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {\n\tuserID, err := extractUserID(ctx)\n\tif err != nil {\n\t\treturn mcp.NewToolResultError(err.Error()), nil\n\t}\n\n\tuid, err := parseAttachmentUID(req.GetString(\"name\", \"\"))\n\tif err != nil {\n\t\treturn mcp.NewToolResultError(err.Error()), nil\n\t}\n\n\tattachment, err := s.store.GetAttachment(ctx, &store.FindAttachment{UID: &uid, CreatorID: &userID})\n\tif err != nil {\n\t\treturn mcp.NewToolResultError(fmt.Sprintf(\"failed to find attachment: %v\", err)), nil\n\t}\n\tif attachment == nil {\n\t\treturn mcp.NewToolResultError(\"attachment not found\"), nil\n\t}\n\n\tif err := s.store.DeleteAttachment(ctx, &store.DeleteAttachment{ID: attachment.ID}); err != nil {\n\t\treturn mcp.NewToolResultError(fmt.Sprintf(\"failed to delete attachment: %v\", err)), nil\n\t}\n\treturn mcp.NewToolResultText(`{\"deleted\":true}`), nil\n}\n\nfunc (s *MCPService) handleLinkAttachmentToMemo(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {\n\tuserID, err := extractUserID(ctx)\n\tif err != nil {\n\t\treturn mcp.NewToolResultError(err.Error()), nil\n\t}\n\n\tuid, err := parseAttachmentUID(req.GetString(\"name\", \"\"))\n\tif err != nil {\n\t\treturn mcp.NewToolResultError(err.Error()), nil\n\t}\n\n\tattachment, err := s.store.GetAttachment(ctx, &store.FindAttachment{UID: &uid})\n\tif err != nil {\n\t\treturn mcp.NewToolResultError(fmt.Sprintf(\"failed to get attachment: %v\", err)), nil\n\t}\n\tif attachment == nil {\n\t\treturn mcp.NewToolResultError(\"attachment not found\"), nil\n\t}\n\tif attachment.CreatorID != userID {\n\t\treturn mcp.NewToolResultError(\"permission denied\"), nil\n\t}\n\n\tmemoUID, err := parseMemoUID(req.GetString(\"memo\", \"\"))\n\tif err != nil {\n\t\treturn mcp.NewToolResultError(err.Error()), nil\n\t}\n\tmemo, err := s.store.GetMemo(ctx, &store.FindMemo{UID: &memoUID})\n\tif err != nil {\n\t\treturn mcp.NewToolResultError(fmt.Sprintf(\"failed to get memo: %v\", err)), nil\n\t}\n\tif memo == nil {\n\t\treturn mcp.NewToolResultError(\"memo not found\"), nil\n\t}\n\n\tif err := s.store.UpdateAttachment(ctx, &store.UpdateAttachment{\n\t\tID:     attachment.ID,\n\t\tMemoID: &memo.ID,\n\t}); err != nil {\n\t\treturn mcp.NewToolResultError(fmt.Sprintf(\"failed to link attachment: %v\", err)), nil\n\t}\n\n\t// Re-fetch to get updated memo UID.\n\tupdated, err := s.store.GetAttachment(ctx, &store.FindAttachment{ID: &attachment.ID})\n\tif err != nil {\n\t\treturn mcp.NewToolResultError(fmt.Sprintf(\"failed to fetch updated attachment: %v\", err)), nil\n\t}\n\tout, err := marshalJSON(storeAttachmentToJSON(updated))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn mcp.NewToolResultText(out), nil\n}\n"
  },
  {
    "path": "server/router/mcp/tools_memo.go",
    "content": "package mcp\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/lithammer/shortuuid/v4\"\n\t\"github.com/mark3labs/mcp-go/mcp\"\n\tmcpserver \"github.com/mark3labs/mcp-go/server\"\n\t\"github.com/pkg/errors\"\n\n\tstorepb \"github.com/usememos/memos/proto/gen/store\"\n\t\"github.com/usememos/memos/server/auth\"\n\t\"github.com/usememos/memos/store\"\n)\n\n// tagRegexp matches #tag patterns in memo content.\n// A tag must start with a letter and contain no whitespace or # characters.\nvar tagRegexp = regexp.MustCompile(`(?:^|\\s)#([A-Za-z][^\\s#]*)`)\n\n// extractTags does a best-effort extraction of #tags from raw markdown content.\n// It is used when creating or updating memos via MCP to pre-populate Payload.Tags.\n// The full markdown service may later rebuild a more accurate payload.\nfunc extractTags(content string) []string {\n\tmatches := tagRegexp.FindAllStringSubmatch(content, -1)\n\tseen := make(map[string]struct{}, len(matches))\n\ttags := make([]string, 0, len(matches))\n\tfor _, m := range matches {\n\t\ttag := m[1]\n\t\tif _, ok := seen[tag]; !ok {\n\t\t\tseen[tag] = struct{}{}\n\t\t\ttags = append(tags, tag)\n\t\t}\n\t}\n\treturn tags\n}\n\n// buildPayload constructs a MemoPayload with tags extracted from content.\n// Returns nil when no tags are found so the store omits the payload entirely.\nfunc buildPayload(content string) *storepb.MemoPayload {\n\ttags := extractTags(content)\n\tif len(tags) == 0 {\n\t\treturn nil\n\t}\n\treturn &storepb.MemoPayload{Tags: tags}\n}\n\n// propertyJSON is the serialisable form of MemoPayload.Property.\ntype propertyJSON struct {\n\tHasLink            bool `json:\"has_link\"`\n\tHasTaskList        bool `json:\"has_task_list\"`\n\tHasCode            bool `json:\"has_code\"`\n\tHasIncompleteTasks bool `json:\"has_incomplete_tasks\"`\n}\n\n// memoJSON is the canonical response shape for all MCP memo results.\n// It serialises correctly with standard encoding/json (no proto marshalling needed).\ntype memoJSON struct {\n\tName       string        `json:\"name\"`\n\tCreator    string        `json:\"creator\"`\n\tCreateTime int64         `json:\"create_time\"`\n\tUpdateTime int64         `json:\"update_time\"`\n\tContent    string        `json:\"content,omitempty\"`\n\tVisibility string        `json:\"visibility\"`\n\tTags       []string      `json:\"tags\"`\n\tPinned     bool          `json:\"pinned\"`\n\tState      string        `json:\"state\"`\n\tProperty   *propertyJSON `json:\"property,omitempty\"`\n\tParent     string        `json:\"parent,omitempty\"`\n}\n\nfunc storeMemoToJSON(m *store.Memo) memoJSON {\n\tj := memoJSON{\n\t\tName:       \"memos/\" + m.UID,\n\t\tCreator:    fmt.Sprintf(\"users/%d\", m.CreatorID),\n\t\tCreateTime: m.CreatedTs,\n\t\tUpdateTime: m.UpdatedTs,\n\t\tContent:    m.Content,\n\t\tVisibility: string(m.Visibility),\n\t\tPinned:     m.Pinned,\n\t\tState:      string(m.RowStatus),\n\t\tTags:       []string{},\n\t}\n\tif m.Payload != nil {\n\t\tif len(m.Payload.Tags) > 0 {\n\t\t\tj.Tags = m.Payload.Tags\n\t\t}\n\t\tif p := m.Payload.Property; p != nil && (p.HasLink || p.HasTaskList || p.HasCode || p.HasIncompleteTasks) {\n\t\t\tj.Property = &propertyJSON{\n\t\t\t\tHasLink:            p.HasLink,\n\t\t\t\tHasTaskList:        p.HasTaskList,\n\t\t\t\tHasCode:            p.HasCode,\n\t\t\t\tHasIncompleteTasks: p.HasIncompleteTasks,\n\t\t\t}\n\t\t}\n\t}\n\tif m.ParentUID != nil {\n\t\tj.Parent = \"memos/\" + *m.ParentUID\n\t}\n\treturn j\n}\n\n// checkMemoAccess returns an error if the caller cannot read memo.\n// userID == 0 means anonymous.\nfunc checkMemoAccess(memo *store.Memo, userID int32) error {\n\tswitch memo.Visibility {\n\tcase store.Protected:\n\t\tif userID == 0 {\n\t\t\treturn errors.New(\"permission denied\")\n\t\t}\n\tcase store.Private:\n\t\tif memo.CreatorID != userID {\n\t\t\treturn errors.New(\"permission denied\")\n\t\t}\n\tdefault:\n\t\t// store.Public and any unknown visibility: allow\n\t}\n\treturn nil\n}\n\n// applyVisibilityFilter restricts find to memos the caller may see.\nfunc applyVisibilityFilter(find *store.FindMemo, userID int32) {\n\tif userID == 0 {\n\t\tfind.VisibilityList = []store.Visibility{store.Public}\n\t} else {\n\t\tfind.Filters = append(find.Filters, fmt.Sprintf(`creator_id == %d || visibility in [\"PUBLIC\", \"PROTECTED\"]`, userID))\n\t}\n}\n\n// parseMemoUID extracts the UID from a \"memos/<uid>\" resource name.\nfunc parseMemoUID(name string) (string, error) {\n\tuid, ok := strings.CutPrefix(name, \"memos/\")\n\tif !ok || uid == \"\" {\n\t\treturn \"\", errors.Errorf(`memo name must be in the format \"memos/<uid>\", got %q`, name)\n\t}\n\treturn uid, nil\n}\n\n// parseVisibility validates a visibility string and returns the store constant.\nfunc parseVisibility(s string) (store.Visibility, error) {\n\tswitch v := store.Visibility(s); v {\n\tcase store.Public, store.Protected, store.Private:\n\t\treturn v, nil\n\tdefault:\n\t\treturn \"\", errors.Errorf(\"visibility must be PRIVATE, PROTECTED, or PUBLIC; got %q\", s)\n\t}\n}\n\n// parseRowStatus validates a state string and returns the store constant.\nfunc parseRowStatus(s string) (store.RowStatus, error) {\n\tswitch rs := store.RowStatus(s); rs {\n\tcase store.Normal, store.Archived:\n\t\treturn rs, nil\n\tdefault:\n\t\treturn \"\", errors.Errorf(\"state must be NORMAL or ARCHIVED; got %q\", s)\n\t}\n}\n\nfunc extractUserID(ctx context.Context) (int32, error) {\n\tid := auth.GetUserID(ctx)\n\tif id == 0 {\n\t\treturn 0, errors.New(\"unauthenticated: a personal access token is required\")\n\t}\n\treturn id, nil\n}\n\nfunc marshalJSON(v any) (string, error) {\n\tb, err := json.Marshal(v)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn string(b), nil\n}\n\nfunc (s *MCPService) registerMemoTools(mcpSrv *mcpserver.MCPServer) {\n\tmcpSrv.AddTool(mcp.NewTool(\"list_memos\",\n\t\tmcp.WithDescription(\"List memos visible to the caller. Authenticated users see their own memos plus public and protected memos; unauthenticated callers see only public memos.\"),\n\t\tmcp.WithNumber(\"page_size\", mcp.Description(\"Maximum memos to return (1–100, default 20)\")),\n\t\tmcp.WithNumber(\"page\", mcp.Description(\"Zero-based page index for pagination (default 0)\")),\n\t\tmcp.WithString(\"state\",\n\t\t\tmcp.Enum(\"NORMAL\", \"ARCHIVED\"),\n\t\t\tmcp.Description(\"Filter by state: NORMAL (default) or ARCHIVED\"),\n\t\t),\n\t\tmcp.WithBoolean(\"order_by_pinned\", mcp.Description(\"When true, pinned memos appear first (default false)\")),\n\t\tmcp.WithString(\"filter\", mcp.Description(`Optional CEL filter, e.g. content.contains(\"keyword\") or tags.exists(t, t == \"work\")`)),\n\t), s.handleListMemos)\n\n\tmcpSrv.AddTool(mcp.NewTool(\"get_memo\",\n\t\tmcp.WithDescription(\"Get a single memo by resource name. Public memos are accessible without authentication.\"),\n\t\tmcp.WithString(\"name\", mcp.Required(), mcp.Description(`Memo resource name, e.g. \"memos/abc123\"`)),\n\t), s.handleGetMemo)\n\n\tmcpSrv.AddTool(mcp.NewTool(\"create_memo\",\n\t\tmcp.WithDescription(\"Create a new memo. Requires authentication.\"),\n\t\tmcp.WithString(\"content\", mcp.Required(), mcp.Description(\"Memo content in Markdown. Use #tag syntax for tagging.\")),\n\t\tmcp.WithString(\"visibility\",\n\t\t\tmcp.Enum(\"PRIVATE\", \"PROTECTED\", \"PUBLIC\"),\n\t\t\tmcp.Description(\"Visibility (default: PRIVATE)\"),\n\t\t),\n\t), s.handleCreateMemo)\n\n\tmcpSrv.AddTool(mcp.NewTool(\"update_memo\",\n\t\tmcp.WithDescription(\"Update a memo's content, visibility, pin state, or archive state. Requires authentication and ownership. Omit any field to leave it unchanged.\"),\n\t\tmcp.WithString(\"name\", mcp.Required(), mcp.Description(`Memo resource name, e.g. \"memos/abc123\"`)),\n\t\tmcp.WithString(\"content\", mcp.Description(\"New Markdown content\")),\n\t\tmcp.WithString(\"visibility\",\n\t\t\tmcp.Enum(\"PRIVATE\", \"PROTECTED\", \"PUBLIC\"),\n\t\t\tmcp.Description(\"New visibility\"),\n\t\t),\n\t\tmcp.WithBoolean(\"pinned\", mcp.Description(\"Pin or unpin the memo\")),\n\t\tmcp.WithString(\"state\",\n\t\t\tmcp.Enum(\"NORMAL\", \"ARCHIVED\"),\n\t\t\tmcp.Description(\"Set to ARCHIVED to archive, NORMAL to restore\"),\n\t\t),\n\t), s.handleUpdateMemo)\n\n\tmcpSrv.AddTool(mcp.NewTool(\"delete_memo\",\n\t\tmcp.WithDescription(\"Permanently delete a memo. Requires authentication and ownership.\"),\n\t\tmcp.WithString(\"name\", mcp.Required(), mcp.Description(`Memo resource name, e.g. \"memos/abc123\"`)),\n\t), s.handleDeleteMemo)\n\n\tmcpSrv.AddTool(mcp.NewTool(\"search_memos\",\n\t\tmcp.WithDescription(\"Search memo content. Authenticated users search their own and visible memos; unauthenticated callers search public memos only.\"),\n\t\tmcp.WithString(\"query\", mcp.Required(), mcp.Description(\"Text to search for in memo content\")),\n\t), s.handleSearchMemos)\n\n\tmcpSrv.AddTool(mcp.NewTool(\"list_memo_comments\",\n\t\tmcp.WithDescription(\"List comments on a memo. Visibility rules for comments match those of the parent memo.\"),\n\t\tmcp.WithString(\"name\", mcp.Required(), mcp.Description(`Memo resource name, e.g. \"memos/abc123\"`)),\n\t), s.handleListMemoComments)\n\n\tmcpSrv.AddTool(mcp.NewTool(\"create_memo_comment\",\n\t\tmcp.WithDescription(\"Add a comment to a memo. The comment inherits the parent memo's visibility. Requires authentication.\"),\n\t\tmcp.WithString(\"name\", mcp.Required(), mcp.Description(`Memo resource name to comment on, e.g. \"memos/abc123\"`)),\n\t\tmcp.WithString(\"content\", mcp.Required(), mcp.Description(\"Comment content in Markdown\")),\n\t), s.handleCreateMemoComment)\n}\n\nfunc (s *MCPService) handleListMemos(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {\n\tuserID := auth.GetUserID(ctx)\n\n\tpageSize := req.GetInt(\"page_size\", 20)\n\tif pageSize <= 0 {\n\t\tpageSize = 20\n\t}\n\tif pageSize > 100 {\n\t\tpageSize = 100\n\t}\n\tpage := req.GetInt(\"page\", 0)\n\tif page < 0 {\n\t\tpage = 0\n\t}\n\n\tvar rowStatus *store.RowStatus\n\tif state := req.GetString(\"state\", \"NORMAL\"); state != \"\" {\n\t\trs, err := parseRowStatus(state)\n\t\tif err != nil {\n\t\t\treturn mcp.NewToolResultError(err.Error()), nil\n\t\t}\n\t\trowStatus = &rs\n\t}\n\n\tlimit := pageSize + 1\n\toffset := page * pageSize\n\tfind := &store.FindMemo{\n\t\tExcludeComments: true,\n\t\tRowStatus:       rowStatus,\n\t\tLimit:           &limit,\n\t\tOffset:          &offset,\n\t\tOrderByPinned:   req.GetBool(\"order_by_pinned\", false),\n\t}\n\tapplyVisibilityFilter(find, userID)\n\tif filter := req.GetString(\"filter\", \"\"); filter != \"\" {\n\t\tfind.Filters = append(find.Filters, filter)\n\t}\n\n\tmemos, err := s.store.ListMemos(ctx, find)\n\tif err != nil {\n\t\treturn mcp.NewToolResultError(fmt.Sprintf(\"failed to list memos: %v\", err)), nil\n\t}\n\n\thasMore := len(memos) > pageSize\n\tif hasMore {\n\t\tmemos = memos[:pageSize]\n\t}\n\n\tresults := make([]memoJSON, len(memos))\n\tfor i, m := range memos {\n\t\tresults[i] = storeMemoToJSON(m)\n\t}\n\n\ttype listResponse struct {\n\t\tMemos   []memoJSON `json:\"memos\"`\n\t\tHasMore bool       `json:\"has_more\"`\n\t}\n\tout, err := marshalJSON(listResponse{Memos: results, HasMore: hasMore})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn mcp.NewToolResultText(out), nil\n}\n\nfunc (s *MCPService) handleGetMemo(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {\n\tuserID := auth.GetUserID(ctx)\n\n\tuid, err := parseMemoUID(req.GetString(\"name\", \"\"))\n\tif err != nil {\n\t\treturn mcp.NewToolResultError(err.Error()), nil\n\t}\n\n\tmemo, err := s.store.GetMemo(ctx, &store.FindMemo{UID: &uid})\n\tif err != nil {\n\t\treturn mcp.NewToolResultError(fmt.Sprintf(\"failed to get memo: %v\", err)), nil\n\t}\n\tif memo == nil {\n\t\treturn mcp.NewToolResultError(\"memo not found\"), nil\n\t}\n\tif err := checkMemoAccess(memo, userID); err != nil {\n\t\treturn mcp.NewToolResultError(err.Error()), nil\n\t}\n\n\tout, err := marshalJSON(storeMemoToJSON(memo))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn mcp.NewToolResultText(out), nil\n}\n\nfunc (s *MCPService) handleCreateMemo(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {\n\tuserID, err := extractUserID(ctx)\n\tif err != nil {\n\t\treturn mcp.NewToolResultError(err.Error()), nil\n\t}\n\n\tcontent := req.GetString(\"content\", \"\")\n\tif content == \"\" {\n\t\treturn mcp.NewToolResultError(\"content is required\"), nil\n\t}\n\tvisibility, err := parseVisibility(req.GetString(\"visibility\", \"PRIVATE\"))\n\tif err != nil {\n\t\treturn mcp.NewToolResultError(err.Error()), nil\n\t}\n\n\tmemo, err := s.store.CreateMemo(ctx, &store.Memo{\n\t\tUID:        shortuuid.New(),\n\t\tCreatorID:  userID,\n\t\tContent:    content,\n\t\tVisibility: visibility,\n\t\tPayload:    buildPayload(content),\n\t})\n\tif err != nil {\n\t\treturn mcp.NewToolResultError(fmt.Sprintf(\"failed to create memo: %v\", err)), nil\n\t}\n\n\tout, err := marshalJSON(storeMemoToJSON(memo))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn mcp.NewToolResultText(out), nil\n}\n\nfunc (s *MCPService) handleUpdateMemo(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {\n\tuserID, err := extractUserID(ctx)\n\tif err != nil {\n\t\treturn mcp.NewToolResultError(err.Error()), nil\n\t}\n\n\tuid, err := parseMemoUID(req.GetString(\"name\", \"\"))\n\tif err != nil {\n\t\treturn mcp.NewToolResultError(err.Error()), nil\n\t}\n\n\tmemo, err := s.store.GetMemo(ctx, &store.FindMemo{UID: &uid})\n\tif err != nil {\n\t\treturn mcp.NewToolResultError(fmt.Sprintf(\"failed to get memo: %v\", err)), nil\n\t}\n\tif memo == nil {\n\t\treturn mcp.NewToolResultError(\"memo not found\"), nil\n\t}\n\tif memo.CreatorID != userID {\n\t\treturn mcp.NewToolResultError(\"permission denied\"), nil\n\t}\n\n\tupdate := &store.UpdateMemo{ID: memo.ID}\n\targs := req.GetArguments()\n\n\tif v := req.GetString(\"content\", \"\"); v != \"\" {\n\t\tupdate.Content = &v\n\t\tupdate.Payload = buildPayload(v)\n\t}\n\tif v := req.GetString(\"visibility\", \"\"); v != \"\" {\n\t\tvis, err := parseVisibility(v)\n\t\tif err != nil {\n\t\t\treturn mcp.NewToolResultError(err.Error()), nil\n\t\t}\n\t\tupdate.Visibility = &vis\n\t}\n\tif v := req.GetString(\"state\", \"\"); v != \"\" {\n\t\trs, err := parseRowStatus(v)\n\t\tif err != nil {\n\t\t\treturn mcp.NewToolResultError(err.Error()), nil\n\t\t}\n\t\tupdate.RowStatus = &rs\n\t}\n\tif _, ok := args[\"pinned\"]; ok {\n\t\tpinned := req.GetBool(\"pinned\", false)\n\t\tupdate.Pinned = &pinned\n\t}\n\n\tif err := s.store.UpdateMemo(ctx, update); err != nil {\n\t\treturn mcp.NewToolResultError(fmt.Sprintf(\"failed to update memo: %v\", err)), nil\n\t}\n\n\tupdated, err := s.store.GetMemo(ctx, &store.FindMemo{ID: &memo.ID})\n\tif err != nil {\n\t\treturn mcp.NewToolResultError(fmt.Sprintf(\"failed to fetch updated memo: %v\", err)), nil\n\t}\n\n\tout, err := marshalJSON(storeMemoToJSON(updated))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn mcp.NewToolResultText(out), nil\n}\n\nfunc (s *MCPService) handleDeleteMemo(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {\n\tuserID, err := extractUserID(ctx)\n\tif err != nil {\n\t\treturn mcp.NewToolResultError(err.Error()), nil\n\t}\n\n\tuid, err := parseMemoUID(req.GetString(\"name\", \"\"))\n\tif err != nil {\n\t\treturn mcp.NewToolResultError(err.Error()), nil\n\t}\n\n\tmemo, err := s.store.GetMemo(ctx, &store.FindMemo{UID: &uid})\n\tif err != nil {\n\t\treturn mcp.NewToolResultError(fmt.Sprintf(\"failed to get memo: %v\", err)), nil\n\t}\n\tif memo == nil {\n\t\treturn mcp.NewToolResultError(\"memo not found\"), nil\n\t}\n\tif memo.CreatorID != userID {\n\t\treturn mcp.NewToolResultError(\"permission denied\"), nil\n\t}\n\n\tif err := s.store.DeleteMemo(ctx, &store.DeleteMemo{ID: memo.ID}); err != nil {\n\t\treturn mcp.NewToolResultError(fmt.Sprintf(\"failed to delete memo: %v\", err)), nil\n\t}\n\treturn mcp.NewToolResultText(`{\"deleted\":true}`), nil\n}\n\nfunc (s *MCPService) handleSearchMemos(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {\n\tuserID := auth.GetUserID(ctx)\n\n\tquery := req.GetString(\"query\", \"\")\n\tif query == \"\" {\n\t\treturn mcp.NewToolResultError(\"query is required\"), nil\n\t}\n\n\tlimit := 50\n\tzero := 0\n\trowStatus := store.Normal\n\tfind := &store.FindMemo{\n\t\tExcludeComments: true,\n\t\tRowStatus:       &rowStatus,\n\t\tLimit:           &limit,\n\t\tOffset:          &zero,\n\t\tFilters:         []string{fmt.Sprintf(`content.contains(%q)`, query)},\n\t}\n\tapplyVisibilityFilter(find, userID)\n\n\tmemos, err := s.store.ListMemos(ctx, find)\n\tif err != nil {\n\t\treturn mcp.NewToolResultError(fmt.Sprintf(\"failed to search memos: %v\", err)), nil\n\t}\n\n\tresults := make([]memoJSON, len(memos))\n\tfor i, m := range memos {\n\t\tresults[i] = storeMemoToJSON(m)\n\t}\n\tout, err := marshalJSON(results)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn mcp.NewToolResultText(out), nil\n}\n\nfunc (s *MCPService) handleListMemoComments(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {\n\tuserID := auth.GetUserID(ctx)\n\n\tuid, err := parseMemoUID(req.GetString(\"name\", \"\"))\n\tif err != nil {\n\t\treturn mcp.NewToolResultError(err.Error()), nil\n\t}\n\n\tparent, err := s.store.GetMemo(ctx, &store.FindMemo{UID: &uid})\n\tif err != nil {\n\t\treturn mcp.NewToolResultError(fmt.Sprintf(\"failed to get memo: %v\", err)), nil\n\t}\n\tif parent == nil {\n\t\treturn mcp.NewToolResultError(\"memo not found\"), nil\n\t}\n\tif err := checkMemoAccess(parent, userID); err != nil {\n\t\treturn mcp.NewToolResultError(err.Error()), nil\n\t}\n\n\trelationType := store.MemoRelationComment\n\trelations, err := s.store.ListMemoRelations(ctx, &store.FindMemoRelation{\n\t\tRelatedMemoID: &parent.ID,\n\t\tType:          &relationType,\n\t})\n\tif err != nil {\n\t\treturn mcp.NewToolResultError(fmt.Sprintf(\"failed to list relations: %v\", err)), nil\n\t}\n\tif len(relations) == 0 {\n\t\tout, _ := marshalJSON([]memoJSON{})\n\t\treturn mcp.NewToolResultText(out), nil\n\t}\n\n\tcommentIDs := make([]int32, len(relations))\n\tfor i, r := range relations {\n\t\tcommentIDs[i] = r.MemoID\n\t}\n\n\tmemos, err := s.store.ListMemos(ctx, &store.FindMemo{IDList: commentIDs})\n\tif err != nil {\n\t\treturn mcp.NewToolResultError(fmt.Sprintf(\"failed to list comments: %v\", err)), nil\n\t}\n\n\tresults := make([]memoJSON, 0, len(memos))\n\tfor _, m := range memos {\n\t\tif checkMemoAccess(m, userID) == nil {\n\t\t\tresults = append(results, storeMemoToJSON(m))\n\t\t}\n\t}\n\tout, err := marshalJSON(results)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn mcp.NewToolResultText(out), nil\n}\n\nfunc (s *MCPService) handleCreateMemoComment(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {\n\tuserID, err := extractUserID(ctx)\n\tif err != nil {\n\t\treturn mcp.NewToolResultError(err.Error()), nil\n\t}\n\n\tuid, err := parseMemoUID(req.GetString(\"name\", \"\"))\n\tif err != nil {\n\t\treturn mcp.NewToolResultError(err.Error()), nil\n\t}\n\tcontent := req.GetString(\"content\", \"\")\n\tif content == \"\" {\n\t\treturn mcp.NewToolResultError(\"content is required\"), nil\n\t}\n\n\tparent, err := s.store.GetMemo(ctx, &store.FindMemo{UID: &uid})\n\tif err != nil {\n\t\treturn mcp.NewToolResultError(fmt.Sprintf(\"failed to get memo: %v\", err)), nil\n\t}\n\tif parent == nil {\n\t\treturn mcp.NewToolResultError(\"memo not found\"), nil\n\t}\n\tif err := checkMemoAccess(parent, userID); err != nil {\n\t\treturn mcp.NewToolResultError(err.Error()), nil\n\t}\n\n\tcomment, err := s.store.CreateMemo(ctx, &store.Memo{\n\t\tUID:        shortuuid.New(),\n\t\tCreatorID:  userID,\n\t\tContent:    content,\n\t\tVisibility: parent.Visibility,\n\t\tPayload:    buildPayload(content),\n\t\tParentUID:  &parent.UID,\n\t})\n\tif err != nil {\n\t\treturn mcp.NewToolResultError(fmt.Sprintf(\"failed to create comment: %v\", err)), nil\n\t}\n\n\tif _, err = s.store.UpsertMemoRelation(ctx, &store.MemoRelation{\n\t\tMemoID:        comment.ID,\n\t\tRelatedMemoID: parent.ID,\n\t\tType:          store.MemoRelationComment,\n\t}); err != nil {\n\t\treturn mcp.NewToolResultError(fmt.Sprintf(\"failed to link comment: %v\", err)), nil\n\t}\n\n\tout, err := marshalJSON(storeMemoToJSON(comment))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn mcp.NewToolResultText(out), nil\n}\n"
  },
  {
    "path": "server/router/mcp/tools_reaction.go",
    "content": "package mcp\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/mark3labs/mcp-go/mcp\"\n\tmcpserver \"github.com/mark3labs/mcp-go/server\"\n\n\t\"github.com/usememos/memos/server/auth\"\n\t\"github.com/usememos/memos/store\"\n)\n\ntype reactionJSON struct {\n\tID           int32  `json:\"id\"`\n\tCreator      string `json:\"creator\"`\n\tReactionType string `json:\"reaction_type\"`\n\tCreateTime   int64  `json:\"create_time\"`\n}\n\nfunc (s *MCPService) registerReactionTools(mcpSrv *mcpserver.MCPServer) {\n\tmcpSrv.AddTool(mcp.NewTool(\"list_reactions\",\n\t\tmcp.WithDescription(\"List all reactions on a memo. Returns reaction type and creator for each reaction.\"),\n\t\tmcp.WithString(\"name\", mcp.Required(), mcp.Description(`Memo resource name, e.g. \"memos/abc123\"`)),\n\t), s.handleListReactions)\n\n\tmcpSrv.AddTool(mcp.NewTool(\"upsert_reaction\",\n\t\tmcp.WithDescription(\"Add a reaction (emoji) to a memo. If the same reaction already exists from the same user, this is a no-op. Requires authentication.\"),\n\t\tmcp.WithString(\"name\", mcp.Required(), mcp.Description(`Memo resource name, e.g. \"memos/abc123\"`)),\n\t\tmcp.WithString(\"reaction_type\", mcp.Required(), mcp.Description(`Reaction emoji, e.g. \"👍\", \"❤️\", \"🎉\"`)),\n\t), s.handleUpsertReaction)\n\n\tmcpSrv.AddTool(mcp.NewTool(\"delete_reaction\",\n\t\tmcp.WithDescription(\"Remove a reaction by its ID. Requires authentication and ownership of the reaction.\"),\n\t\tmcp.WithNumber(\"id\", mcp.Required(), mcp.Description(\"Reaction ID to delete\")),\n\t), s.handleDeleteReaction)\n}\n\nfunc (s *MCPService) handleListReactions(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {\n\tuserID := auth.GetUserID(ctx)\n\n\tuid, err := parseMemoUID(req.GetString(\"name\", \"\"))\n\tif err != nil {\n\t\treturn mcp.NewToolResultError(err.Error()), nil\n\t}\n\n\tmemo, err := s.store.GetMemo(ctx, &store.FindMemo{UID: &uid})\n\tif err != nil {\n\t\treturn mcp.NewToolResultError(fmt.Sprintf(\"failed to get memo: %v\", err)), nil\n\t}\n\tif memo == nil {\n\t\treturn mcp.NewToolResultError(\"memo not found\"), nil\n\t}\n\tif err := checkMemoAccess(memo, userID); err != nil {\n\t\treturn mcp.NewToolResultError(err.Error()), nil\n\t}\n\n\tcontentID := \"memos/\" + uid\n\treactions, err := s.store.ListReactions(ctx, &store.FindReaction{ContentID: &contentID})\n\tif err != nil {\n\t\treturn mcp.NewToolResultError(fmt.Sprintf(\"failed to list reactions: %v\", err)), nil\n\t}\n\n\tresults := make([]reactionJSON, len(reactions))\n\tfor i, r := range reactions {\n\t\tresults[i] = reactionJSON{\n\t\t\tID:           r.ID,\n\t\t\tCreator:      fmt.Sprintf(\"users/%d\", r.CreatorID),\n\t\t\tReactionType: r.ReactionType,\n\t\t\tCreateTime:   r.CreatedTs,\n\t\t}\n\t}\n\n\tout, err := marshalJSON(results)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn mcp.NewToolResultText(out), nil\n}\n\nfunc (s *MCPService) handleUpsertReaction(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {\n\tuserID, err := extractUserID(ctx)\n\tif err != nil {\n\t\treturn mcp.NewToolResultError(err.Error()), nil\n\t}\n\n\tuid, err := parseMemoUID(req.GetString(\"name\", \"\"))\n\tif err != nil {\n\t\treturn mcp.NewToolResultError(err.Error()), nil\n\t}\n\treactionType := req.GetString(\"reaction_type\", \"\")\n\tif reactionType == \"\" {\n\t\treturn mcp.NewToolResultError(\"reaction_type is required\"), nil\n\t}\n\n\tmemo, err := s.store.GetMemo(ctx, &store.FindMemo{UID: &uid})\n\tif err != nil {\n\t\treturn mcp.NewToolResultError(fmt.Sprintf(\"failed to get memo: %v\", err)), nil\n\t}\n\tif memo == nil {\n\t\treturn mcp.NewToolResultError(\"memo not found\"), nil\n\t}\n\tif err := checkMemoAccess(memo, userID); err != nil {\n\t\treturn mcp.NewToolResultError(err.Error()), nil\n\t}\n\n\t// Validate reaction type against allowed reactions.\n\tmemoRelatedSetting, err := s.store.GetInstanceMemoRelatedSetting(ctx)\n\tif err != nil {\n\t\treturn mcp.NewToolResultError(fmt.Sprintf(\"failed to get reaction settings: %v\", err)), nil\n\t}\n\tallowed := false\n\tfor _, r := range memoRelatedSetting.Reactions {\n\t\tif r == reactionType {\n\t\t\tallowed = true\n\t\t\tbreak\n\t\t}\n\t}\n\tif !allowed {\n\t\treturn mcp.NewToolResultError(fmt.Sprintf(\"reaction %q is not in the allowed reaction list\", reactionType)), nil\n\t}\n\n\tcontentID := \"memos/\" + uid\n\treaction, err := s.store.UpsertReaction(ctx, &store.Reaction{\n\t\tCreatorID:    userID,\n\t\tContentID:    contentID,\n\t\tReactionType: reactionType,\n\t})\n\tif err != nil {\n\t\treturn mcp.NewToolResultError(fmt.Sprintf(\"failed to upsert reaction: %v\", err)), nil\n\t}\n\n\tout, err := marshalJSON(reactionJSON{\n\t\tID:           reaction.ID,\n\t\tCreator:      fmt.Sprintf(\"users/%d\", reaction.CreatorID),\n\t\tReactionType: reaction.ReactionType,\n\t\tCreateTime:   reaction.CreatedTs,\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn mcp.NewToolResultText(out), nil\n}\n\nfunc (s *MCPService) handleDeleteReaction(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {\n\tuserID, err := extractUserID(ctx)\n\tif err != nil {\n\t\treturn mcp.NewToolResultError(err.Error()), nil\n\t}\n\n\treactionID := int32(req.GetInt(\"id\", 0))\n\tif reactionID == 0 {\n\t\treturn mcp.NewToolResultError(\"id is required\"), nil\n\t}\n\n\treaction, err := s.store.GetReaction(ctx, &store.FindReaction{ID: &reactionID})\n\tif err != nil {\n\t\treturn mcp.NewToolResultError(fmt.Sprintf(\"failed to get reaction: %v\", err)), nil\n\t}\n\tif reaction == nil {\n\t\treturn mcp.NewToolResultError(\"reaction not found\"), nil\n\t}\n\tif reaction.CreatorID != userID {\n\t\treturn mcp.NewToolResultError(\"permission denied: can only delete your own reactions\"), nil\n\t}\n\n\tif err := s.store.DeleteReaction(ctx, &store.DeleteReaction{ID: reactionID}); err != nil {\n\t\treturn mcp.NewToolResultError(fmt.Sprintf(\"failed to delete reaction: %v\", err)), nil\n\t}\n\treturn mcp.NewToolResultText(`{\"deleted\":true}`), nil\n}\n"
  },
  {
    "path": "server/router/mcp/tools_relation.go",
    "content": "package mcp\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/mark3labs/mcp-go/mcp\"\n\tmcpserver \"github.com/mark3labs/mcp-go/server\"\n\n\t\"github.com/usememos/memos/store\"\n)\n\ntype relationJSON struct {\n\tMemo        string `json:\"memo\"`\n\tRelatedMemo string `json:\"related_memo\"`\n\tType        string `json:\"type\"`\n}\n\nfunc (s *MCPService) registerRelationTools(mcpSrv *mcpserver.MCPServer) {\n\tmcpSrv.AddTool(mcp.NewTool(\"list_memo_relations\",\n\t\tmcp.WithDescription(\"List all relations (references and comments) for a memo. Requires read access to the memo.\"),\n\t\tmcp.WithString(\"name\", mcp.Required(), mcp.Description(`Memo resource name, e.g. \"memos/abc123\"`)),\n\t\tmcp.WithString(\"type\",\n\t\t\tmcp.Enum(\"REFERENCE\", \"COMMENT\"),\n\t\t\tmcp.Description(\"Filter by relation type (optional)\"),\n\t\t),\n\t), s.handleListMemoRelations)\n\n\tmcpSrv.AddTool(mcp.NewTool(\"create_memo_relation\",\n\t\tmcp.WithDescription(\"Create a reference relation between two memos. Requires authentication. For comments, use create_memo_comment instead.\"),\n\t\tmcp.WithString(\"name\", mcp.Required(), mcp.Description(`Source memo resource name, e.g. \"memos/abc123\"`)),\n\t\tmcp.WithString(\"related_memo\", mcp.Required(), mcp.Description(`Target memo resource name, e.g. \"memos/def456\"`)),\n\t), s.handleCreateMemoRelation)\n\n\tmcpSrv.AddTool(mcp.NewTool(\"delete_memo_relation\",\n\t\tmcp.WithDescription(\"Delete a reference relation between two memos. Requires authentication and ownership of the source memo.\"),\n\t\tmcp.WithString(\"name\", mcp.Required(), mcp.Description(`Source memo resource name, e.g. \"memos/abc123\"`)),\n\t\tmcp.WithString(\"related_memo\", mcp.Required(), mcp.Description(`Target memo resource name, e.g. \"memos/def456\"`)),\n\t), s.handleDeleteMemoRelation)\n}\n\nfunc (s *MCPService) handleListMemoRelations(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {\n\tuid, err := parseMemoUID(req.GetString(\"name\", \"\"))\n\tif err != nil {\n\t\treturn mcp.NewToolResultError(err.Error()), nil\n\t}\n\n\tmemo, err := s.store.GetMemo(ctx, &store.FindMemo{UID: &uid})\n\tif err != nil {\n\t\treturn mcp.NewToolResultError(fmt.Sprintf(\"failed to get memo: %v\", err)), nil\n\t}\n\tif memo == nil {\n\t\treturn mcp.NewToolResultError(\"memo not found\"), nil\n\t}\n\n\tfind := &store.FindMemoRelation{\n\t\tMemoIDList: []int32{memo.ID},\n\t}\n\tif typeStr := req.GetString(\"type\", \"\"); typeStr != \"\" {\n\t\tswitch store.MemoRelationType(typeStr) {\n\t\tcase store.MemoRelationReference, store.MemoRelationComment:\n\t\t\tt := store.MemoRelationType(typeStr)\n\t\t\tfind.Type = &t\n\t\tdefault:\n\t\t\treturn mcp.NewToolResultError(fmt.Sprintf(\"type must be REFERENCE or COMMENT, got %q\", typeStr)), nil\n\t\t}\n\t}\n\n\trelations, err := s.store.ListMemoRelations(ctx, find)\n\tif err != nil {\n\t\treturn mcp.NewToolResultError(fmt.Sprintf(\"failed to list relations: %v\", err)), nil\n\t}\n\n\t// Resolve memo IDs to UIDs.\n\tidSet := make(map[int32]struct{})\n\tfor _, r := range relations {\n\t\tidSet[r.MemoID] = struct{}{}\n\t\tidSet[r.RelatedMemoID] = struct{}{}\n\t}\n\tids := make([]int32, 0, len(idSet))\n\tfor id := range idSet {\n\t\tids = append(ids, id)\n\t}\n\tmemos, err := s.store.ListMemos(ctx, &store.FindMemo{IDList: ids, ExcludeContent: true})\n\tif err != nil {\n\t\treturn mcp.NewToolResultError(fmt.Sprintf(\"failed to resolve memos: %v\", err)), nil\n\t}\n\tuidByID := make(map[int32]string, len(memos))\n\tfor _, m := range memos {\n\t\tuidByID[m.ID] = m.UID\n\t}\n\n\tresults := make([]relationJSON, 0, len(relations))\n\tfor _, r := range relations {\n\t\tmemoUID, ok1 := uidByID[r.MemoID]\n\t\trelatedUID, ok2 := uidByID[r.RelatedMemoID]\n\t\tif !ok1 || !ok2 {\n\t\t\tcontinue\n\t\t}\n\t\tresults = append(results, relationJSON{\n\t\t\tMemo:        \"memos/\" + memoUID,\n\t\t\tRelatedMemo: \"memos/\" + relatedUID,\n\t\t\tType:        string(r.Type),\n\t\t})\n\t}\n\n\tout, err := marshalJSON(results)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn mcp.NewToolResultText(out), nil\n}\n\nfunc (s *MCPService) handleCreateMemoRelation(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {\n\tuserID, err := extractUserID(ctx)\n\tif err != nil {\n\t\treturn mcp.NewToolResultError(err.Error()), nil\n\t}\n\n\tsrcUID, err := parseMemoUID(req.GetString(\"name\", \"\"))\n\tif err != nil {\n\t\treturn mcp.NewToolResultError(err.Error()), nil\n\t}\n\tdstUID, err := parseMemoUID(req.GetString(\"related_memo\", \"\"))\n\tif err != nil {\n\t\treturn mcp.NewToolResultError(err.Error()), nil\n\t}\n\n\tsrcMemo, err := s.store.GetMemo(ctx, &store.FindMemo{UID: &srcUID})\n\tif err != nil {\n\t\treturn mcp.NewToolResultError(fmt.Sprintf(\"failed to get source memo: %v\", err)), nil\n\t}\n\tif srcMemo == nil {\n\t\treturn mcp.NewToolResultError(\"source memo not found\"), nil\n\t}\n\tif srcMemo.CreatorID != userID {\n\t\treturn mcp.NewToolResultError(\"permission denied: must own the source memo\"), nil\n\t}\n\n\tdstMemo, err := s.store.GetMemo(ctx, &store.FindMemo{UID: &dstUID})\n\tif err != nil {\n\t\treturn mcp.NewToolResultError(fmt.Sprintf(\"failed to get related memo: %v\", err)), nil\n\t}\n\tif dstMemo == nil {\n\t\treturn mcp.NewToolResultError(\"related memo not found\"), nil\n\t}\n\n\trelation, err := s.store.UpsertMemoRelation(ctx, &store.MemoRelation{\n\t\tMemoID:        srcMemo.ID,\n\t\tRelatedMemoID: dstMemo.ID,\n\t\tType:          store.MemoRelationReference,\n\t})\n\tif err != nil {\n\t\treturn mcp.NewToolResultError(fmt.Sprintf(\"failed to create relation: %v\", err)), nil\n\t}\n\n\tout, err := marshalJSON(relationJSON{\n\t\tMemo:        \"memos/\" + srcUID,\n\t\tRelatedMemo: \"memos/\" + dstUID,\n\t\tType:        string(relation.Type),\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn mcp.NewToolResultText(out), nil\n}\n\nfunc (s *MCPService) handleDeleteMemoRelation(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {\n\tuserID, err := extractUserID(ctx)\n\tif err != nil {\n\t\treturn mcp.NewToolResultError(err.Error()), nil\n\t}\n\n\tsrcUID, err := parseMemoUID(req.GetString(\"name\", \"\"))\n\tif err != nil {\n\t\treturn mcp.NewToolResultError(err.Error()), nil\n\t}\n\tdstUID, err := parseMemoUID(req.GetString(\"related_memo\", \"\"))\n\tif err != nil {\n\t\treturn mcp.NewToolResultError(err.Error()), nil\n\t}\n\n\tsrcMemo, err := s.store.GetMemo(ctx, &store.FindMemo{UID: &srcUID})\n\tif err != nil {\n\t\treturn mcp.NewToolResultError(fmt.Sprintf(\"failed to get source memo: %v\", err)), nil\n\t}\n\tif srcMemo == nil {\n\t\treturn mcp.NewToolResultError(\"source memo not found\"), nil\n\t}\n\tif srcMemo.CreatorID != userID {\n\t\treturn mcp.NewToolResultError(\"permission denied: must own the source memo\"), nil\n\t}\n\n\tdstMemo, err := s.store.GetMemo(ctx, &store.FindMemo{UID: &dstUID})\n\tif err != nil {\n\t\treturn mcp.NewToolResultError(fmt.Sprintf(\"failed to get related memo: %v\", err)), nil\n\t}\n\tif dstMemo == nil {\n\t\treturn mcp.NewToolResultError(\"related memo not found\"), nil\n\t}\n\n\trefType := store.MemoRelationReference\n\tif err := s.store.DeleteMemoRelation(ctx, &store.DeleteMemoRelation{\n\t\tMemoID:        &srcMemo.ID,\n\t\tRelatedMemoID: &dstMemo.ID,\n\t\tType:          &refType,\n\t}); err != nil {\n\t\treturn mcp.NewToolResultError(fmt.Sprintf(\"failed to delete relation: %v\", err)), nil\n\t}\n\treturn mcp.NewToolResultText(`{\"deleted\":true}`), nil\n}\n"
  },
  {
    "path": "server/router/mcp/tools_tag.go",
    "content": "package mcp\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"slices\"\n\n\t\"github.com/mark3labs/mcp-go/mcp\"\n\tmcpserver \"github.com/mark3labs/mcp-go/server\"\n\n\t\"github.com/usememos/memos/server/auth\"\n\t\"github.com/usememos/memos/store\"\n)\n\nfunc (s *MCPService) registerTagTools(mcpSrv *mcpserver.MCPServer) {\n\tmcpSrv.AddTool(mcp.NewTool(\"list_tags\",\n\t\tmcp.WithDescription(\"List all tags with their memo counts. Authenticated users see tags from their own and visible memos; unauthenticated callers see tags from public memos only. Results are sorted by count descending, then alphabetically.\"),\n\t), s.handleListTags)\n}\n\ntype tagEntry struct {\n\tTag   string `json:\"tag\"`\n\tCount int    `json:\"count\"`\n}\n\nfunc (s *MCPService) handleListTags(ctx context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) {\n\tuserID := auth.GetUserID(ctx)\n\n\trowStatus := store.Normal\n\tfind := &store.FindMemo{\n\t\tExcludeComments: true,\n\t\tExcludeContent:  true,\n\t\tRowStatus:       &rowStatus,\n\t}\n\tapplyVisibilityFilter(find, userID)\n\n\tmemos, err := s.store.ListMemos(ctx, find)\n\tif err != nil {\n\t\treturn mcp.NewToolResultError(fmt.Sprintf(\"failed to list memos: %v\", err)), nil\n\t}\n\n\tcounts := make(map[string]int)\n\tfor _, m := range memos {\n\t\tif m.Payload == nil {\n\t\t\tcontinue\n\t\t}\n\t\tfor _, tag := range m.Payload.Tags {\n\t\t\tcounts[tag]++\n\t\t}\n\t}\n\n\tentries := make([]tagEntry, 0, len(counts))\n\tfor tag, count := range counts {\n\t\tentries = append(entries, tagEntry{Tag: tag, Count: count})\n\t}\n\tslices.SortFunc(entries, func(a, b tagEntry) int {\n\t\tif a.Count != b.Count {\n\t\t\tif a.Count > b.Count {\n\t\t\t\treturn -1\n\t\t\t}\n\t\t\treturn 1\n\t\t}\n\t\tswitch {\n\t\tcase a.Tag < b.Tag:\n\t\t\treturn -1\n\t\tcase a.Tag > b.Tag:\n\t\t\treturn 1\n\t\tdefault:\n\t\t\treturn 0\n\t\t}\n\t})\n\n\tout, err := marshalJSON(entries)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn mcp.NewToolResultText(out), nil\n}\n"
  },
  {
    "path": "server/router/rss/rss.go",
    "content": "package rss\n\nimport (\n\t\"context\"\n\t\"crypto/sha256\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/gorilla/feeds\"\n\t\"github.com/labstack/echo/v5\"\n\n\t\"github.com/usememos/memos/internal/profile\"\n\t\"github.com/usememos/memos/plugin/markdown\"\n\tstorepb \"github.com/usememos/memos/proto/gen/store\"\n\t\"github.com/usememos/memos/store\"\n)\n\nconst (\n\tmaxRSSItemCount      = 100\n\tdefaultCacheDuration = 1 * time.Hour\n\tmaxCacheSize         = 50 // Maximum number of cached feeds\n)\n\nvar (\n\t// Regex to match markdown headings at the start of a line.\n\tmarkdownHeadingRegex = regexp.MustCompile(`^#{1,6}\\s*`)\n)\n\n// cacheEntry represents a cached RSS feed with expiration.\ntype cacheEntry struct {\n\tcontent      string\n\tetag         string\n\tlastModified time.Time\n\tcreatedAt    time.Time\n}\n\ntype RSSService struct {\n\tProfile         *profile.Profile\n\tStore           *store.Store\n\tMarkdownService markdown.Service\n\n\t// Cache for RSS feeds\n\tcache      map[string]*cacheEntry\n\tcacheMutex sync.RWMutex\n}\n\ntype RSSHeading struct {\n\tTitle       string\n\tDescription string\n\tLanguage    string\n}\n\nfunc NewRSSService(profile *profile.Profile, store *store.Store, markdownService markdown.Service) *RSSService {\n\treturn &RSSService{\n\t\tProfile:         profile,\n\t\tStore:           store,\n\t\tMarkdownService: markdownService,\n\t\tcache:           make(map[string]*cacheEntry),\n\t}\n}\n\nfunc (s *RSSService) RegisterRoutes(g *echo.Group) {\n\tg.GET(\"/explore/rss.xml\", s.GetExploreRSS)\n\tg.GET(\"/u/:username/rss.xml\", s.GetUserRSS)\n}\n\nfunc (s *RSSService) GetExploreRSS(c *echo.Context) error {\n\tctx := c.Request().Context()\n\tcacheKey := \"explore\"\n\n\t// Check cache first\n\tif cached := s.getFromCache(cacheKey); cached != nil {\n\t\t// Check ETag for conditional request\n\t\tif c.Request().Header.Get(\"If-None-Match\") == cached.etag {\n\t\t\treturn c.NoContent(http.StatusNotModified)\n\t\t}\n\t\ts.setRSSHeaders(c, cached.etag, cached.lastModified)\n\t\treturn c.String(http.StatusOK, cached.content)\n\t}\n\n\tnormalStatus := store.Normal\n\tlimit := maxRSSItemCount\n\tmemoFind := store.FindMemo{\n\t\tRowStatus:      &normalStatus,\n\t\tVisibilityList: []store.Visibility{store.Public},\n\t\tLimit:          &limit,\n\t}\n\tmemoList, err := s.Store.ListMemos(ctx, &memoFind)\n\tif err != nil {\n\t\treturn echo.NewHTTPError(http.StatusInternalServerError, \"Failed to find memo list\").Wrap(err)\n\t}\n\n\tbaseURL := c.Scheme() + \"://\" + c.Request().Host\n\trss, lastModified, err := s.generateRSSFromMemoList(ctx, memoList, baseURL, nil)\n\tif err != nil {\n\t\treturn echo.NewHTTPError(http.StatusInternalServerError, \"Failed to generate rss\").Wrap(err)\n\t}\n\n\t// Cache the result\n\tetag := s.putInCache(cacheKey, rss, lastModified)\n\ts.setRSSHeaders(c, etag, lastModified)\n\treturn c.String(http.StatusOK, rss)\n}\n\nfunc (s *RSSService) GetUserRSS(c *echo.Context) error {\n\tctx := c.Request().Context()\n\tusername := c.Param(\"username\")\n\tcacheKey := \"user:\" + username\n\n\t// Check cache first\n\tif cached := s.getFromCache(cacheKey); cached != nil {\n\t\t// Check ETag for conditional request\n\t\tif c.Request().Header.Get(\"If-None-Match\") == cached.etag {\n\t\t\treturn c.NoContent(http.StatusNotModified)\n\t\t}\n\t\ts.setRSSHeaders(c, cached.etag, cached.lastModified)\n\t\treturn c.String(http.StatusOK, cached.content)\n\t}\n\n\tuser, err := s.Store.GetUser(ctx, &store.FindUser{\n\t\tUsername: &username,\n\t})\n\tif err != nil {\n\t\treturn echo.NewHTTPError(http.StatusInternalServerError, \"Failed to find user\").Wrap(err)\n\t}\n\tif user == nil {\n\t\treturn echo.NewHTTPError(http.StatusNotFound, \"User not found\")\n\t}\n\n\tnormalStatus := store.Normal\n\tlimit := maxRSSItemCount\n\tmemoFind := store.FindMemo{\n\t\tCreatorID:      &user.ID,\n\t\tRowStatus:      &normalStatus,\n\t\tVisibilityList: []store.Visibility{store.Public},\n\t\tLimit:          &limit,\n\t}\n\tmemoList, err := s.Store.ListMemos(ctx, &memoFind)\n\tif err != nil {\n\t\treturn echo.NewHTTPError(http.StatusInternalServerError, \"Failed to find memo list\").Wrap(err)\n\t}\n\n\tbaseURL := c.Scheme() + \"://\" + c.Request().Host\n\trss, lastModified, err := s.generateRSSFromMemoList(ctx, memoList, baseURL, user)\n\tif err != nil {\n\t\treturn echo.NewHTTPError(http.StatusInternalServerError, \"Failed to generate rss\").Wrap(err)\n\t}\n\n\t// Cache the result\n\tetag := s.putInCache(cacheKey, rss, lastModified)\n\ts.setRSSHeaders(c, etag, lastModified)\n\treturn c.String(http.StatusOK, rss)\n}\n\nfunc (s *RSSService) generateRSSFromMemoList(ctx context.Context, memoList []*store.Memo, baseURL string, user *store.User) (string, time.Time, error) {\n\trssHeading, err := getRSSHeading(ctx, s.Store)\n\tif err != nil {\n\t\treturn \"\", time.Time{}, err\n\t}\n\n\tfeed := &feeds.Feed{\n\t\tTitle:       rssHeading.Title,\n\t\tLink:        &feeds.Link{Href: baseURL},\n\t\tDescription: rssHeading.Description,\n\t\tCreated:     time.Now(),\n\t}\n\n\tvar itemCountLimit = min(len(memoList), maxRSSItemCount)\n\tif itemCountLimit == 0 {\n\t\t// Return empty feed if no memos\n\t\trss, err := feed.ToRss()\n\t\treturn rss, time.Time{}, err\n\t}\n\n\t// Track the most recent update time for Last-Modified header\n\tvar lastModified time.Time\n\tif len(memoList) > 0 {\n\t\tlastModified = time.Unix(memoList[0].UpdatedTs, 0)\n\t}\n\n\t// Batch load all attachments for all memos to avoid N+1 query problem\n\tmemoIDs := make([]int32, itemCountLimit)\n\tfor i := 0; i < itemCountLimit; i++ {\n\t\tmemoIDs[i] = memoList[i].ID\n\t}\n\n\tallAttachments, err := s.Store.ListAttachments(ctx, &store.FindAttachment{\n\t\tMemoIDList: memoIDs,\n\t})\n\tif err != nil {\n\t\treturn \"\", lastModified, err\n\t}\n\n\t// Group attachments by memo ID for quick lookup\n\tattachmentsByMemoID := make(map[int32][]*store.Attachment)\n\tfor _, attachment := range allAttachments {\n\t\tif attachment.MemoID != nil {\n\t\t\tattachmentsByMemoID[*attachment.MemoID] = append(attachmentsByMemoID[*attachment.MemoID], attachment)\n\t\t}\n\t}\n\n\t// Batch load all memo creators\n\tcreatorMap := make(map[int32]*store.User)\n\tif user != nil {\n\t\t// Single user feed - reuse the user object\n\t\tcreatorMap[user.ID] = user\n\t} else {\n\t\t// Multi-user feed - batch load all unique creators\n\t\tcreatorIDs := make(map[int32]bool)\n\t\tfor _, memo := range memoList[:itemCountLimit] {\n\t\t\tcreatorIDs[memo.CreatorID] = true\n\t\t}\n\n\t\t// Batch load all users with a single query by getting all users and filtering\n\t\t// Note: This is more efficient than N separate queries\n\t\tfor creatorID := range creatorIDs {\n\t\t\tcreator, err := s.Store.GetUser(ctx, &store.FindUser{ID: &creatorID})\n\t\t\tif err == nil && creator != nil {\n\t\t\t\tcreatorMap[creatorID] = creator\n\t\t\t}\n\t\t}\n\t}\n\n\t// Generate feed items\n\tfeed.Items = make([]*feeds.Item, itemCountLimit)\n\tfor i := 0; i < itemCountLimit; i++ {\n\t\tmemo := memoList[i]\n\n\t\t// Generate item title from memo content\n\t\ttitle := s.generateItemTitle(memo.Content)\n\n\t\t// Render content as HTML\n\t\thtmlContent, err := s.getRSSItemDescription(memo.Content)\n\t\tif err != nil {\n\t\t\treturn \"\", lastModified, err\n\t\t}\n\n\t\tlink := &feeds.Link{Href: baseURL + \"/memos/\" + memo.UID}\n\n\t\titem := &feeds.Item{\n\t\t\tTitle:       title,\n\t\t\tLink:        link,\n\t\t\tDescription: htmlContent, // Summary/excerpt\n\t\t\tContent:     htmlContent, // Full content in content:encoded\n\t\t\tCreated:     time.Unix(memo.CreatedTs, 0),\n\t\t\tUpdated:     time.Unix(memo.UpdatedTs, 0),\n\t\t\tId:          link.Href,\n\t\t}\n\n\t\t// Add author information\n\t\tif creator, ok := creatorMap[memo.CreatorID]; ok {\n\t\t\tauthorName := creator.Nickname\n\t\t\tif authorName == \"\" {\n\t\t\t\tauthorName = creator.Username\n\t\t\t}\n\t\t\titem.Author = &feeds.Author{\n\t\t\t\tName:  authorName,\n\t\t\t\tEmail: creator.Email,\n\t\t\t}\n\t\t}\n\n\t\t// Note: gorilla/feeds doesn't support categories in RSS items\n\t\t// Tags could be added to the description or content if needed\n\n\t\t// Add first attachment as enclosure\n\t\tif attachments, ok := attachmentsByMemoID[memo.ID]; ok && len(attachments) > 0 {\n\t\t\tattachment := attachments[0]\n\t\t\tenclosure := feeds.Enclosure{}\n\t\t\tif attachment.StorageType == storepb.AttachmentStorageType_EXTERNAL || attachment.StorageType == storepb.AttachmentStorageType_S3 {\n\t\t\t\tenclosure.Url = attachment.Reference\n\t\t\t} else {\n\t\t\t\tenclosure.Url = fmt.Sprintf(\"%s/file/attachments/%s/%s\", baseURL, attachment.UID, attachment.Filename)\n\t\t\t}\n\t\t\tenclosure.Length = strconv.Itoa(int(attachment.Size))\n\t\t\tenclosure.Type = attachment.Type\n\t\t\titem.Enclosure = &enclosure\n\t\t}\n\n\t\tfeed.Items[i] = item\n\t}\n\n\trss, err := feed.ToRss()\n\tif err != nil {\n\t\treturn \"\", lastModified, err\n\t}\n\treturn rss, lastModified, nil\n}\n\nfunc (*RSSService) generateItemTitle(content string) string {\n\t// Extract first line as title\n\tlines := strings.Split(content, \"\\n\")\n\ttitle := strings.TrimSpace(lines[0])\n\n\t// Remove markdown heading syntax using regex (handles # to ###### with optional spaces)\n\ttitle = markdownHeadingRegex.ReplaceAllString(title, \"\")\n\ttitle = strings.TrimSpace(title)\n\n\t// Limit title length\n\tconst maxTitleLength = 100\n\tif len(title) > maxTitleLength {\n\t\t// Find last space before limit to avoid cutting words\n\t\tcutoff := maxTitleLength\n\t\tfor i := min(maxTitleLength-1, len(title)-1); i > 0; i-- {\n\t\t\tif title[i] == ' ' {\n\t\t\t\tcutoff = i\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif cutoff < maxTitleLength {\n\t\t\ttitle = title[:cutoff] + \"...\"\n\t\t} else {\n\t\t\t// No space found, just truncate\n\t\t\ttitle = title[:maxTitleLength] + \"...\"\n\t\t}\n\t}\n\n\t// If title is empty, use a default\n\tif title == \"\" {\n\t\ttitle = \"Memo\"\n\t}\n\n\treturn title\n}\n\nfunc (s *RSSService) getRSSItemDescription(content string) (string, error) {\n\thtml, err := s.MarkdownService.RenderHTML([]byte(content))\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn html, nil\n}\n\n// getFromCache retrieves a cached feed entry if it exists and is not expired.\nfunc (s *RSSService) getFromCache(key string) *cacheEntry {\n\ts.cacheMutex.RLock()\n\tentry, exists := s.cache[key]\n\ts.cacheMutex.RUnlock()\n\n\tif !exists {\n\t\treturn nil\n\t}\n\n\t// Check if cache entry is still valid\n\tif time.Since(entry.createdAt) > defaultCacheDuration {\n\t\t// Entry is expired, remove it\n\t\ts.cacheMutex.Lock()\n\t\tdelete(s.cache, key)\n\t\ts.cacheMutex.Unlock()\n\t\treturn nil\n\t}\n\n\treturn entry\n}\n\n// putInCache stores a feed in the cache and returns its ETag.\nfunc (s *RSSService) putInCache(key, content string, lastModified time.Time) string {\n\ts.cacheMutex.Lock()\n\tdefer s.cacheMutex.Unlock()\n\n\t// Generate ETag from content hash\n\thash := sha256.Sum256([]byte(content))\n\tetag := fmt.Sprintf(`\"%x\"`, hash[:8])\n\n\t// Implement simple LRU: if cache is too large, remove oldest entries\n\tif len(s.cache) >= maxCacheSize {\n\t\tvar oldestKey string\n\t\tvar oldestTime time.Time\n\t\tfor k, v := range s.cache {\n\t\t\tif oldestKey == \"\" || v.createdAt.Before(oldestTime) {\n\t\t\t\toldestKey = k\n\t\t\t\toldestTime = v.createdAt\n\t\t\t}\n\t\t}\n\t\tif oldestKey != \"\" {\n\t\t\tdelete(s.cache, oldestKey)\n\t\t}\n\t}\n\n\ts.cache[key] = &cacheEntry{\n\t\tcontent:      content,\n\t\tetag:         etag,\n\t\tlastModified: lastModified,\n\t\tcreatedAt:    time.Now(),\n\t}\n\n\treturn etag\n}\n\n// setRSSHeaders sets appropriate HTTP headers for RSS responses.\nfunc (*RSSService) setRSSHeaders(c *echo.Context, etag string, lastModified time.Time) {\n\tc.Response().Header().Set(echo.HeaderContentType, \"application/rss+xml; charset=utf-8\")\n\tc.Response().Header().Set(echo.HeaderCacheControl, fmt.Sprintf(\"public, max-age=%d\", int(defaultCacheDuration.Seconds())))\n\tc.Response().Header().Set(\"ETag\", etag)\n\tif !lastModified.IsZero() {\n\t\tc.Response().Header().Set(\"Last-Modified\", lastModified.UTC().Format(http.TimeFormat))\n\t}\n}\n\nfunc getRSSHeading(ctx context.Context, stores *store.Store) (RSSHeading, error) {\n\tsettings, err := stores.GetInstanceGeneralSetting(ctx)\n\tif err != nil {\n\t\treturn RSSHeading{}, err\n\t}\n\tif settings == nil || settings.CustomProfile == nil {\n\t\treturn RSSHeading{\n\t\t\tTitle:       \"Memos\",\n\t\t\tDescription: \"An open source, lightweight note-taking service. Easily capture and share your great thoughts.\",\n\t\t\tLanguage:    \"en-us\",\n\t\t}, nil\n\t}\n\tcustomProfile := settings.CustomProfile\n\n\treturn RSSHeading{\n\t\tTitle:       customProfile.Title,\n\t\tDescription: customProfile.Description,\n\t\tLanguage:    \"en-us\",\n\t}, nil\n}\n"
  },
  {
    "path": "server/runner/memopayload/runner.go",
    "content": "package memopayload\n\nimport (\n\t\"context\"\n\t\"log/slog\"\n\n\t\"github.com/pkg/errors\"\n\n\t\"github.com/usememos/memos/plugin/markdown\"\n\tstorepb \"github.com/usememos/memos/proto/gen/store\"\n\t\"github.com/usememos/memos/store\"\n)\n\ntype Runner struct {\n\tStore           *store.Store\n\tMarkdownService markdown.Service\n}\n\nfunc NewRunner(store *store.Store, markdownService markdown.Service) *Runner {\n\treturn &Runner{\n\t\tStore:           store,\n\t\tMarkdownService: markdownService,\n\t}\n}\n\n// RunOnce rebuilds the payload of all memos.\nfunc (r *Runner) RunOnce(ctx context.Context) {\n\t// Process memos in batches to avoid loading all memos into memory at once\n\tconst batchSize = 100\n\toffset := 0\n\tprocessed := 0\n\n\tfor {\n\t\tlimit := batchSize\n\t\tmemos, err := r.Store.ListMemos(ctx, &store.FindMemo{\n\t\t\tLimit:  &limit,\n\t\t\tOffset: &offset,\n\t\t})\n\t\tif err != nil {\n\t\t\tslog.Error(\"failed to list memos\", \"err\", err)\n\t\t\treturn\n\t\t}\n\n\t\t// Break if no more memos\n\t\tif len(memos) == 0 {\n\t\t\tbreak\n\t\t}\n\n\t\t// Process batch\n\t\tbatchSuccessCount := 0\n\t\tfor _, memo := range memos {\n\t\t\tif err := RebuildMemoPayload(memo, r.MarkdownService); err != nil {\n\t\t\t\tslog.Error(\"failed to rebuild memo payload\", \"err\", err, \"memoID\", memo.ID)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif err := r.Store.UpdateMemo(ctx, &store.UpdateMemo{\n\t\t\t\tID:      memo.ID,\n\t\t\t\tPayload: memo.Payload,\n\t\t\t}); err != nil {\n\t\t\t\tslog.Error(\"failed to update memo\", \"err\", err, \"memoID\", memo.ID)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tbatchSuccessCount++\n\t\t}\n\n\t\tprocessed += len(memos)\n\t\tslog.Info(\"Processed memo batch\", \"batchSize\", len(memos), \"successCount\", batchSuccessCount, \"totalProcessed\", processed)\n\n\t\t// Move to next batch\n\t\toffset += len(memos)\n\t}\n}\n\nfunc RebuildMemoPayload(memo *store.Memo, markdownService markdown.Service) error {\n\tif memo.Payload == nil {\n\t\tmemo.Payload = &storepb.MemoPayload{}\n\t}\n\n\t// Use goldmark service to extract all metadata in a single pass (more efficient)\n\tdata, err := markdownService.ExtractAll([]byte(memo.Content))\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"failed to extract markdown metadata\")\n\t}\n\n\tmemo.Payload.Tags = data.Tags\n\tmemo.Payload.Property = data.Property\n\treturn nil\n}\n"
  },
  {
    "path": "server/runner/s3presign/runner.go",
    "content": "package s3presign\n\nimport (\n\t\"context\"\n\t\"log/slog\"\n\t\"time\"\n\n\t\"google.golang.org/protobuf/types/known/timestamppb\"\n\n\t\"github.com/usememos/memos/plugin/storage/s3\"\n\tstorepb \"github.com/usememos/memos/proto/gen/store\"\n\t\"github.com/usememos/memos/store\"\n)\n\ntype Runner struct {\n\tStore *store.Store\n}\n\nfunc NewRunner(store *store.Store) *Runner {\n\treturn &Runner{\n\t\tStore: store,\n\t}\n}\n\n// Schedule runner every 12 hours.\nconst runnerInterval = time.Hour * 12\n\nfunc (r *Runner) Run(ctx context.Context) {\n\tticker := time.NewTicker(runnerInterval)\n\tdefer ticker.Stop()\n\n\tfor {\n\t\tselect {\n\t\tcase <-ticker.C:\n\t\t\tr.RunOnce(ctx)\n\t\tcase <-ctx.Done():\n\t\t\treturn\n\t\t}\n\t}\n}\n\nfunc (r *Runner) RunOnce(ctx context.Context) {\n\tr.CheckAndPresign(ctx)\n}\n\nfunc (r *Runner) CheckAndPresign(ctx context.Context) {\n\tinstanceStorageSetting, err := r.Store.GetInstanceStorageSetting(ctx)\n\tif err != nil {\n\t\treturn\n\t}\n\n\ts3StorageType := storepb.AttachmentStorageType_S3\n\t// Limit attachments to a reasonable batch size\n\tconst batchSize = 100\n\toffset := 0\n\n\tfor {\n\t\tlimit := batchSize\n\t\tattachments, err := r.Store.ListAttachments(ctx, &store.FindAttachment{\n\t\t\tGetBlob:     false,\n\t\t\tStorageType: &s3StorageType,\n\t\t\tLimit:       &limit,\n\t\t\tOffset:      &offset,\n\t\t})\n\t\tif err != nil {\n\t\t\tslog.Error(\"Failed to list attachments for presigning\", \"error\", err)\n\t\t\treturn\n\t\t}\n\n\t\t// Break if no more attachments\n\t\tif len(attachments) == 0 {\n\t\t\tbreak\n\t\t}\n\n\t\t// Process batch of attachments\n\t\tpresignCount := 0\n\t\tfor _, attachment := range attachments {\n\t\t\ts3ObjectPayload := attachment.Payload.GetS3Object()\n\t\t\tif s3ObjectPayload == nil {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif s3ObjectPayload.LastPresignedTime != nil {\n\t\t\t\t// Skip if the presigned URL is still valid for the next 4 days.\n\t\t\t\t// The expiration time is set to 5 days.\n\t\t\t\tif time.Now().Before(s3ObjectPayload.LastPresignedTime.AsTime().Add(4 * 24 * time.Hour)) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\n\t\t\ts3Config := instanceStorageSetting.GetS3Config()\n\t\t\tif s3ObjectPayload.S3Config != nil {\n\t\t\t\ts3Config = s3ObjectPayload.S3Config\n\t\t\t}\n\t\t\tif s3Config == nil {\n\t\t\t\tslog.Error(\"S3 config is not found\")\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\ts3Client, err := s3.NewClient(ctx, s3Config)\n\t\t\tif err != nil {\n\t\t\t\tslog.Error(\"Failed to create S3 client\", \"error\", err)\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tpresignURL, err := s3Client.PresignGetObject(ctx, s3ObjectPayload.Key)\n\t\t\tif err != nil {\n\t\t\t\tslog.Error(\"Failed to presign URL\", \"error\", err, \"attachmentID\", attachment.ID)\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\ts3ObjectPayload.S3Config = s3Config\n\t\t\ts3ObjectPayload.LastPresignedTime = timestamppb.New(time.Now())\n\t\t\tif err := r.Store.UpdateAttachment(ctx, &store.UpdateAttachment{\n\t\t\t\tID:        attachment.ID,\n\t\t\t\tReference: &presignURL,\n\t\t\t\tPayload: &storepb.AttachmentPayload{\n\t\t\t\t\tPayload: &storepb.AttachmentPayload_S3Object_{\n\t\t\t\t\t\tS3Object: s3ObjectPayload,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}); err != nil {\n\t\t\t\tslog.Error(\"Failed to update attachment\", \"error\", err, \"attachmentID\", attachment.ID)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tpresignCount++\n\t\t}\n\n\t\tslog.Info(\"Presigned batch of S3 attachments\", \"batchSize\", len(attachments), \"presigned\", presignCount)\n\n\t\t// Move to next batch\n\t\toffset += len(attachments)\n\t}\n}\n"
  },
  {
    "path": "server/server.go",
    "content": "package server\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"net\"\n\t\"net/http\"\n\t\"runtime\"\n\t\"time\"\n\n\t\"github.com/google/uuid\"\n\t\"github.com/labstack/echo/v5\"\n\t\"github.com/labstack/echo/v5/middleware\"\n\t\"github.com/pkg/errors\"\n\n\t\"github.com/usememos/memos/internal/profile\"\n\tstorepb \"github.com/usememos/memos/proto/gen/store\"\n\tapiv1 \"github.com/usememos/memos/server/router/api/v1\"\n\t\"github.com/usememos/memos/server/router/fileserver\"\n\t\"github.com/usememos/memos/server/router/frontend\"\n\tmcprouter \"github.com/usememos/memos/server/router/mcp\"\n\t\"github.com/usememos/memos/server/router/rss\"\n\t\"github.com/usememos/memos/server/runner/s3presign\"\n\t\"github.com/usememos/memos/store\"\n)\n\ntype Server struct {\n\tSecret  string\n\tProfile *profile.Profile\n\tStore   *store.Store\n\n\techoServer        *echo.Echo\n\thttpServer        *http.Server\n\trunnerCancelFuncs []context.CancelFunc\n}\n\nfunc NewServer(ctx context.Context, profile *profile.Profile, store *store.Store) (*Server, error) {\n\ts := &Server{\n\t\tStore:   store,\n\t\tProfile: profile,\n\t}\n\n\techoServer := echo.New()\n\techoServer.Use(middleware.Recover())\n\ts.echoServer = echoServer\n\n\tinstanceBasicSetting, err := s.getOrUpsertInstanceBasicSetting(ctx)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"failed to get instance basic setting\")\n\t}\n\n\tsecret := \"usememos\"\n\tif !profile.Demo {\n\t\tsecret = instanceBasicSetting.SecretKey\n\t}\n\ts.Secret = secret\n\n\t// Register healthz endpoint.\n\techoServer.GET(\"/healthz\", func(c *echo.Context) error {\n\t\treturn c.String(http.StatusOK, \"Service ready.\")\n\t})\n\n\t// Serve frontend static files.\n\tfrontend.NewFrontendService(profile, store).Serve(ctx, echoServer)\n\n\trootGroup := echoServer.Group(\"\")\n\n\tapiV1Service := apiv1.NewAPIV1Service(s.Secret, profile, store)\n\n\t// Register HTTP file server routes BEFORE gRPC-Gateway to ensure proper range request handling for Safari.\n\t// This uses native HTTP serving (http.ServeContent) instead of gRPC for video/audio files.\n\tfileServerService := fileserver.NewFileServerService(s.Profile, s.Store, s.Secret)\n\tfileServerService.RegisterRoutes(echoServer)\n\n\t// Create and register RSS routes (needs markdown service from apiV1Service).\n\trss.NewRSSService(s.Profile, s.Store, apiV1Service.MarkdownService).RegisterRoutes(rootGroup)\n\n\t// Register gRPC gateway as api v1 (includes SSE endpoint on CORS-enabled group).\n\tif err := apiV1Service.RegisterGateway(ctx, echoServer); err != nil {\n\t\treturn nil, errors.Wrap(err, \"failed to register gRPC gateway\")\n\t}\n\n\t// Register MCP server.\n\tmcpService := mcprouter.NewMCPService(s.Profile, s.Store, s.Secret)\n\tmcpService.RegisterRoutes(echoServer)\n\n\treturn s, nil\n}\n\nfunc (s *Server) Start(ctx context.Context) error {\n\tvar address, network string\n\tif len(s.Profile.UNIXSock) == 0 {\n\t\taddress = fmt.Sprintf(\"%s:%d\", s.Profile.Addr, s.Profile.Port)\n\t\tnetwork = \"tcp\"\n\t} else {\n\t\taddress = s.Profile.UNIXSock\n\t\tnetwork = \"unix\"\n\t}\n\tlistener, err := net.Listen(network, address)\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"failed to listen\")\n\t}\n\n\t// Start Echo server directly (no cmux needed - all traffic is HTTP).\n\ts.httpServer = &http.Server{Handler: s.echoServer}\n\tgo func() {\n\t\tif err := s.httpServer.Serve(listener); err != nil && err != http.ErrServerClosed {\n\t\t\tslog.Error(\"failed to start echo server\", \"error\", err)\n\t\t}\n\t}()\n\ts.StartBackgroundRunners(ctx)\n\n\treturn nil\n}\n\nfunc (s *Server) Shutdown(ctx context.Context) {\n\tctx, cancel := context.WithTimeout(ctx, 10*time.Second)\n\tdefer cancel()\n\n\tslog.Info(\"server shutting down\")\n\n\t// Cancel all background runners\n\tfor _, cancelFunc := range s.runnerCancelFuncs {\n\t\tif cancelFunc != nil {\n\t\t\tcancelFunc()\n\t\t}\n\t}\n\n\t// Shutdown HTTP server.\n\tif s.httpServer != nil {\n\t\tif err := s.httpServer.Shutdown(ctx); err != nil {\n\t\t\tslog.Error(\"failed to shutdown server\", slog.String(\"error\", err.Error()))\n\t\t}\n\t}\n\n\t// Close database connection.\n\tif err := s.Store.Close(); err != nil {\n\t\tslog.Error(\"failed to close database\", slog.String(\"error\", err.Error()))\n\t}\n\n\tslog.Info(\"memos stopped properly\")\n}\n\nfunc (s *Server) StartBackgroundRunners(ctx context.Context) {\n\t// Create a separate context for each background runner\n\t// This allows us to control cancellation for each runner independently\n\ts3Context, s3Cancel := context.WithCancel(ctx)\n\n\t// Store the cancel function so we can properly shut down runners\n\ts.runnerCancelFuncs = append(s.runnerCancelFuncs, s3Cancel)\n\n\t// Create and start S3 presign runner\n\ts3presignRunner := s3presign.NewRunner(s.Store)\n\ts3presignRunner.RunOnce(ctx)\n\n\t// Start continuous S3 presign runner\n\tgo func() {\n\t\ts3presignRunner.Run(s3Context)\n\t\tslog.Info(\"s3presign runner stopped\")\n\t}()\n\n\t// Log the number of goroutines running\n\tslog.Info(\"background runners started\", \"goroutines\", runtime.NumGoroutine())\n}\n\nfunc (s *Server) getOrUpsertInstanceBasicSetting(ctx context.Context) (*storepb.InstanceBasicSetting, error) {\n\tinstanceBasicSetting, err := s.Store.GetInstanceBasicSetting(ctx)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"failed to get instance basic setting\")\n\t}\n\tmodified := false\n\tif instanceBasicSetting.SecretKey == \"\" {\n\t\tinstanceBasicSetting.SecretKey = uuid.NewString()\n\t\tmodified = true\n\t}\n\tif modified {\n\t\tinstanceSetting, err := s.Store.UpsertInstanceSetting(ctx, &storepb.InstanceSetting{\n\t\t\tKey:   storepb.InstanceSettingKey_BASIC,\n\t\t\tValue: &storepb.InstanceSetting_BasicSetting{BasicSetting: instanceBasicSetting},\n\t\t})\n\t\tif err != nil {\n\t\t\treturn nil, errors.Wrap(err, \"failed to upsert instance setting\")\n\t\t}\n\t\tinstanceBasicSetting = instanceSetting.GetBasicSetting()\n\t}\n\treturn instanceBasicSetting, nil\n}\n"
  },
  {
    "path": "store/attachment.go",
    "content": "package store\n\nimport (\n\t\"context\"\n\t\"log/slog\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/pkg/errors\"\n\n\t\"github.com/usememos/memos/internal/base\"\n\t\"github.com/usememos/memos/plugin/storage/s3\"\n\tstorepb \"github.com/usememos/memos/proto/gen/store\"\n)\n\ntype Attachment struct {\n\t// ID is the system generated unique identifier for the attachment.\n\tID int32\n\t// UID is the user defined unique identifier for the attachment.\n\tUID string\n\n\t// Standard fields\n\tCreatorID int32\n\tCreatedTs int64\n\tUpdatedTs int64\n\n\t// Domain specific fields\n\tFilename    string\n\tBlob        []byte\n\tType        string\n\tSize        int64\n\tStorageType storepb.AttachmentStorageType\n\tReference   string\n\tPayload     *storepb.AttachmentPayload\n\n\t// The related memo ID.\n\tMemoID *int32\n\n\t// Composed field\n\tMemoUID *string\n}\n\ntype FindAttachment struct {\n\tGetBlob        bool\n\tID             *int32\n\tUID            *string\n\tCreatorID      *int32\n\tFilename       *string\n\tFilenameSearch *string\n\tMemoID         *int32\n\tMemoIDList     []int32\n\tHasRelatedMemo bool\n\tStorageType    *storepb.AttachmentStorageType\n\tFilters        []string\n\tLimit          *int\n\tOffset         *int\n}\n\ntype UpdateAttachment struct {\n\tID        int32\n\tUID       *string\n\tUpdatedTs *int64\n\tFilename  *string\n\tMemoID    *int32\n\tReference *string\n\tPayload   *storepb.AttachmentPayload\n}\n\ntype DeleteAttachment struct {\n\tID     int32\n\tMemoID *int32\n}\n\nfunc (s *Store) CreateAttachment(ctx context.Context, create *Attachment) (*Attachment, error) {\n\tif !base.UIDMatcher.MatchString(create.UID) {\n\t\treturn nil, errors.New(\"invalid uid\")\n\t}\n\treturn s.driver.CreateAttachment(ctx, create)\n}\n\nfunc (s *Store) ListAttachments(ctx context.Context, find *FindAttachment) ([]*Attachment, error) {\n\t// Set default limits to prevent loading too many attachments at once\n\tshouldApplyDefaultLimit := find.Limit == nil && len(find.MemoIDList) == 0\n\tif shouldApplyDefaultLimit && find.GetBlob {\n\t\t// When fetching blobs, we should be especially careful with limits\n\t\tdefaultLimit := 10\n\t\tfind.Limit = &defaultLimit\n\t} else if shouldApplyDefaultLimit {\n\t\t// Even without blobs, let's default to a reasonable limit\n\t\tdefaultLimit := 100\n\t\tfind.Limit = &defaultLimit\n\t}\n\n\treturn s.driver.ListAttachments(ctx, find)\n}\n\nfunc (s *Store) GetAttachment(ctx context.Context, find *FindAttachment) (*Attachment, error) {\n\tattachments, err := s.ListAttachments(ctx, find)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif len(attachments) == 0 {\n\t\treturn nil, nil\n\t}\n\n\treturn attachments[0], nil\n}\n\nfunc (s *Store) UpdateAttachment(ctx context.Context, update *UpdateAttachment) error {\n\tif update.UID != nil && !base.UIDMatcher.MatchString(*update.UID) {\n\t\treturn errors.New(\"invalid uid\")\n\t}\n\treturn s.driver.UpdateAttachment(ctx, update)\n}\n\nfunc (s *Store) DeleteAttachment(ctx context.Context, delete *DeleteAttachment) error {\n\tattachment, err := s.GetAttachment(ctx, &FindAttachment{ID: &delete.ID})\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"failed to get attachment\")\n\t}\n\tif attachment == nil {\n\t\treturn errors.New(\"attachment not found\")\n\t}\n\n\tif attachment.StorageType == storepb.AttachmentStorageType_LOCAL {\n\t\tif err := func() error {\n\t\t\tp := filepath.FromSlash(attachment.Reference)\n\t\t\tif !filepath.IsAbs(p) {\n\t\t\t\tp = filepath.Join(s.profile.Data, p)\n\t\t\t}\n\t\t\terr := os.Remove(p)\n\t\t\tif err != nil && !os.IsNotExist(err) {\n\t\t\t\treturn errors.Wrap(err, \"failed to delete local file\")\n\t\t\t}\n\t\t\treturn nil\n\t\t}(); err != nil {\n\t\t\treturn errors.Wrap(err, \"failed to delete local file\")\n\t\t}\n\t} else if attachment.StorageType == storepb.AttachmentStorageType_S3 {\n\t\tif err := func() error {\n\t\t\ts3ObjectPayload := attachment.Payload.GetS3Object()\n\t\t\tif s3ObjectPayload == nil {\n\t\t\t\treturn errors.Errorf(\"No s3 object found\")\n\t\t\t}\n\t\t\tinstanceStorageSetting, err := s.GetInstanceStorageSetting(ctx)\n\t\t\tif err != nil {\n\t\t\t\treturn errors.Wrap(err, \"failed to get instance storage setting\")\n\t\t\t}\n\t\t\ts3Config := s3ObjectPayload.S3Config\n\t\t\tif s3Config == nil {\n\t\t\t\tif instanceStorageSetting.S3Config == nil {\n\t\t\t\t\treturn errors.Errorf(\"S3 config is not found\")\n\t\t\t\t}\n\t\t\t\ts3Config = instanceStorageSetting.S3Config\n\t\t\t}\n\n\t\t\ts3Client, err := s3.NewClient(ctx, s3Config)\n\t\t\tif err != nil {\n\t\t\t\treturn errors.Wrap(err, \"Failed to create s3 client\")\n\t\t\t}\n\t\t\tif err := s3Client.DeleteObject(ctx, s3ObjectPayload.Key); err != nil {\n\t\t\t\treturn errors.Wrap(err, \"Failed to delete s3 object\")\n\t\t\t}\n\t\t\treturn nil\n\t\t}(); err != nil {\n\t\t\tslog.Warn(\"Failed to delete s3 object\", slog.Any(\"err\", err))\n\t\t}\n\t}\n\n\treturn s.driver.DeleteAttachment(ctx, delete)\n}\n"
  },
  {
    "path": "store/cache/cache.go",
    "content": "package cache\n\nimport (\n\t\"context\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n)\n\n// Interface defines the operations a cache must support.\ntype Interface interface {\n\t// Set adds a value to the cache with the default TTL.\n\tSet(ctx context.Context, key string, value any)\n\n\t// SetWithTTL adds a value to the cache with a custom TTL.\n\tSetWithTTL(ctx context.Context, key string, value any, ttl time.Duration)\n\n\t// Get retrieves a value from the cache.\n\tGet(ctx context.Context, key string) (any, bool)\n\n\t// Delete removes a value from the cache.\n\tDelete(ctx context.Context, key string)\n\n\t// Clear removes all values from the cache.\n\tClear(ctx context.Context)\n\n\t// Size returns the number of items in the cache.\n\tSize() int64\n\n\t// Close stops all background tasks and releases resources.\n\tClose() error\n}\n\n// item represents a cached value with metadata.\ntype item struct {\n\tvalue      any\n\texpiration time.Time\n\tsize       int // Approximate size in bytes\n}\n\n// Config contains options for configuring a cache.\ntype Config struct {\n\t// DefaultTTL is the default time-to-live for cache entries.\n\tDefaultTTL time.Duration\n\n\t// CleanupInterval is how often the cache runs cleanup.\n\tCleanupInterval time.Duration\n\n\t// MaxItems is the maximum number of items allowed in the cache.\n\tMaxItems int\n\n\t// OnEviction is called when an item is evicted from the cache.\n\tOnEviction func(key string, value any)\n}\n\n// DefaultConfig returns a default configuration for the cache.\nfunc DefaultConfig() Config {\n\treturn Config{\n\t\tDefaultTTL:      10 * time.Minute,\n\t\tCleanupInterval: 5 * time.Minute,\n\t\tMaxItems:        1000,\n\t\tOnEviction:      nil,\n\t}\n}\n\n// Cache is a thread-safe in-memory cache with TTL and memory management.\ntype Cache struct {\n\titemCount  atomic.Int64 // Use atomic operations to track item count\n\tdata       sync.Map\n\tconfig     Config\n\tstopChan   chan struct{}\n\tclosedChan chan struct{}\n}\n\n// New creates a new memory cache with the given configuration.\nfunc New(config Config) *Cache {\n\tc := &Cache{\n\t\tconfig:     config,\n\t\tstopChan:   make(chan struct{}),\n\t\tclosedChan: make(chan struct{}),\n\t}\n\n\tgo c.cleanupLoop()\n\treturn c\n}\n\n// NewDefault creates a new memory cache with default configuration.\nfunc NewDefault() *Cache {\n\treturn New(DefaultConfig())\n}\n\n// Set adds a value to the cache with the default TTL.\nfunc (c *Cache) Set(ctx context.Context, key string, value any) {\n\tc.SetWithTTL(ctx, key, value, c.config.DefaultTTL)\n}\n\n// SetWithTTL adds a value to the cache with a custom TTL.\nfunc (c *Cache) SetWithTTL(_ context.Context, key string, value any, ttl time.Duration) {\n\t// Estimate size of the item (very rough approximation).\n\tsize := estimateSize(value)\n\n\t// Check if item already exists to avoid double counting.\n\tif _, exists := c.data.Load(key); exists {\n\t\tc.data.Delete(key)\n\t} else {\n\t\t// Only increment if this is a new key.\n\t\t(&c.itemCount).Add(1)\n\t}\n\n\tc.data.Store(key, item{\n\t\tvalue:      value,\n\t\texpiration: time.Now().Add(ttl),\n\t\tsize:       size,\n\t})\n\n\t// If we're over the max items, clean up old items.\n\tif c.config.MaxItems > 0 && (&c.itemCount).Load() > int64(c.config.MaxItems) {\n\t\tc.cleanupOldest()\n\t}\n}\n\n// Get retrieves a value from the cache.\nfunc (c *Cache) Get(_ context.Context, key string) (any, bool) {\n\tvalue, ok := c.data.Load(key)\n\tif !ok {\n\t\treturn nil, false\n\t}\n\n\titm, ok := value.(item)\n\tif !ok {\n\t\t// If the value is not of type item, it means it was corrupted or not set correctly.\n\t\tc.data.Delete(key)\n\t\treturn nil, false\n\t}\n\tif time.Now().After(itm.expiration) {\n\t\tc.data.Delete(key)\n\t\t(&c.itemCount).Add(-1)\n\n\t\tif c.config.OnEviction != nil {\n\t\t\tc.config.OnEviction(key, itm.value)\n\t\t}\n\n\t\treturn nil, false\n\t}\n\n\treturn itm.value, true\n}\n\n// Delete removes a value from the cache.\nfunc (c *Cache) Delete(_ context.Context, key string) {\n\tif value, loaded := c.data.LoadAndDelete(key); loaded {\n\t\t(&c.itemCount).Add(-1)\n\n\t\tif c.config.OnEviction != nil {\n\t\t\tif itm, ok := value.(item); ok {\n\t\t\t\tc.config.OnEviction(key, itm.value)\n\t\t\t}\n\t\t}\n\t}\n}\n\n// Clear removes all values from the cache.\nfunc (c *Cache) Clear(_ context.Context) {\n\tcount := 0\n\tc.data.Range(func(key, value any) bool {\n\t\tif c.config.OnEviction != nil {\n\t\t\titm, ok := value.(item)\n\t\t\tif ok {\n\t\t\t\tif keyStr, ok := key.(string); ok {\n\t\t\t\t\tc.config.OnEviction(keyStr, itm.value)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tc.data.Delete(key)\n\t\tcount++\n\t\treturn true\n\t})\n\t(&c.itemCount).Store(0)\n}\n\n// Size returns the number of items in the cache.\nfunc (c *Cache) Size() int64 {\n\treturn (&c.itemCount).Load()\n}\n\n// Close stops the cache cleanup goroutine.\nfunc (c *Cache) Close() error {\n\tselect {\n\tcase <-c.stopChan:\n\t\t// Already closed\n\t\treturn nil\n\tdefault:\n\t\tclose(c.stopChan)\n\t\t<-c.closedChan // Wait for cleanup goroutine to exit\n\t\treturn nil\n\t}\n}\n\n// cleanupLoop periodically cleans up expired items.\nfunc (c *Cache) cleanupLoop() {\n\tticker := time.NewTicker(c.config.CleanupInterval)\n\tdefer func() {\n\t\tticker.Stop()\n\t\tclose(c.closedChan)\n\t}()\n\n\tfor {\n\t\tselect {\n\t\tcase <-ticker.C:\n\t\t\tc.cleanup()\n\t\tcase <-c.stopChan:\n\t\t\treturn\n\t\t}\n\t}\n}\n\n// cleanup removes expired items.\nfunc (c *Cache) cleanup() {\n\tevicted := make(map[string]any)\n\tcount := 0\n\n\tc.data.Range(func(key, value any) bool {\n\t\titm, ok := value.(item)\n\t\tif !ok {\n\t\t\treturn true\n\t\t}\n\t\tif time.Now().After(itm.expiration) {\n\t\t\tc.data.Delete(key)\n\t\t\tcount++\n\n\t\t\tif c.config.OnEviction != nil {\n\t\t\t\tif keyStr, ok := key.(string); ok {\n\t\t\t\t\tevicted[keyStr] = itm.value\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn true\n\t})\n\n\tif count > 0 {\n\t\t(&c.itemCount).Add(-int64(count))\n\n\t\t// Call eviction callbacks outside the loop to avoid blocking the range\n\t\tif c.config.OnEviction != nil {\n\t\t\tfor k, v := range evicted {\n\t\t\t\tc.config.OnEviction(k, v)\n\t\t\t}\n\t\t}\n\t}\n}\n\n// cleanupOldest removes the oldest items if we're over the max items.\nfunc (c *Cache) cleanupOldest() {\n\t// Remove 20% of max items at once\n\tthreshold := max(c.config.MaxItems/5, 1)\n\n\tcurrentCount := (&c.itemCount).Load()\n\n\t// If we're not over the threshold, don't do anything\n\tif currentCount <= int64(c.config.MaxItems) {\n\t\treturn\n\t}\n\n\t// Find the oldest items\n\ttype keyExpPair struct {\n\t\tkey        string\n\t\tvalue      any\n\t\texpiration time.Time\n\t}\n\tcandidates := make([]keyExpPair, 0, threshold)\n\n\tc.data.Range(func(key, value any) bool {\n\t\titm, ok := value.(item)\n\t\tif !ok {\n\t\t\treturn true\n\t\t}\n\t\tif keyStr, ok := key.(string); ok && len(candidates) < threshold {\n\t\t\tcandidates = append(candidates, keyExpPair{keyStr, itm.value, itm.expiration})\n\t\t\treturn true\n\t\t}\n\n\t\t// Find the newest item in candidates\n\t\tnewestIdx := 0\n\t\tfor i := 1; i < len(candidates); i++ {\n\t\t\tif candidates[i].expiration.After(candidates[newestIdx].expiration) {\n\t\t\t\tnewestIdx = i\n\t\t\t}\n\t\t}\n\n\t\t// Replace it if this item is older\n\t\tif itm.expiration.Before(candidates[newestIdx].expiration) {\n\t\t\tcandidates[newestIdx] = keyExpPair{key.(string), itm.value, itm.expiration}\n\t\t}\n\n\t\treturn true\n\t})\n\n\t// Delete the oldest items\n\tdeletedCount := 0\n\tfor _, candidate := range candidates {\n\t\tc.data.Delete(candidate.key)\n\t\tdeletedCount++\n\n\t\tif c.config.OnEviction != nil {\n\t\t\tc.config.OnEviction(candidate.key, candidate.value)\n\t\t}\n\t}\n\n\t// Update count\n\tif deletedCount > 0 {\n\t\t(&c.itemCount).Add(-int64(deletedCount))\n\t}\n}\n\n// estimateSize attempts to estimate the memory footprint of a value.\nfunc estimateSize(value any) int {\n\tswitch v := value.(type) {\n\tcase string:\n\t\treturn len(v) + 24 // base size + string overhead\n\tcase []byte:\n\t\treturn len(v) + 24 // base size + slice overhead\n\tcase map[string]any:\n\t\treturn len(v) * 64 // rough estimate\n\tdefault:\n\t\treturn 64 // default conservative estimate\n\t}\n}\n"
  },
  {
    "path": "store/cache/cache_test.go",
    "content": "package cache\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"sync\"\n\t\"testing\"\n\t\"time\"\n)\n\nfunc TestCacheBasicOperations(t *testing.T) {\n\tctx := context.Background()\n\tconfig := DefaultConfig()\n\tconfig.DefaultTTL = 100 * time.Millisecond\n\tconfig.CleanupInterval = 50 * time.Millisecond\n\tcache := New(config)\n\tdefer cache.Close()\n\n\t// Test Set and Get\n\tcache.Set(ctx, \"key1\", \"value1\")\n\tif val, ok := cache.Get(ctx, \"key1\"); !ok || val != \"value1\" {\n\t\tt.Errorf(\"Expected 'value1', got %v, exists: %v\", val, ok)\n\t}\n\n\t// Test SetWithTTL\n\tcache.SetWithTTL(ctx, \"key2\", \"value2\", 200*time.Millisecond)\n\tif val, ok := cache.Get(ctx, \"key2\"); !ok || val != \"value2\" {\n\t\tt.Errorf(\"Expected 'value2', got %v, exists: %v\", val, ok)\n\t}\n\n\t// Test Delete\n\tcache.Delete(ctx, \"key1\")\n\tif _, ok := cache.Get(ctx, \"key1\"); ok {\n\t\tt.Error(\"Key 'key1' should have been deleted\")\n\t}\n\n\t// Test automatic expiration\n\ttime.Sleep(150 * time.Millisecond)\n\tif _, ok := cache.Get(ctx, \"key1\"); ok {\n\t\tt.Error(\"Key 'key1' should have expired\")\n\t}\n\t// key2 should still be valid (200ms TTL)\n\tif _, ok := cache.Get(ctx, \"key2\"); !ok {\n\t\tt.Error(\"Key 'key2' should still be valid\")\n\t}\n\n\t// Wait for key2 to expire\n\ttime.Sleep(100 * time.Millisecond)\n\tif _, ok := cache.Get(ctx, \"key2\"); ok {\n\t\tt.Error(\"Key 'key2' should have expired\")\n\t}\n\n\t// Test Clear\n\tcache.Set(ctx, \"key3\", \"value3\")\n\tcache.Clear(ctx)\n\tif _, ok := cache.Get(ctx, \"key3\"); ok {\n\t\tt.Error(\"Cache should be empty after Clear()\")\n\t}\n}\n\nfunc TestCacheEviction(t *testing.T) {\n\tctx := context.Background()\n\tconfig := DefaultConfig()\n\tconfig.MaxItems = 5\n\tcache := New(config)\n\tdefer cache.Close()\n\n\t// Add 5 items (max capacity)\n\tfor i := 0; i < 5; i++ {\n\t\tkey := fmt.Sprintf(\"key%d\", i)\n\t\tcache.Set(ctx, key, i)\n\t}\n\n\t// Verify all 5 items are in the cache\n\tfor i := 0; i < 5; i++ {\n\t\tkey := fmt.Sprintf(\"key%d\", i)\n\t\tif _, ok := cache.Get(ctx, key); !ok {\n\t\t\tt.Errorf(\"Key '%s' should be in the cache\", key)\n\t\t}\n\t}\n\n\t// Add 2 more items to trigger eviction\n\tcache.Set(ctx, \"keyA\", \"valueA\")\n\tcache.Set(ctx, \"keyB\", \"valueB\")\n\n\t// Verify size is still within limits\n\tif cache.Size() > int64(config.MaxItems) {\n\t\tt.Errorf(\"Cache size %d exceeds limit %d\", cache.Size(), config.MaxItems)\n\t}\n\n\t// Some of the original keys should have been evicted\n\tevictedCount := 0\n\tfor i := 0; i < 5; i++ {\n\t\tkey := fmt.Sprintf(\"key%d\", i)\n\t\tif _, ok := cache.Get(ctx, key); !ok {\n\t\t\tevictedCount++\n\t\t}\n\t}\n\n\tif evictedCount == 0 {\n\t\tt.Error(\"No keys were evicted despite exceeding max items\")\n\t}\n\n\t// The newer keys should still be present\n\tif _, ok := cache.Get(ctx, \"keyA\"); !ok {\n\t\tt.Error(\"Key 'keyA' should be in the cache\")\n\t}\n\tif _, ok := cache.Get(ctx, \"keyB\"); !ok {\n\t\tt.Error(\"Key 'keyB' should be in the cache\")\n\t}\n}\n\nfunc TestCacheConcurrency(t *testing.T) {\n\tctx := context.Background()\n\tcache := NewDefault()\n\tdefer cache.Close()\n\n\tconst goroutines = 10\n\tconst operationsPerGoroutine = 100\n\n\tvar wg sync.WaitGroup\n\twg.Add(goroutines)\n\n\tfor i := 0; i < goroutines; i++ {\n\t\tgo func(id int) {\n\t\t\tdefer wg.Done()\n\n\t\t\tbaseKey := fmt.Sprintf(\"worker%d-\", id)\n\n\t\t\t// Set operations\n\t\t\tfor j := 0; j < operationsPerGoroutine; j++ {\n\t\t\t\tkey := fmt.Sprintf(\"%skey%d\", baseKey, j)\n\t\t\t\tvalue := fmt.Sprintf(\"value%d-%d\", id, j)\n\t\t\t\tcache.Set(ctx, key, value)\n\t\t\t}\n\n\t\t\t// Get operations\n\t\t\tfor j := 0; j < operationsPerGoroutine; j++ {\n\t\t\t\tkey := fmt.Sprintf(\"%skey%d\", baseKey, j)\n\t\t\t\tval, ok := cache.Get(ctx, key)\n\t\t\t\tif !ok {\n\t\t\t\t\tt.Errorf(\"Key '%s' should exist in cache\", key)\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\texpected := fmt.Sprintf(\"value%d-%d\", id, j)\n\t\t\t\tif val != expected {\n\t\t\t\t\tt.Errorf(\"For key '%s', expected '%s', got '%s'\", key, expected, val)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Delete half the keys\n\t\t\tfor j := 0; j < operationsPerGoroutine/2; j++ {\n\t\t\t\tkey := fmt.Sprintf(\"%skey%d\", baseKey, j)\n\t\t\t\tcache.Delete(ctx, key)\n\t\t\t}\n\t\t}(i)\n\t}\n\n\twg.Wait()\n\n\t// Verify size and deletion\n\tvar totalKeysExpected int64 = goroutines * operationsPerGoroutine / 2\n\tif cache.Size() != totalKeysExpected {\n\t\tt.Errorf(\"Expected cache size to be %d, got %d\", totalKeysExpected, cache.Size())\n\t}\n}\n\nfunc TestEvictionCallback(t *testing.T) {\n\tctx := context.Background()\n\tevicted := make(map[string]interface{})\n\tevictedMu := sync.Mutex{}\n\n\tconfig := DefaultConfig()\n\tconfig.DefaultTTL = 50 * time.Millisecond\n\tconfig.CleanupInterval = 25 * time.Millisecond\n\tconfig.OnEviction = func(key string, value interface{}) {\n\t\tevictedMu.Lock()\n\t\tevicted[key] = value\n\t\tevictedMu.Unlock()\n\t}\n\n\tcache := New(config)\n\tdefer cache.Close()\n\n\t// Add items\n\tcache.Set(ctx, \"key1\", \"value1\")\n\tcache.Set(ctx, \"key2\", \"value2\")\n\n\t// Manually delete\n\tcache.Delete(ctx, \"key1\")\n\n\t// Verify manual deletion triggered callback\n\ttime.Sleep(10 * time.Millisecond) // Small delay to ensure callback processed\n\tevictedMu.Lock()\n\tif evicted[\"key1\"] != \"value1\" {\n\t\tt.Error(\"Eviction callback not triggered for manual deletion\")\n\t}\n\tevictedMu.Unlock()\n\n\t// Wait for automatic expiration\n\ttime.Sleep(60 * time.Millisecond)\n\n\t// Verify TTL expiration triggered callback\n\tevictedMu.Lock()\n\tif evicted[\"key2\"] != \"value2\" {\n\t\tt.Error(\"Eviction callback not triggered for TTL expiration\")\n\t}\n\tevictedMu.Unlock()\n}\n"
  },
  {
    "path": "store/cache.go",
    "content": "package store\n\nimport (\n\t\"fmt\"\n)\n\nfunc getUserSettingCacheKey(userID int32, key string) string {\n\treturn fmt.Sprintf(\"%d-%s\", userID, key)\n}\n"
  },
  {
    "path": "store/common.go",
    "content": "package store\n\nimport \"google.golang.org/protobuf/encoding/protojson\"\n\nvar (\n\tprotojsonUnmarshaler = protojson.UnmarshalOptions{\n\t\tAllowPartial:   true,\n\t\tDiscardUnknown: true,\n\t}\n)\n\n// RowStatus is the status for a row.\ntype RowStatus string\n\nconst (\n\t// Normal is the status for a normal row.\n\tNormal RowStatus = \"NORMAL\"\n\t// Archived is the status for an archived row.\n\tArchived RowStatus = \"ARCHIVED\"\n)\n\nfunc (r RowStatus) String() string {\n\treturn string(r)\n}\n"
  },
  {
    "path": "store/db/db.go",
    "content": "package db\n\nimport (\n\t\"github.com/pkg/errors\"\n\n\t\"github.com/usememos/memos/internal/profile\"\n\t\"github.com/usememos/memos/store\"\n\t\"github.com/usememos/memos/store/db/mysql\"\n\t\"github.com/usememos/memos/store/db/postgres\"\n\t\"github.com/usememos/memos/store/db/sqlite\"\n)\n\n// NewDBDriver creates new db driver based on profile.\nfunc NewDBDriver(profile *profile.Profile) (store.Driver, error) {\n\tvar driver store.Driver\n\tvar err error\n\n\tswitch profile.Driver {\n\tcase \"sqlite\":\n\t\tdriver, err = sqlite.NewDB(profile)\n\tcase \"mysql\":\n\t\tdriver, err = mysql.NewDB(profile)\n\tcase \"postgres\":\n\t\tdriver, err = postgres.NewDB(profile)\n\tdefault:\n\t\treturn nil, errors.New(\"unknown db driver\")\n\t}\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"failed to create db driver\")\n\t}\n\treturn driver, nil\n}\n"
  },
  {
    "path": "store/db/mysql/attachment.go",
    "content": "package mysql\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/pkg/errors\"\n\t\"google.golang.org/protobuf/encoding/protojson\"\n\n\t\"github.com/usememos/memos/plugin/filter\"\n\tstorepb \"github.com/usememos/memos/proto/gen/store\"\n\t\"github.com/usememos/memos/store\"\n)\n\nfunc (d *DB) CreateAttachment(ctx context.Context, create *store.Attachment) (*store.Attachment, error) {\n\tfields := []string{\"`uid`\", \"`filename`\", \"`blob`\", \"`type`\", \"`size`\", \"`creator_id`\", \"`memo_id`\", \"`storage_type`\", \"`reference`\", \"`payload`\"}\n\tplaceholder := []string{\"?\", \"?\", \"?\", \"?\", \"?\", \"?\", \"?\", \"?\", \"?\", \"?\"}\n\tstorageType := \"\"\n\tif create.StorageType != storepb.AttachmentStorageType_ATTACHMENT_STORAGE_TYPE_UNSPECIFIED {\n\t\tstorageType = create.StorageType.String()\n\t}\n\tpayloadString := \"{}\"\n\tif create.Payload != nil {\n\t\tbytes, err := protojson.Marshal(create.Payload)\n\t\tif err != nil {\n\t\t\treturn nil, errors.Wrap(err, \"failed to marshal attachment payload\")\n\t\t}\n\t\tpayloadString = string(bytes)\n\t}\n\targs := []any{create.UID, create.Filename, create.Blob, create.Type, create.Size, create.CreatorID, create.MemoID, storageType, create.Reference, payloadString}\n\n\tstmt := \"INSERT INTO `attachment` (\" + strings.Join(fields, \", \") + \") VALUES (\" + strings.Join(placeholder, \", \") + \")\"\n\tresult, err := d.db.ExecContext(ctx, stmt, args...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tid, err := result.LastInsertId()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tid32 := int32(id)\n\treturn d.GetAttachment(ctx, &store.FindAttachment{ID: &id32})\n}\n\nfunc (d *DB) ListAttachments(ctx context.Context, find *store.FindAttachment) ([]*store.Attachment, error) {\n\twhere, args := []string{\"1 = 1\"}, []any{}\n\n\tif v := find.ID; v != nil {\n\t\twhere, args = append(where, \"`attachment`.`id` = ?\"), append(args, *v)\n\t}\n\tif v := find.UID; v != nil {\n\t\twhere, args = append(where, \"`attachment`.`uid` = ?\"), append(args, *v)\n\t}\n\tif v := find.CreatorID; v != nil {\n\t\twhere, args = append(where, \"`attachment`.`creator_id` = ?\"), append(args, *v)\n\t}\n\tif v := find.Filename; v != nil {\n\t\twhere, args = append(where, \"`attachment`.`filename` = ?\"), append(args, *v)\n\t}\n\tif v := find.FilenameSearch; v != nil {\n\t\twhere, args = append(where, \"`attachment`.`filename` LIKE ?\"), append(args, \"%\"+*v+\"%\")\n\t}\n\tif v := find.MemoID; v != nil {\n\t\twhere, args = append(where, \"`attachment`.`memo_id` = ?\"), append(args, *v)\n\t}\n\tif len(find.MemoIDList) > 0 {\n\t\tplaceholders := make([]string, 0, len(find.MemoIDList))\n\t\tfor range find.MemoIDList {\n\t\t\tplaceholders = append(placeholders, \"?\")\n\t\t}\n\t\twhere = append(where, \"`attachment`.`memo_id` IN (\"+strings.Join(placeholders, \",\")+\")\")\n\t\tfor _, id := range find.MemoIDList {\n\t\t\targs = append(args, id)\n\t\t}\n\t}\n\tif find.HasRelatedMemo {\n\t\twhere = append(where, \"`attachment`.`memo_id` IS NOT NULL\")\n\t}\n\tif find.StorageType != nil {\n\t\twhere, args = append(where, \"`attachment`.`storage_type` = ?\"), append(args, find.StorageType.String())\n\t}\n\n\tif len(find.Filters) > 0 {\n\t\tengine, err := filter.DefaultAttachmentEngine()\n\t\tif err != nil {\n\t\t\treturn nil, errors.Wrap(err, \"failed to get filter engine\")\n\t\t}\n\t\tif err := filter.AppendConditions(ctx, engine, find.Filters, filter.DialectMySQL, &where, &args); err != nil {\n\t\t\treturn nil, errors.Wrap(err, \"failed to append filter conditions\")\n\t\t}\n\t}\n\n\tfields := []string{\n\t\t\"`attachment`.`id` AS `id`\",\n\t\t\"`attachment`.`uid` AS `uid`\",\n\t\t\"`attachment`.`filename` AS `filename`\",\n\t\t\"`attachment`.`type` AS `type`\",\n\t\t\"`attachment`.`size` AS `size`\",\n\t\t\"`attachment`.`creator_id` AS `creator_id`\",\n\t\t\"UNIX_TIMESTAMP(`attachment`.`created_ts`) AS `created_ts`\",\n\t\t\"UNIX_TIMESTAMP(`attachment`.`updated_ts`) AS `updated_ts`\",\n\t\t\"`attachment`.`memo_id` AS `memo_id`\",\n\t\t\"`attachment`.`storage_type` AS `storage_type`\",\n\t\t\"`attachment`.`reference` AS `reference`\",\n\t\t\"`attachment`.`payload` AS `payload`\",\n\t\t\"CASE WHEN `memo`.`uid` IS NOT NULL THEN `memo`.`uid` ELSE NULL END AS `memo_uid`\",\n\t}\n\tif find.GetBlob {\n\t\tfields = append(fields, \"`attachment`.`blob` AS `blob`\")\n\t}\n\n\tquery := \"SELECT \" + strings.Join(fields, \", \") + \" FROM `attachment`\" + \" \" +\n\t\t\"LEFT JOIN `memo` ON `attachment`.`memo_id` = `memo`.`id`\" + \" \" +\n\t\t\"WHERE \" + strings.Join(where, \" AND \") + \" \" +\n\t\t\"ORDER BY `updated_ts` DESC\"\n\tif find.Limit != nil {\n\t\tquery = fmt.Sprintf(\"%s LIMIT %d\", query, *find.Limit)\n\t\tif find.Offset != nil {\n\t\t\tquery = fmt.Sprintf(\"%s OFFSET %d\", query, *find.Offset)\n\t\t}\n\t}\n\n\trows, err := d.db.QueryContext(ctx, query, args...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\n\tlist := make([]*store.Attachment, 0)\n\tfor rows.Next() {\n\t\tattachment := store.Attachment{}\n\t\tvar memoID sql.NullInt32\n\t\tvar storageType string\n\t\tvar payloadBytes []byte\n\t\tdests := []any{\n\t\t\t&attachment.ID,\n\t\t\t&attachment.UID,\n\t\t\t&attachment.Filename,\n\t\t\t&attachment.Type,\n\t\t\t&attachment.Size,\n\t\t\t&attachment.CreatorID,\n\t\t\t&attachment.CreatedTs,\n\t\t\t&attachment.UpdatedTs,\n\t\t\t&memoID,\n\t\t\t&storageType,\n\t\t\t&attachment.Reference,\n\t\t\t&payloadBytes,\n\t\t\t&attachment.MemoUID,\n\t\t}\n\t\tif find.GetBlob {\n\t\t\tdests = append(dests, &attachment.Blob)\n\t\t}\n\t\tif err := rows.Scan(dests...); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tif memoID.Valid {\n\t\t\tattachment.MemoID = &memoID.Int32\n\t\t}\n\t\tattachment.StorageType = storepb.AttachmentStorageType(storepb.AttachmentStorageType_value[storageType])\n\t\tpayload := &storepb.AttachmentPayload{}\n\t\tif err := protojsonUnmarshaler.Unmarshal(payloadBytes, payload); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tattachment.Payload = payload\n\t\tlist = append(list, &attachment)\n\t}\n\n\tif err := rows.Err(); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn list, nil\n}\n\nfunc (d *DB) GetAttachment(ctx context.Context, find *store.FindAttachment) (*store.Attachment, error) {\n\tlist, err := d.ListAttachments(ctx, find)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif len(list) == 0 {\n\t\treturn nil, nil\n\t}\n\n\treturn list[0], nil\n}\n\nfunc (d *DB) UpdateAttachment(ctx context.Context, update *store.UpdateAttachment) error {\n\tset, args := []string{}, []any{}\n\n\tif v := update.UID; v != nil {\n\t\tset, args = append(set, \"`uid` = ?\"), append(args, *v)\n\t}\n\tif v := update.UpdatedTs; v != nil {\n\t\tset, args = append(set, \"`updated_ts` = FROM_UNIXTIME(?)\"), append(args, *v)\n\t}\n\tif v := update.Filename; v != nil {\n\t\tset, args = append(set, \"`filename` = ?\"), append(args, *v)\n\t}\n\tif v := update.MemoID; v != nil {\n\t\tset, args = append(set, \"`memo_id` = ?\"), append(args, *v)\n\t}\n\tif v := update.Reference; v != nil {\n\t\tset, args = append(set, \"`reference` = ?\"), append(args, *v)\n\t}\n\tif v := update.Payload; v != nil {\n\t\tbytes, err := protojson.Marshal(v)\n\t\tif err != nil {\n\t\t\treturn errors.Wrap(err, \"failed to marshal attachment payload\")\n\t\t}\n\t\tset, args = append(set, \"`payload` = ?\"), append(args, string(bytes))\n\t}\n\n\targs = append(args, update.ID)\n\tstmt := \"UPDATE `attachment` SET \" + strings.Join(set, \", \") + \" WHERE `id` = ?\"\n\tresult, err := d.db.ExecContext(ctx, stmt, args...)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif _, err := result.RowsAffected(); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc (d *DB) DeleteAttachment(ctx context.Context, delete *store.DeleteAttachment) error {\n\tstmt := \"DELETE FROM `attachment` WHERE `id` = ?\"\n\tresult, err := d.db.ExecContext(ctx, stmt, delete.ID)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif _, err := result.RowsAffected(); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "store/db/mysql/common.go",
    "content": "package mysql\n\nimport \"google.golang.org/protobuf/encoding/protojson\"\n\nvar (\n\tprotojsonUnmarshaler = protojson.UnmarshalOptions{\n\t\tAllowPartial:   true,\n\t\tDiscardUnknown: true,\n\t}\n)\n"
  },
  {
    "path": "store/db/mysql/idp.go",
    "content": "package mysql\n\nimport (\n\t\"context\"\n\t\"strings\"\n\n\t\"github.com/pkg/errors\"\n\n\tstorepb \"github.com/usememos/memos/proto/gen/store\"\n\t\"github.com/usememos/memos/store\"\n)\n\nfunc (d *DB) CreateIdentityProvider(ctx context.Context, create *store.IdentityProvider) (*store.IdentityProvider, error) {\n\tplaceholders := []string{\"?\", \"?\", \"?\", \"?\", \"?\"}\n\tfields := []string{\"`uid`\", \"`name`\", \"`type`\", \"`identifier_filter`\", \"`config`\"}\n\targs := []any{create.UID, create.Name, create.Type.String(), create.IdentifierFilter, create.Config}\n\n\tstmt := \"INSERT INTO `idp` (\" + strings.Join(fields, \", \") + \") VALUES (\" + strings.Join(placeholders, \", \") + \")\"\n\tresult, err := d.db.ExecContext(ctx, stmt, args...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tid, err := result.LastInsertId()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tcreate.ID = int32(id)\n\treturn create, nil\n}\n\nfunc (d *DB) ListIdentityProviders(ctx context.Context, find *store.FindIdentityProvider) ([]*store.IdentityProvider, error) {\n\twhere, args := []string{\"1 = 1\"}, []any{}\n\tif v := find.ID; v != nil {\n\t\twhere, args = append(where, \"`id` = ?\"), append(args, *v)\n\t}\n\tif v := find.UID; v != nil {\n\t\twhere, args = append(where, \"`uid` = ?\"), append(args, *v)\n\t}\n\n\trows, err := d.db.QueryContext(ctx, \"SELECT `id`, `uid`, `name`, `type`, `identifier_filter`, `config` FROM `idp` WHERE \"+strings.Join(where, \" AND \")+\" ORDER BY `id` ASC\",\n\t\targs...,\n\t)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\n\tvar identityProviders []*store.IdentityProvider\n\tfor rows.Next() {\n\t\tvar identityProvider store.IdentityProvider\n\t\tvar typeString string\n\t\tif err := rows.Scan(\n\t\t\t&identityProvider.ID,\n\t\t\t&identityProvider.UID,\n\t\t\t&identityProvider.Name,\n\t\t\t&typeString,\n\t\t\t&identityProvider.IdentifierFilter,\n\t\t\t&identityProvider.Config,\n\t\t); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tidentityProvider.Type = storepb.IdentityProvider_Type(storepb.IdentityProvider_Type_value[typeString])\n\t\tidentityProviders = append(identityProviders, &identityProvider)\n\t}\n\n\tif err := rows.Err(); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn identityProviders, nil\n}\n\nfunc (d *DB) GetIdentityProvider(ctx context.Context, find *store.FindIdentityProvider) (*store.IdentityProvider, error) {\n\tlist, err := d.ListIdentityProviders(ctx, find)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif len(list) == 0 {\n\t\treturn nil, nil\n\t}\n\n\tidentityProvider := list[0]\n\treturn identityProvider, nil\n}\n\nfunc (d *DB) UpdateIdentityProvider(ctx context.Context, update *store.UpdateIdentityProvider) (*store.IdentityProvider, error) {\n\tset, args := []string{}, []any{}\n\tif v := update.Name; v != nil {\n\t\tset, args = append(set, \"`name` = ?\"), append(args, *v)\n\t}\n\tif v := update.IdentifierFilter; v != nil {\n\t\tset, args = append(set, \"`identifier_filter` = ?\"), append(args, *v)\n\t}\n\tif v := update.Config; v != nil {\n\t\tset, args = append(set, \"`config` = ?\"), append(args, *v)\n\t}\n\targs = append(args, update.ID)\n\n\tstmt := \"UPDATE `idp` SET \" + strings.Join(set, \", \") + \" WHERE `id` = ?\"\n\t_, err := d.db.ExecContext(ctx, stmt, args...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tidentityProvider, err := d.GetIdentityProvider(ctx, &store.FindIdentityProvider{\n\t\tID: &update.ID,\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif identityProvider == nil {\n\t\treturn nil, errors.Errorf(\"idp %d not found\", update.ID)\n\t}\n\treturn identityProvider, nil\n}\n\nfunc (d *DB) DeleteIdentityProvider(ctx context.Context, delete *store.DeleteIdentityProvider) error {\n\twhere, args := []string{\"`id` = ?\"}, []any{delete.ID}\n\tstmt := \"DELETE FROM `idp` WHERE \" + strings.Join(where, \" AND \")\n\tresult, err := d.db.ExecContext(ctx, stmt, args...)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif _, err = result.RowsAffected(); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "store/db/mysql/inbox.go",
    "content": "package mysql\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/pkg/errors\"\n\t\"google.golang.org/protobuf/encoding/protojson\"\n\n\tstorepb \"github.com/usememos/memos/proto/gen/store\"\n\t\"github.com/usememos/memos/store\"\n)\n\nfunc (d *DB) CreateInbox(ctx context.Context, create *store.Inbox) (*store.Inbox, error) {\n\tmessageString := \"{}\"\n\tif create.Message != nil {\n\t\tbytes, err := protojson.Marshal(create.Message)\n\t\tif err != nil {\n\t\t\treturn nil, errors.Wrap(err, \"failed to marshal inbox message\")\n\t\t}\n\t\tmessageString = string(bytes)\n\t}\n\n\tfields := []string{\"`sender_id`\", \"`receiver_id`\", \"`status`\", \"`message`\"}\n\tplaceholder := []string{\"?\", \"?\", \"?\", \"?\"}\n\targs := []any{create.SenderID, create.ReceiverID, create.Status, messageString}\n\n\tstmt := \"INSERT INTO `inbox` (\" + strings.Join(fields, \", \") + \") VALUES (\" + strings.Join(placeholder, \", \") + \")\"\n\tresult, err := d.db.ExecContext(ctx, stmt, args...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tid, err := result.LastInsertId()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tid32 := int32(id)\n\tinbox, err := d.GetInbox(ctx, &store.FindInbox{ID: &id32})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn inbox, nil\n}\n\nfunc (d *DB) ListInboxes(ctx context.Context, find *store.FindInbox) ([]*store.Inbox, error) {\n\twhere, args := []string{\"1 = 1\"}, []any{}\n\n\tif find.ID != nil {\n\t\twhere, args = append(where, \"`id` = ?\"), append(args, *find.ID)\n\t}\n\tif find.SenderID != nil {\n\t\twhere, args = append(where, \"`sender_id` = ?\"), append(args, *find.SenderID)\n\t}\n\tif find.ReceiverID != nil {\n\t\twhere, args = append(where, \"`receiver_id` = ?\"), append(args, *find.ReceiverID)\n\t}\n\tif find.Status != nil {\n\t\twhere, args = append(where, \"`status` = ?\"), append(args, *find.Status)\n\t}\n\tif find.MessageType != nil {\n\t\t// Filter by message type using JSON extraction\n\t\t// Note: The type field in JSON is stored as string representation of the enum name\n\t\tif *find.MessageType == storepb.InboxMessage_TYPE_UNSPECIFIED {\n\t\t\twhere, args = append(where, \"(JSON_EXTRACT(`message`, '$.type') IS NULL OR JSON_EXTRACT(`message`, '$.type') = ?)\"), append(args, find.MessageType.String())\n\t\t} else {\n\t\t\twhere, args = append(where, \"JSON_EXTRACT(`message`, '$.type') = ?\"), append(args, find.MessageType.String())\n\t\t}\n\t}\n\n\tquery := \"SELECT `id`, UNIX_TIMESTAMP(`created_ts`), `sender_id`, `receiver_id`, `status`, `message` FROM `inbox` WHERE \" + strings.Join(where, \" AND \") + \" ORDER BY `created_ts` DESC\"\n\tif find.Limit != nil {\n\t\tquery = fmt.Sprintf(\"%s LIMIT %d\", query, *find.Limit)\n\t\tif find.Offset != nil {\n\t\t\tquery = fmt.Sprintf(\"%s OFFSET %d\", query, *find.Offset)\n\t\t}\n\t}\n\trows, err := d.db.QueryContext(ctx, query, args...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\n\tlist := []*store.Inbox{}\n\tfor rows.Next() {\n\t\tinbox := &store.Inbox{}\n\t\tvar messageBytes []byte\n\t\tif err := rows.Scan(\n\t\t\t&inbox.ID,\n\t\t\t&inbox.CreatedTs,\n\t\t\t&inbox.SenderID,\n\t\t\t&inbox.ReceiverID,\n\t\t\t&inbox.Status,\n\t\t\t&messageBytes,\n\t\t); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tmessage := &storepb.InboxMessage{}\n\t\tif err := protojsonUnmarshaler.Unmarshal(messageBytes, message); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tinbox.Message = message\n\t\tlist = append(list, inbox)\n\t}\n\n\tif err := rows.Err(); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn list, nil\n}\n\nfunc (d *DB) GetInbox(ctx context.Context, find *store.FindInbox) (*store.Inbox, error) {\n\tlist, err := d.ListInboxes(ctx, find)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"failed to get inbox\")\n\t}\n\tif len(list) != 1 {\n\t\treturn nil, errors.Errorf(\"unexpected inbox count: %d\", len(list))\n\t}\n\treturn list[0], nil\n}\n\nfunc (d *DB) UpdateInbox(ctx context.Context, update *store.UpdateInbox) (*store.Inbox, error) {\n\tset, args := []string{\"`status` = ?\"}, []any{update.Status.String()}\n\targs = append(args, update.ID)\n\tquery := \"UPDATE `inbox` SET \" + strings.Join(set, \", \") + \" WHERE `id` = ?\"\n\tif _, err := d.db.ExecContext(ctx, query, args...); err != nil {\n\t\treturn nil, errors.Wrap(err, \"failed to update inbox\")\n\t}\n\tinbox, err := d.GetInbox(ctx, &store.FindInbox{ID: &update.ID})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn inbox, nil\n}\n\nfunc (d *DB) DeleteInbox(ctx context.Context, delete *store.DeleteInbox) error {\n\tresult, err := d.db.ExecContext(ctx, \"DELETE FROM `inbox` WHERE `id` = ?\", delete.ID)\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"failed to delete inbox\")\n\t}\n\tif _, err := result.RowsAffected(); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "store/db/mysql/instance_setting.go",
    "content": "package mysql\n\nimport (\n\t\"context\"\n\t\"strings\"\n\n\t\"github.com/usememos/memos/store\"\n)\n\nfunc (d *DB) UpsertInstanceSetting(ctx context.Context, upsert *store.InstanceSetting) (*store.InstanceSetting, error) {\n\tstmt := \"INSERT INTO `system_setting` (`name`, `value`, `description`) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE `value` = ?, `description` = ?\"\n\t_, err := d.db.ExecContext(\n\t\tctx,\n\t\tstmt,\n\t\tupsert.Name,\n\t\tupsert.Value,\n\t\tupsert.Description,\n\t\tupsert.Value,\n\t\tupsert.Description,\n\t)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn upsert, nil\n}\n\nfunc (d *DB) ListInstanceSettings(ctx context.Context, find *store.FindInstanceSetting) ([]*store.InstanceSetting, error) {\n\twhere, args := []string{\"1 = 1\"}, []any{}\n\tif find.Name != \"\" {\n\t\twhere, args = append(where, \"`name` = ?\"), append(args, find.Name)\n\t}\n\n\tquery := \"SELECT `name`, `value`, `description` FROM `system_setting` WHERE \" + strings.Join(where, \" AND \")\n\trows, err := d.db.QueryContext(ctx, query, args...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\n\tlist := []*store.InstanceSetting{}\n\tfor rows.Next() {\n\t\tsystemSettingMessage := &store.InstanceSetting{}\n\t\tif err := rows.Scan(\n\t\t\t&systemSettingMessage.Name,\n\t\t\t&systemSettingMessage.Value,\n\t\t\t&systemSettingMessage.Description,\n\t\t); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tlist = append(list, systemSettingMessage)\n\t}\n\n\tif err := rows.Err(); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn list, nil\n}\n\nfunc (d *DB) DeleteInstanceSetting(ctx context.Context, delete *store.DeleteInstanceSetting) error {\n\tstmt := \"DELETE FROM `system_setting` WHERE `name` = ?\"\n\t_, err := d.db.ExecContext(ctx, stmt, delete.Name)\n\treturn err\n}\n"
  },
  {
    "path": "store/db/mysql/memo.go",
    "content": "package mysql\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/pkg/errors\"\n\t\"google.golang.org/protobuf/encoding/protojson\"\n\n\t\"github.com/usememos/memos/plugin/filter\"\n\tstorepb \"github.com/usememos/memos/proto/gen/store\"\n\t\"github.com/usememos/memos/store\"\n)\n\nfunc (d *DB) CreateMemo(ctx context.Context, create *store.Memo) (*store.Memo, error) {\n\tfields := []string{\"`uid`\", \"`creator_id`\", \"`content`\", \"`visibility`\", \"`payload`\"}\n\tplaceholder := []string{\"?\", \"?\", \"?\", \"?\", \"?\"}\n\tpayload := \"{}\"\n\tif create.Payload != nil {\n\t\tpayloadBytes, err := protojson.Marshal(create.Payload)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tpayload = string(payloadBytes)\n\t}\n\targs := []any{create.UID, create.CreatorID, create.Content, create.Visibility, payload}\n\n\t// Add custom timestamps if provided\n\tif create.CreatedTs != 0 {\n\t\tfields = append(fields, \"`created_ts`\")\n\t\tplaceholder = append(placeholder, \"FROM_UNIXTIME(?)\")\n\t\targs = append(args, create.CreatedTs)\n\t}\n\tif create.UpdatedTs != 0 {\n\t\tfields = append(fields, \"`updated_ts`\")\n\t\tplaceholder = append(placeholder, \"FROM_UNIXTIME(?)\")\n\t\targs = append(args, create.UpdatedTs)\n\t}\n\n\tstmt := \"INSERT INTO `memo` (\" + strings.Join(fields, \", \") + \") VALUES (\" + strings.Join(placeholder, \", \") + \")\"\n\tresult, err := d.db.ExecContext(ctx, stmt, args...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\trawID, err := result.LastInsertId()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tid := int32(rawID)\n\tmemo, err := d.GetMemo(ctx, &store.FindMemo{ID: &id})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif memo == nil {\n\t\treturn nil, errors.Errorf(\"failed to create memo\")\n\t}\n\treturn memo, nil\n}\n\nfunc (d *DB) ListMemos(ctx context.Context, find *store.FindMemo) ([]*store.Memo, error) {\n\twhere, having, args := []string{\"1 = 1\"}, []string{\"1 = 1\"}, []any{}\n\n\tengine, err := filter.DefaultEngine()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif err := filter.AppendConditions(ctx, engine, find.Filters, filter.DialectMySQL, &where, &args); err != nil {\n\t\treturn nil, err\n\t}\n\tif v := find.ID; v != nil {\n\t\twhere, args = append(where, \"`memo`.`id` = ?\"), append(args, *v)\n\t}\n\tif len(find.IDList) > 0 {\n\t\tplaceholders := make([]string, 0, len(find.IDList))\n\t\tfor range find.IDList {\n\t\t\tplaceholders = append(placeholders, \"?\")\n\t\t}\n\t\twhere = append(where, \"`memo`.`id` IN (\"+strings.Join(placeholders, \",\")+\")\")\n\t\tfor _, id := range find.IDList {\n\t\t\targs = append(args, id)\n\t\t}\n\t}\n\tif v := find.UID; v != nil {\n\t\twhere, args = append(where, \"`memo`.`uid` = ?\"), append(args, *v)\n\t}\n\tif len(find.UIDList) > 0 {\n\t\tplaceholders := make([]string, 0, len(find.UIDList))\n\t\tfor range find.UIDList {\n\t\t\tplaceholders = append(placeholders, \"?\")\n\t\t}\n\t\twhere = append(where, \"`memo`.`uid` IN (\"+strings.Join(placeholders, \",\")+\")\")\n\t\tfor _, uid := range find.UIDList {\n\t\t\targs = append(args, uid)\n\t\t}\n\t}\n\tif v := find.CreatorID; v != nil {\n\t\twhere, args = append(where, \"`memo`.`creator_id` = ?\"), append(args, *v)\n\t}\n\tif v := find.RowStatus; v != nil {\n\t\twhere, args = append(where, \"`memo`.`row_status` = ?\"), append(args, *v)\n\t}\n\tif v := find.VisibilityList; len(v) != 0 {\n\t\tplaceholder := []string{}\n\t\tfor _, visibility := range v {\n\t\t\tplaceholder = append(placeholder, \"?\")\n\t\t\targs = append(args, visibility.String())\n\t\t}\n\t\twhere = append(where, fmt.Sprintf(\"`memo`.`visibility` in (%s)\", strings.Join(placeholder, \",\")))\n\t}\n\tif find.ExcludeComments {\n\t\thaving = append(having, \"`parent_uid` IS NULL\")\n\t}\n\n\torder := \"DESC\"\n\tif find.OrderByTimeAsc {\n\t\torder = \"ASC\"\n\t}\n\torderBy := []string{}\n\tif find.OrderByPinned {\n\t\torderBy = append(orderBy, \"`pinned` DESC\")\n\t}\n\tif find.OrderByUpdatedTs {\n\t\torderBy = append(orderBy, \"`updated_ts` \"+order)\n\t} else {\n\t\torderBy = append(orderBy, \"`created_ts` \"+order)\n\t}\n\t// Add id as final tie-breaker\n\torderBy = append(orderBy, \"`id` DESC\")\n\tfields := []string{\n\t\t\"`memo`.`id` AS `id`\",\n\t\t\"`memo`.`uid` AS `uid`\",\n\t\t\"`memo`.`creator_id` AS `creator_id`\",\n\t\t\"UNIX_TIMESTAMP(`memo`.`created_ts`) AS `created_ts`\",\n\t\t\"UNIX_TIMESTAMP(`memo`.`updated_ts`) AS `updated_ts`\",\n\t\t\"`memo`.`row_status` AS `row_status`\",\n\t\t\"`memo`.`visibility` AS `visibility`\",\n\t\t\"`memo`.`pinned` AS `pinned`\",\n\t\t\"`memo`.`payload` AS `payload`\",\n\t\t\"CASE WHEN `parent_memo`.`uid` IS NOT NULL THEN `parent_memo`.`uid` ELSE NULL END AS `parent_uid`\",\n\t}\n\tif !find.ExcludeContent {\n\t\tfields = append(fields, \"`memo`.`content` AS `content`\")\n\t}\n\n\tquery := \"SELECT \" + strings.Join(fields, \", \") + \" FROM `memo`\" + \" \" +\n\t\t\"LEFT JOIN `memo_relation` ON `memo`.`id` = `memo_relation`.`memo_id` AND `memo_relation`.`type` = 'COMMENT'\" + \" \" +\n\t\t\"LEFT JOIN `memo` AS `parent_memo` ON `memo_relation`.`related_memo_id` = `parent_memo`.`id`\" + \" \" +\n\t\t\"WHERE \" + strings.Join(where, \" AND \") + \" \" +\n\t\t\"HAVING \" + strings.Join(having, \" AND \") + \" \" +\n\t\t\"ORDER BY \" + strings.Join(orderBy, \", \")\n\tif find.Limit != nil {\n\t\tquery = fmt.Sprintf(\"%s LIMIT %d\", query, *find.Limit)\n\t\tif find.Offset != nil {\n\t\t\tquery = fmt.Sprintf(\"%s OFFSET %d\", query, *find.Offset)\n\t\t}\n\t}\n\n\trows, err := d.db.QueryContext(ctx, query, args...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\n\tlist := make([]*store.Memo, 0)\n\tfor rows.Next() {\n\t\tvar memo store.Memo\n\t\tvar payloadBytes []byte\n\t\tdests := []any{\n\t\t\t&memo.ID,\n\t\t\t&memo.UID,\n\t\t\t&memo.CreatorID,\n\t\t\t&memo.CreatedTs,\n\t\t\t&memo.UpdatedTs,\n\t\t\t&memo.RowStatus,\n\t\t\t&memo.Visibility,\n\t\t\t&memo.Pinned,\n\t\t\t&payloadBytes,\n\t\t\t&memo.ParentUID,\n\t\t}\n\t\tif !find.ExcludeContent {\n\t\t\tdests = append(dests, &memo.Content)\n\t\t}\n\t\tif err := rows.Scan(dests...); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tpayload := &storepb.MemoPayload{}\n\t\tif err := protojsonUnmarshaler.Unmarshal(payloadBytes, payload); err != nil {\n\t\t\treturn nil, errors.Wrap(err, \"failed to unmarshal payload\")\n\t\t}\n\t\tmemo.Payload = payload\n\t\tlist = append(list, &memo)\n\t}\n\n\tif err := rows.Err(); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn list, nil\n}\n\nfunc (d *DB) GetMemo(ctx context.Context, find *store.FindMemo) (*store.Memo, error) {\n\tlist, err := d.ListMemos(ctx, find)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif len(list) == 0 {\n\t\treturn nil, nil\n\t}\n\n\tmemo := list[0]\n\treturn memo, nil\n}\n\nfunc (d *DB) UpdateMemo(ctx context.Context, update *store.UpdateMemo) error {\n\tset, args := []string{}, []any{}\n\tif v := update.UID; v != nil {\n\t\tset, args = append(set, \"`uid` = ?\"), append(args, *v)\n\t}\n\tif v := update.CreatedTs; v != nil {\n\t\tset, args = append(set, \"`created_ts` = FROM_UNIXTIME(?)\"), append(args, *v)\n\t}\n\tif v := update.UpdatedTs; v != nil {\n\t\tset, args = append(set, \"`updated_ts` = FROM_UNIXTIME(?)\"), append(args, *v)\n\t}\n\tif v := update.RowStatus; v != nil {\n\t\tset, args = append(set, \"`row_status` = ?\"), append(args, *v)\n\t}\n\tif v := update.Content; v != nil {\n\t\tset, args = append(set, \"`content` = ?\"), append(args, *v)\n\t}\n\tif v := update.Visibility; v != nil {\n\t\tset, args = append(set, \"`visibility` = ?\"), append(args, *v)\n\t}\n\tif v := update.Pinned; v != nil {\n\t\tset, args = append(set, \"`pinned` = ?\"), append(args, *v)\n\t}\n\tif v := update.Payload; v != nil {\n\t\tpayloadBytes, err := protojson.Marshal(v)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tset, args = append(set, \"`payload` = ?\"), append(args, string(payloadBytes))\n\t}\n\tif len(set) == 0 {\n\t\treturn nil\n\t}\n\targs = append(args, update.ID)\n\n\tstmt := \"UPDATE `memo` SET \" + strings.Join(set, \", \") + \" WHERE `id` = ?\"\n\tif _, err := d.db.ExecContext(ctx, stmt, args...); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc (d *DB) DeleteMemo(ctx context.Context, delete *store.DeleteMemo) error {\n\twhere, args := []string{\"`id` = ?\"}, []any{delete.ID}\n\tstmt := \"DELETE FROM `memo` WHERE \" + strings.Join(where, \" AND \")\n\tresult, err := d.db.ExecContext(ctx, stmt, args...)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif _, err := result.RowsAffected(); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "store/db/mysql/memo_relation.go",
    "content": "package mysql\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/usememos/memos/plugin/filter\"\n\t\"github.com/usememos/memos/store\"\n)\n\nfunc (d *DB) UpsertMemoRelation(ctx context.Context, create *store.MemoRelation) (*store.MemoRelation, error) {\n\tstmt := \"INSERT INTO `memo_relation` (`memo_id`, `related_memo_id`, `type`) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE `type` = `type`\"\n\t_, err := d.db.ExecContext(\n\t\tctx,\n\t\tstmt,\n\t\tcreate.MemoID,\n\t\tcreate.RelatedMemoID,\n\t\tcreate.Type,\n\t)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tmemoRelation := store.MemoRelation{\n\t\tMemoID:        create.MemoID,\n\t\tRelatedMemoID: create.RelatedMemoID,\n\t\tType:          create.Type,\n\t}\n\n\treturn &memoRelation, nil\n}\n\nfunc (d *DB) ListMemoRelations(ctx context.Context, find *store.FindMemoRelation) ([]*store.MemoRelation, error) {\n\twhere, args := []string{\"TRUE\"}, []any{}\n\tif find.MemoID != nil {\n\t\twhere, args = append(where, \"`memo_id` = ?\"), append(args, find.MemoID)\n\t}\n\tif find.RelatedMemoID != nil {\n\t\twhere, args = append(where, \"`related_memo_id` = ?\"), append(args, find.RelatedMemoID)\n\t}\n\tif find.Type != nil {\n\t\twhere, args = append(where, \"`type` = ?\"), append(args, find.Type)\n\t}\n\tif len(find.MemoIDList) > 0 {\n\t\tplaceholders := make([]string, len(find.MemoIDList))\n\t\tfor i, id := range find.MemoIDList {\n\t\t\tplaceholders[i] = \"?\"\n\t\t\targs = append(args, id)\n\t\t}\n\t\tinClause := strings.Join(placeholders, \", \")\n\t\tfor _, id := range find.MemoIDList {\n\t\t\targs = append(args, id)\n\t\t}\n\t\twhere = append(where, fmt.Sprintf(\"(`memo_id` IN (%s) OR `related_memo_id` IN (%s))\", inClause, inClause))\n\t}\n\tif find.MemoFilter != nil {\n\t\tengine, err := filter.DefaultEngine()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tstmt, err := engine.CompileToStatement(ctx, *find.MemoFilter, filter.RenderOptions{\n\t\t\tDialect:           filter.DialectMySQL,\n\t\t\tPlaceholderOffset: 0,\n\t\t})\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif stmt.SQL != \"\" {\n\t\t\twhere = append(where, fmt.Sprintf(\"memo_id IN (SELECT id FROM memo WHERE %s)\", stmt.SQL))\n\t\t\twhere = append(where, fmt.Sprintf(\"related_memo_id IN (SELECT id FROM memo WHERE %s)\", stmt.SQL))\n\t\t\targs = append(args, append(stmt.Args, stmt.Args...)...)\n\t\t}\n\t}\n\n\trows, err := d.db.QueryContext(ctx, \"SELECT `memo_id`, `related_memo_id`, `type` FROM `memo_relation` WHERE \"+strings.Join(where, \" AND \"), args...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\n\tlist := []*store.MemoRelation{}\n\tfor rows.Next() {\n\t\tmemoRelation := &store.MemoRelation{}\n\t\tif err := rows.Scan(\n\t\t\t&memoRelation.MemoID,\n\t\t\t&memoRelation.RelatedMemoID,\n\t\t\t&memoRelation.Type,\n\t\t); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tlist = append(list, memoRelation)\n\t}\n\n\tif err := rows.Err(); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn list, nil\n}\n\nfunc (d *DB) DeleteMemoRelation(ctx context.Context, delete *store.DeleteMemoRelation) error {\n\twhere, args := []string{\"TRUE\"}, []any{}\n\tif delete.MemoID != nil {\n\t\twhere, args = append(where, \"`memo_id` = ?\"), append(args, delete.MemoID)\n\t}\n\tif delete.RelatedMemoID != nil {\n\t\twhere, args = append(where, \"`related_memo_id` = ?\"), append(args, delete.RelatedMemoID)\n\t}\n\tif delete.Type != nil {\n\t\twhere, args = append(where, \"`type` = ?\"), append(args, delete.Type)\n\t}\n\tstmt := \"DELETE FROM `memo_relation` WHERE \" + strings.Join(where, \" AND \")\n\tresult, err := d.db.ExecContext(ctx, stmt, args...)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif _, err = result.RowsAffected(); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "store/db/mysql/memo_share.go",
    "content": "package mysql\n\nimport (\n\t\"context\"\n\t\"strings\"\n\n\t\"github.com/pkg/errors\"\n\n\t\"github.com/usememos/memos/store\"\n)\n\nfunc (d *DB) CreateMemoShare(ctx context.Context, create *store.MemoShare) (*store.MemoShare, error) {\n\tfields := []string{\"`uid`\", \"`memo_id`\", \"`creator_id`\"}\n\tplaceholders := []string{\"?\", \"?\", \"?\"}\n\targs := []any{create.UID, create.MemoID, create.CreatorID}\n\n\tif create.ExpiresTs != nil {\n\t\tfields = append(fields, \"`expires_ts`\")\n\t\tplaceholders = append(placeholders, \"?\")\n\t\targs = append(args, *create.ExpiresTs)\n\t}\n\n\tstmt := \"INSERT INTO `memo_share` (\" + strings.Join(fields, \", \") + \") VALUES (\" + strings.Join(placeholders, \", \") + \")\"\n\tresult, err := d.db.ExecContext(ctx, stmt, args...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\trawID, err := result.LastInsertId()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tid := int32(rawID)\n\tms, err := d.GetMemoShare(ctx, &store.FindMemoShare{ID: &id})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif ms == nil {\n\t\treturn nil, errors.Errorf(\"failed to create memo share\")\n\t}\n\treturn ms, nil\n}\n\nfunc (d *DB) ListMemoShares(ctx context.Context, find *store.FindMemoShare) ([]*store.MemoShare, error) {\n\twhere, args := []string{\"1 = 1\"}, []any{}\n\n\tif find.ID != nil {\n\t\twhere, args = append(where, \"`id` = ?\"), append(args, *find.ID)\n\t}\n\tif find.UID != nil {\n\t\twhere, args = append(where, \"`uid` = ?\"), append(args, *find.UID)\n\t}\n\tif find.MemoID != nil {\n\t\twhere, args = append(where, \"`memo_id` = ?\"), append(args, *find.MemoID)\n\t}\n\n\trows, err := d.db.QueryContext(ctx, `\n\t\tSELECT\n\t\t\tid,\n\t\t\tuid,\n\t\t\tmemo_id,\n\t\t\tcreator_id,\n\t\t\tcreated_ts,\n\t\t\texpires_ts\n\t\tFROM memo_share\n\t\tWHERE `+strings.Join(where, \" AND \")+`\n\t\tORDER BY id ASC`,\n\t\targs...,\n\t)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\n\tlist := []*store.MemoShare{}\n\tfor rows.Next() {\n\t\tms := &store.MemoShare{}\n\t\tif err := rows.Scan(\n\t\t\t&ms.ID,\n\t\t\t&ms.UID,\n\t\t\t&ms.MemoID,\n\t\t\t&ms.CreatorID,\n\t\t\t&ms.CreatedTs,\n\t\t\t&ms.ExpiresTs,\n\t\t); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tlist = append(list, ms)\n\t}\n\n\tif err := rows.Err(); err != nil {\n\t\treturn nil, err\n\t}\n\treturn list, nil\n}\n\nfunc (d *DB) GetMemoShare(ctx context.Context, find *store.FindMemoShare) (*store.MemoShare, error) {\n\tlist, err := d.ListMemoShares(ctx, find)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif len(list) == 0 {\n\t\treturn nil, nil\n\t}\n\treturn list[0], nil\n}\n\nfunc (d *DB) DeleteMemoShare(ctx context.Context, delete *store.DeleteMemoShare) error {\n\twhere, args := []string{\"1 = 1\"}, []any{}\n\tif delete.ID != nil {\n\t\twhere, args = append(where, \"`id` = ?\"), append(args, *delete.ID)\n\t}\n\tif delete.UID != nil {\n\t\twhere, args = append(where, \"`uid` = ?\"), append(args, *delete.UID)\n\t}\n\t_, err := d.db.ExecContext(ctx, \"DELETE FROM `memo_share` WHERE \"+strings.Join(where, \" AND \"), args...)\n\treturn err\n}\n"
  },
  {
    "path": "store/db/mysql/mysql.go",
    "content": "package mysql\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\n\t\"github.com/go-sql-driver/mysql\"\n\t\"github.com/pkg/errors\"\n\n\t\"github.com/usememos/memos/internal/profile\"\n\t\"github.com/usememos/memos/store\"\n)\n\ntype DB struct {\n\tdb      *sql.DB\n\tprofile *profile.Profile\n\tconfig  *mysql.Config\n}\n\nfunc NewDB(profile *profile.Profile) (store.Driver, error) {\n\t// Open MySQL connection with parameter.\n\t// multiStatements=true is required for migration.\n\t// See more in: https://github.com/go-sql-driver/mysql#multistatements\n\tdsn, err := mergeDSN(profile.DSN)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tdriver := DB{profile: profile}\n\tdriver.config, err = mysql.ParseDSN(dsn)\n\tif err != nil {\n\t\treturn nil, errors.New(\"Parse DSN error\")\n\t}\n\n\tdriver.db, err = sql.Open(\"mysql\", dsn)\n\tif err != nil {\n\t\treturn nil, errors.Wrapf(err, \"failed to open db: %s\", profile.DSN)\n\t}\n\n\treturn &driver, nil\n}\n\nfunc (d *DB) GetDB() *sql.DB {\n\treturn d.db\n}\n\nfunc (d *DB) Close() error {\n\treturn d.db.Close()\n}\n\nfunc (d *DB) IsInitialized(ctx context.Context) (bool, error) {\n\tvar exists bool\n\terr := d.db.QueryRowContext(ctx, \"SELECT EXISTS (SELECT 1 FROM information_schema.tables WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'memo' AND TABLE_TYPE = 'BASE TABLE')\").Scan(&exists)\n\tif err != nil {\n\t\treturn false, errors.Wrap(err, \"failed to check if database is initialized\")\n\t}\n\treturn exists, nil\n}\n\nfunc mergeDSN(baseDSN string) (string, error) {\n\tconfig, err := mysql.ParseDSN(baseDSN)\n\tif err != nil {\n\t\treturn \"\", errors.Wrapf(err, \"failed to parse DSN: %s\", baseDSN)\n\t}\n\n\tconfig.MultiStatements = true\n\treturn config.FormatDSN(), nil\n}\n"
  },
  {
    "path": "store/db/mysql/reaction.go",
    "content": "package mysql\n\nimport (\n\t\"context\"\n\t\"strings\"\n\n\t\"github.com/pkg/errors\"\n\n\t\"github.com/usememos/memos/store\"\n)\n\nfunc (d *DB) UpsertReaction(ctx context.Context, upsert *store.Reaction) (*store.Reaction, error) {\n\tfields := []string{\"`creator_id`\", \"`content_id`\", \"`reaction_type`\"}\n\tplaceholder := []string{\"?\", \"?\", \"?\"}\n\targs := []interface{}{upsert.CreatorID, upsert.ContentID, upsert.ReactionType}\n\tstmt := \"INSERT INTO `reaction` (\" + strings.Join(fields, \", \") + \") VALUES (\" + strings.Join(placeholder, \", \") + \")\"\n\tresult, err := d.db.ExecContext(ctx, stmt, args...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\trawID, err := result.LastInsertId()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tid := int32(rawID)\n\treaction, err := d.GetReaction(ctx, &store.FindReaction{ID: &id})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif reaction == nil {\n\t\treturn nil, errors.Errorf(\"failed to create reaction\")\n\t}\n\treturn reaction, nil\n}\n\nfunc (d *DB) ListReactions(ctx context.Context, find *store.FindReaction) ([]*store.Reaction, error) {\n\twhere, args := []string{\"1 = 1\"}, []any{}\n\n\tif find.ID != nil {\n\t\twhere, args = append(where, \"`id` = ?\"), append(args, *find.ID)\n\t}\n\tif find.CreatorID != nil {\n\t\twhere, args = append(where, \"`creator_id` = ?\"), append(args, *find.CreatorID)\n\t}\n\tif find.ContentID != nil {\n\t\twhere, args = append(where, \"`content_id` = ?\"), append(args, *find.ContentID)\n\t}\n\tif len(find.ContentIDList) > 0 {\n\t\tplaceholders := make([]string, 0, len(find.ContentIDList))\n\t\tfor _, id := range find.ContentIDList {\n\t\t\tplaceholders = append(placeholders, \"?\")\n\t\t\targs = append(args, id)\n\t\t}\n\t\twhere = append(where, \"`content_id` IN (\"+strings.Join(placeholders, \",\")+\")\")\n\t}\n\n\trows, err := d.db.QueryContext(ctx, `\n\t\tSELECT\n\t\t\tid,\n\t\t\tUNIX_TIMESTAMP(created_ts) AS created_ts,\n\t\t\tcreator_id,\n\t\t\tcontent_id,\n\t\t\treaction_type\n\t\tFROM reaction\n\t\tWHERE `+strings.Join(where, \" AND \")+`\n\t\tORDER BY id ASC`,\n\t\targs...,\n\t)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\n\tlist := []*store.Reaction{}\n\tfor rows.Next() {\n\t\treaction := &store.Reaction{}\n\t\tif err := rows.Scan(\n\t\t\t&reaction.ID,\n\t\t\t&reaction.CreatedTs,\n\t\t\t&reaction.CreatorID,\n\t\t\t&reaction.ContentID,\n\t\t\t&reaction.ReactionType,\n\t\t); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tlist = append(list, reaction)\n\t}\n\n\tif err := rows.Err(); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn list, nil\n}\n\nfunc (d *DB) GetReaction(ctx context.Context, find *store.FindReaction) (*store.Reaction, error) {\n\tlist, err := d.ListReactions(ctx, find)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif len(list) == 0 {\n\t\treturn nil, nil\n\t}\n\n\treaction := list[0]\n\treturn reaction, nil\n}\n\nfunc (d *DB) DeleteReaction(ctx context.Context, delete *store.DeleteReaction) error {\n\t_, err := d.db.ExecContext(ctx, \"DELETE FROM `reaction` WHERE `id` = ?\", delete.ID)\n\treturn err\n}\n"
  },
  {
    "path": "store/db/mysql/user.go",
    "content": "package mysql\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/pkg/errors\"\n\n\t\"github.com/usememos/memos/store\"\n)\n\nfunc (d *DB) CreateUser(ctx context.Context, create *store.User) (*store.User, error) {\n\tfields := []string{\"`username`\", \"`role`\", \"`email`\", \"`nickname`\", \"`password_hash`\", \"`avatar_url`\"}\n\tplaceholder := []string{\"?\", \"?\", \"?\", \"?\", \"?\", \"?\"}\n\targs := []any{create.Username, create.Role, create.Email, create.Nickname, create.PasswordHash, create.AvatarURL}\n\n\tstmt := \"INSERT INTO user (\" + strings.Join(fields, \", \") + \") VALUES (\" + strings.Join(placeholder, \", \") + \")\"\n\tresult, err := d.db.ExecContext(ctx, stmt, args...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tid, err := result.LastInsertId()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tid32 := int32(id)\n\tlist, err := d.ListUsers(ctx, &store.FindUser{ID: &id32})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif len(list) != 1 {\n\t\treturn nil, errors.Errorf(\"unexpected user count: %d\", len(list))\n\t}\n\n\treturn list[0], nil\n}\n\nfunc (d *DB) UpdateUser(ctx context.Context, update *store.UpdateUser) (*store.User, error) {\n\tset, args := []string{}, []any{}\n\tif v := update.UpdatedTs; v != nil {\n\t\tset, args = append(set, \"`updated_ts` = FROM_UNIXTIME(?)\"), append(args, *v)\n\t}\n\tif v := update.RowStatus; v != nil {\n\t\tset, args = append(set, \"`row_status` = ?\"), append(args, *v)\n\t}\n\tif v := update.Username; v != nil {\n\t\tset, args = append(set, \"`username` = ?\"), append(args, *v)\n\t}\n\tif v := update.Email; v != nil {\n\t\tset, args = append(set, \"`email` = ?\"), append(args, *v)\n\t}\n\tif v := update.Nickname; v != nil {\n\t\tset, args = append(set, \"`nickname` = ?\"), append(args, *v)\n\t}\n\tif v := update.AvatarURL; v != nil {\n\t\tset, args = append(set, \"`avatar_url` = ?\"), append(args, *v)\n\t}\n\tif v := update.PasswordHash; v != nil {\n\t\tset, args = append(set, \"`password_hash` = ?\"), append(args, *v)\n\t}\n\tif v := update.Description; v != nil {\n\t\tset, args = append(set, \"`description` = ?\"), append(args, *v)\n\t}\n\tif v := update.Role; v != nil {\n\t\tset, args = append(set, \"`role` = ?\"), append(args, *v)\n\t}\n\targs = append(args, update.ID)\n\n\tquery := \"UPDATE `user` SET \" + strings.Join(set, \", \") + \" WHERE `id` = ?\"\n\tif _, err := d.db.ExecContext(ctx, query, args...); err != nil {\n\t\treturn nil, err\n\t}\n\n\tuser, err := d.GetUser(ctx, &store.FindUser{ID: &update.ID})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn user, nil\n}\n\nfunc (d *DB) ListUsers(ctx context.Context, find *store.FindUser) ([]*store.User, error) {\n\twhere, args := []string{\"1 = 1\"}, []any{}\n\n\tif len(find.Filters) > 0 {\n\t\treturn nil, errors.Errorf(\"user filters are not supported\")\n\t}\n\n\tif v := find.ID; v != nil {\n\t\twhere, args = append(where, \"`id` = ?\"), append(args, *v)\n\t}\n\tif v := find.Username; v != nil {\n\t\twhere, args = append(where, \"`username` = ?\"), append(args, *v)\n\t}\n\tif v := find.Role; v != nil {\n\t\twhere, args = append(where, \"`role` = ?\"), append(args, *v)\n\t}\n\tif v := find.Email; v != nil {\n\t\twhere, args = append(where, \"`email` = ?\"), append(args, *v)\n\t}\n\tif v := find.Nickname; v != nil {\n\t\twhere, args = append(where, \"`nickname` = ?\"), append(args, *v)\n\t}\n\n\torderBy := []string{\"`created_ts` DESC\", \"`row_status` DESC\"}\n\tquery := \"SELECT `id`, `username`, `role`, `email`, `nickname`, `password_hash`, `avatar_url`, `description`, UNIX_TIMESTAMP(`created_ts`), UNIX_TIMESTAMP(`updated_ts`), `row_status` FROM `user` WHERE \" + strings.Join(where, \" AND \") + \" ORDER BY \" + strings.Join(orderBy, \", \")\n\tif v := find.Limit; v != nil {\n\t\tquery += fmt.Sprintf(\" LIMIT %d\", *v)\n\t}\n\trows, err := d.db.QueryContext(ctx, query, args...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\n\tlist := make([]*store.User, 0)\n\tfor rows.Next() {\n\t\tvar user store.User\n\t\tif err := rows.Scan(\n\t\t\t&user.ID,\n\t\t\t&user.Username,\n\t\t\t&user.Role,\n\t\t\t&user.Email,\n\t\t\t&user.Nickname,\n\t\t\t&user.PasswordHash,\n\t\t\t&user.AvatarURL,\n\t\t\t&user.Description,\n\t\t\t&user.CreatedTs,\n\t\t\t&user.UpdatedTs,\n\t\t\t&user.RowStatus,\n\t\t); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tlist = append(list, &user)\n\t}\n\n\tif err := rows.Err(); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn list, nil\n}\n\nfunc (d *DB) GetUser(ctx context.Context, find *store.FindUser) (*store.User, error) {\n\tlist, err := d.ListUsers(ctx, find)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif len(list) != 1 {\n\t\treturn nil, errors.Errorf(\"unexpected user count: %d\", len(list))\n\t}\n\treturn list[0], nil\n}\n\nfunc (d *DB) DeleteUser(ctx context.Context, delete *store.DeleteUser) error {\n\tresult, err := d.db.ExecContext(ctx, \"DELETE FROM `user` WHERE `id` = ?\", delete.ID)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif _, err := result.RowsAffected(); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "store/db/mysql/user_setting.go",
    "content": "package mysql\n\nimport (\n\t\"context\"\n\t\"strings\"\n\n\t\"github.com/pkg/errors\"\n\n\tstorepb \"github.com/usememos/memos/proto/gen/store\"\n\t\"github.com/usememos/memos/store\"\n)\n\nfunc (d *DB) UpsertUserSetting(ctx context.Context, upsert *store.UserSetting) (*store.UserSetting, error) {\n\tstmt := \"INSERT INTO `user_setting` (`user_id`, `key`, `value`) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE `value` = ?\"\n\tif _, err := d.db.ExecContext(ctx, stmt, upsert.UserID, upsert.Key.String(), upsert.Value, upsert.Value); err != nil {\n\t\treturn nil, err\n\t}\n\treturn upsert, nil\n}\n\nfunc (d *DB) ListUserSettings(ctx context.Context, find *store.FindUserSetting) ([]*store.UserSetting, error) {\n\twhere, args := []string{\"1 = 1\"}, []any{}\n\n\tif v := find.Key; v != storepb.UserSetting_KEY_UNSPECIFIED {\n\t\twhere, args = append(where, \"`key` = ?\"), append(args, v.String())\n\t}\n\tif v := find.UserID; v != nil {\n\t\twhere, args = append(where, \"`user_id` = ?\"), append(args, *find.UserID)\n\t}\n\n\tquery := \"SELECT `user_id`, `key`, `value` FROM `user_setting` WHERE \" + strings.Join(where, \" AND \")\n\trows, err := d.db.QueryContext(ctx, query, args...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\n\tuserSettingList := make([]*store.UserSetting, 0)\n\tfor rows.Next() {\n\t\tuserSetting := &store.UserSetting{}\n\t\tvar keyString string\n\t\tif err := rows.Scan(\n\t\t\t&userSetting.UserID,\n\t\t\t&keyString,\n\t\t\t&userSetting.Value,\n\t\t); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tuserSetting.Key = storepb.UserSetting_Key(storepb.UserSetting_Key_value[keyString])\n\t\tuserSettingList = append(userSettingList, userSetting)\n\t}\n\n\tif err := rows.Err(); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn userSettingList, nil\n}\n\nfunc (d *DB) GetUserByPATHash(ctx context.Context, tokenHash string) (*store.PATQueryResult, error) {\n\tquery := `\n\t\tSELECT\n\t\t\tuser_id,\n\t\t\tvalue\n\t\tFROM user_setting\n\t\tWHERE ` + \"`key`\" + ` = 'PERSONAL_ACCESS_TOKENS'\n\t\t  AND JSON_SEARCH(value, 'one', ?, NULL, '$.tokens[*].tokenHash') IS NOT NULL\n\t`\n\n\tvar userID int32\n\tvar tokensJSON string\n\n\terr := d.db.QueryRowContext(ctx, query, tokenHash).Scan(&userID, &tokensJSON)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tpatsUserSetting := &storepb.PersonalAccessTokensUserSetting{}\n\tif err := protojsonUnmarshaler.Unmarshal([]byte(tokensJSON), patsUserSetting); err != nil {\n\t\treturn nil, err\n\t}\n\n\tfor _, pat := range patsUserSetting.Tokens {\n\t\tif pat.TokenHash == tokenHash {\n\t\t\treturn &store.PATQueryResult{\n\t\t\t\tUserID: userID,\n\t\t\t\tPAT:    pat,\n\t\t\t}, nil\n\t\t}\n\t}\n\n\treturn nil, errors.New(\"PAT not found\")\n}\n"
  },
  {
    "path": "store/db/postgres/attachment.go",
    "content": "package postgres\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/pkg/errors\"\n\t\"google.golang.org/protobuf/encoding/protojson\"\n\n\t\"github.com/usememos/memos/plugin/filter\"\n\tstorepb \"github.com/usememos/memos/proto/gen/store\"\n\t\"github.com/usememos/memos/store\"\n)\n\nfunc (d *DB) CreateAttachment(ctx context.Context, create *store.Attachment) (*store.Attachment, error) {\n\tfields := []string{\"uid\", \"filename\", \"blob\", \"type\", \"size\", \"creator_id\", \"memo_id\", \"storage_type\", \"reference\", \"payload\"}\n\tstorageType := \"\"\n\tif create.StorageType != storepb.AttachmentStorageType_ATTACHMENT_STORAGE_TYPE_UNSPECIFIED {\n\t\tstorageType = create.StorageType.String()\n\t}\n\tpayloadString := \"{}\"\n\tif create.Payload != nil {\n\t\tbytes, err := protojson.Marshal(create.Payload)\n\t\tif err != nil {\n\t\t\treturn nil, errors.Wrap(err, \"failed to marshal attachment payload\")\n\t\t}\n\t\tpayloadString = string(bytes)\n\t}\n\targs := []any{create.UID, create.Filename, create.Blob, create.Type, create.Size, create.CreatorID, create.MemoID, storageType, create.Reference, payloadString}\n\n\tstmt := \"INSERT INTO attachment (\" + strings.Join(fields, \", \") + \") VALUES (\" + placeholders(len(args)) + \") RETURNING id, created_ts, updated_ts\"\n\tif err := d.db.QueryRowContext(ctx, stmt, args...).Scan(&create.ID, &create.CreatedTs, &create.UpdatedTs); err != nil {\n\t\treturn nil, err\n\t}\n\treturn create, nil\n}\n\nfunc (d *DB) ListAttachments(ctx context.Context, find *store.FindAttachment) ([]*store.Attachment, error) {\n\twhere, args := []string{\"1 = 1\"}, []any{}\n\n\tif v := find.ID; v != nil {\n\t\twhere, args = append(where, \"attachment.id = \"+placeholder(len(args)+1)), append(args, *v)\n\t}\n\tif v := find.UID; v != nil {\n\t\twhere, args = append(where, \"attachment.uid = \"+placeholder(len(args)+1)), append(args, *v)\n\t}\n\tif v := find.CreatorID; v != nil {\n\t\twhere, args = append(where, \"attachment.creator_id = \"+placeholder(len(args)+1)), append(args, *v)\n\t}\n\tif v := find.Filename; v != nil {\n\t\twhere, args = append(where, \"attachment.filename = \"+placeholder(len(args)+1)), append(args, *v)\n\t}\n\tif v := find.FilenameSearch; v != nil {\n\t\twhere, args = append(where, \"attachment.filename LIKE \"+placeholder(len(args)+1)), append(args, fmt.Sprintf(\"%%%s%%\", *v))\n\t}\n\tif v := find.MemoID; v != nil {\n\t\twhere, args = append(where, \"attachment.memo_id = \"+placeholder(len(args)+1)), append(args, *v)\n\t}\n\tif len(find.MemoIDList) > 0 {\n\t\tholders := make([]string, 0, len(find.MemoIDList))\n\t\tfor _, id := range find.MemoIDList {\n\t\t\tholders = append(holders, placeholder(len(args)+1))\n\t\t\targs = append(args, id)\n\t\t}\n\t\twhere = append(where, \"attachment.memo_id IN (\"+strings.Join(holders, \", \")+\")\")\n\t}\n\tif find.HasRelatedMemo {\n\t\twhere = append(where, \"attachment.memo_id IS NOT NULL\")\n\t}\n\tif v := find.StorageType; v != nil {\n\t\twhere, args = append(where, \"attachment.storage_type = \"+placeholder(len(args)+1)), append(args, v.String())\n\t}\n\n\tif len(find.Filters) > 0 {\n\t\tengine, err := filter.DefaultAttachmentEngine()\n\t\tif err != nil {\n\t\t\treturn nil, errors.Wrap(err, \"failed to get filter engine\")\n\t\t}\n\t\tif err := filter.AppendConditions(ctx, engine, find.Filters, filter.DialectPostgres, &where, &args); err != nil {\n\t\t\treturn nil, errors.Wrap(err, \"failed to append filter conditions\")\n\t\t}\n\t}\n\n\tfields := []string{\n\t\t\"attachment.id AS id\",\n\t\t\"attachment.uid AS uid\",\n\t\t\"attachment.filename AS filename\",\n\t\t\"attachment.type AS type\",\n\t\t\"attachment.size AS size\",\n\t\t\"attachment.creator_id AS creator_id\",\n\t\t\"attachment.created_ts AS created_ts\",\n\t\t\"attachment.updated_ts AS updated_ts\",\n\t\t\"attachment.memo_id AS memo_id\",\n\t\t\"attachment.storage_type AS storage_type\",\n\t\t\"attachment.reference AS reference\",\n\t\t\"attachment.payload AS payload\",\n\t\t\"CASE WHEN memo.uid IS NOT NULL THEN memo.uid ELSE NULL END AS memo_uid\",\n\t}\n\tif find.GetBlob {\n\t\tfields = append(fields, \"attachment.blob AS blob\")\n\t}\n\n\tquery := fmt.Sprintf(`\n\t\tSELECT\n\t\t\t%s\n\t\tFROM attachment\n\t\tLEFT JOIN memo ON attachment.memo_id = memo.id\n\t\tWHERE %s\n\t\tORDER BY attachment.updated_ts DESC\n\t`, strings.Join(fields, \", \"), strings.Join(where, \" AND \"))\n\tif find.Limit != nil {\n\t\tquery = fmt.Sprintf(\"%s LIMIT %d\", query, *find.Limit)\n\t\tif find.Offset != nil {\n\t\t\tquery = fmt.Sprintf(\"%s OFFSET %d\", query, *find.Offset)\n\t\t}\n\t}\n\n\trows, err := d.db.QueryContext(ctx, query, args...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\n\tlist := make([]*store.Attachment, 0)\n\tfor rows.Next() {\n\t\tattachment := store.Attachment{}\n\t\tvar memoID sql.NullInt32\n\t\tvar storageType string\n\t\tvar payloadBytes []byte\n\t\tdests := []any{\n\t\t\t&attachment.ID,\n\t\t\t&attachment.UID,\n\t\t\t&attachment.Filename,\n\t\t\t&attachment.Type,\n\t\t\t&attachment.Size,\n\t\t\t&attachment.CreatorID,\n\t\t\t&attachment.CreatedTs,\n\t\t\t&attachment.UpdatedTs,\n\t\t\t&memoID,\n\t\t\t&storageType,\n\t\t\t&attachment.Reference,\n\t\t\t&payloadBytes,\n\t\t\t&attachment.MemoUID,\n\t\t}\n\t\tif find.GetBlob {\n\t\t\tdests = append(dests, &attachment.Blob)\n\t\t}\n\t\tif err := rows.Scan(dests...); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tif memoID.Valid {\n\t\t\tattachment.MemoID = &memoID.Int32\n\t\t}\n\t\tattachment.StorageType = storepb.AttachmentStorageType(storepb.AttachmentStorageType_value[storageType])\n\t\tpayload := &storepb.AttachmentPayload{}\n\t\tif err := protojsonUnmarshaler.Unmarshal(payloadBytes, payload); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tattachment.Payload = payload\n\t\tlist = append(list, &attachment)\n\t}\n\n\tif err := rows.Err(); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn list, nil\n}\n\nfunc (d *DB) UpdateAttachment(ctx context.Context, update *store.UpdateAttachment) error {\n\tset, args := []string{}, []any{}\n\n\tif v := update.UID; v != nil {\n\t\tset, args = append(set, \"uid = \"+placeholder(len(args)+1)), append(args, *v)\n\t}\n\tif v := update.UpdatedTs; v != nil {\n\t\tset, args = append(set, \"updated_ts = \"+placeholder(len(args)+1)), append(args, *v)\n\t}\n\tif v := update.Filename; v != nil {\n\t\tset, args = append(set, \"filename = \"+placeholder(len(args)+1)), append(args, *v)\n\t}\n\tif v := update.MemoID; v != nil {\n\t\tset, args = append(set, \"memo_id = \"+placeholder(len(args)+1)), append(args, *v)\n\t}\n\tif v := update.Reference; v != nil {\n\t\tset, args = append(set, \"reference = \"+placeholder(len(args)+1)), append(args, *v)\n\t}\n\tif v := update.Payload; v != nil {\n\t\tbytes, err := protojson.Marshal(v)\n\t\tif err != nil {\n\t\t\treturn errors.Wrap(err, \"failed to marshal attachment payload\")\n\t\t}\n\t\tset, args = append(set, \"payload = \"+placeholder(len(args)+1)), append(args, string(bytes))\n\t}\n\n\tstmt := `UPDATE attachment SET ` + strings.Join(set, \", \") + ` WHERE id = ` + placeholder(len(args)+1)\n\targs = append(args, update.ID)\n\tresult, err := d.db.ExecContext(ctx, stmt, args...)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif _, err := result.RowsAffected(); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc (d *DB) DeleteAttachment(ctx context.Context, delete *store.DeleteAttachment) error {\n\tstmt := `DELETE FROM attachment WHERE id = $1`\n\tresult, err := d.db.ExecContext(ctx, stmt, delete.ID)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif _, err := result.RowsAffected(); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "store/db/postgres/common.go",
    "content": "package postgres\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"google.golang.org/protobuf/encoding/protojson\"\n)\n\nvar (\n\tprotojsonUnmarshaler = protojson.UnmarshalOptions{\n\t\tDiscardUnknown: true,\n\t}\n)\n\nfunc placeholder(n int) string {\n\treturn \"$\" + fmt.Sprint(n)\n}\n\nfunc placeholders(n int) string {\n\tlist := []string{}\n\tfor i := 0; i < n; i++ {\n\t\tlist = append(list, placeholder(i+1))\n\t}\n\treturn strings.Join(list, \", \")\n}\n"
  },
  {
    "path": "store/db/postgres/idp.go",
    "content": "package postgres\n\nimport (\n\t\"context\"\n\t\"strings\"\n\n\tstorepb \"github.com/usememos/memos/proto/gen/store\"\n\t\"github.com/usememos/memos/store\"\n)\n\nfunc (d *DB) CreateIdentityProvider(ctx context.Context, create *store.IdentityProvider) (*store.IdentityProvider, error) {\n\tfields := []string{\"uid\", \"name\", \"type\", \"identifier_filter\", \"config\"}\n\targs := []any{create.UID, create.Name, create.Type.String(), create.IdentifierFilter, create.Config}\n\tstmt := \"INSERT INTO idp (\" + strings.Join(fields, \", \") + \") VALUES (\" + placeholders(len(args)) + \") RETURNING id\"\n\tif err := d.db.QueryRowContext(ctx, stmt, args...).Scan(&create.ID); err != nil {\n\t\treturn nil, err\n\t}\n\n\tidentityProvider := create\n\treturn identityProvider, nil\n}\n\nfunc (d *DB) ListIdentityProviders(ctx context.Context, find *store.FindIdentityProvider) ([]*store.IdentityProvider, error) {\n\twhere, args := []string{\"1 = 1\"}, []any{}\n\tif v := find.ID; v != nil {\n\t\twhere, args = append(where, \"id = \"+placeholder(len(args)+1)), append(args, *v)\n\t}\n\tif v := find.UID; v != nil {\n\t\twhere, args = append(where, \"uid = \"+placeholder(len(args)+1)), append(args, *v)\n\t}\n\n\trows, err := d.db.QueryContext(ctx, `\n\t\tSELECT\n\t\t\tid,\n\t\t\tuid,\n\t\t\tname,\n\t\t\ttype,\n\t\t\tidentifier_filter,\n\t\t\tconfig\n\t\tFROM idp\n\t\tWHERE `+strings.Join(where, \" AND \")+` ORDER BY id ASC`,\n\t\targs...,\n\t)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\n\tvar identityProviders []*store.IdentityProvider\n\tfor rows.Next() {\n\t\tvar identityProvider store.IdentityProvider\n\t\tvar typeString string\n\t\tif err := rows.Scan(\n\t\t\t&identityProvider.ID,\n\t\t\t&identityProvider.UID,\n\t\t\t&identityProvider.Name,\n\t\t\t&typeString,\n\t\t\t&identityProvider.IdentifierFilter,\n\t\t\t&identityProvider.Config,\n\t\t); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tidentityProvider.Type = storepb.IdentityProvider_Type(storepb.IdentityProvider_Type_value[typeString])\n\t\tidentityProviders = append(identityProviders, &identityProvider)\n\t}\n\n\tif err := rows.Err(); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn identityProviders, nil\n}\n\nfunc (d *DB) UpdateIdentityProvider(ctx context.Context, update *store.UpdateIdentityProvider) (*store.IdentityProvider, error) {\n\tset, args := []string{}, []any{}\n\tif v := update.Name; v != nil {\n\t\tset, args = append(set, \"name = \"+placeholder(len(args)+1)), append(args, *v)\n\t}\n\tif v := update.IdentifierFilter; v != nil {\n\t\tset, args = append(set, \"identifier_filter = \"+placeholder(len(args)+1)), append(args, *v)\n\t}\n\tif v := update.Config; v != nil {\n\t\tset, args = append(set, \"config = \"+placeholder(len(args)+1)), append(args, *v)\n\t}\n\n\tstmt := `\n\t\tUPDATE idp\n\t\tSET ` + strings.Join(set, \", \") + `\n\t\tWHERE id = ` + placeholder(len(args)+1) + `\n\t\tRETURNING id, uid, name, type, identifier_filter, config\n\t`\n\targs = append(args, update.ID)\n\n\tvar identityProvider store.IdentityProvider\n\tvar typeString string\n\tif err := d.db.QueryRowContext(ctx, stmt, args...).Scan(\n\t\t&identityProvider.ID,\n\t\t&identityProvider.UID,\n\t\t&identityProvider.Name,\n\t\t&typeString,\n\t\t&identityProvider.IdentifierFilter,\n\t\t&identityProvider.Config,\n\t); err != nil {\n\t\treturn nil, err\n\t}\n\n\tidentityProvider.Type = storepb.IdentityProvider_Type(storepb.IdentityProvider_Type_value[typeString])\n\treturn &identityProvider, nil\n}\n\nfunc (d *DB) DeleteIdentityProvider(ctx context.Context, delete *store.DeleteIdentityProvider) error {\n\twhere, args := []string{\"id = $1\"}, []any{delete.ID}\n\tstmt := `DELETE FROM idp WHERE ` + strings.Join(where, \" AND \")\n\tresult, err := d.db.ExecContext(ctx, stmt, args...)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif _, err = result.RowsAffected(); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "store/db/postgres/inbox.go",
    "content": "package postgres\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/pkg/errors\"\n\t\"google.golang.org/protobuf/encoding/protojson\"\n\n\tstorepb \"github.com/usememos/memos/proto/gen/store\"\n\t\"github.com/usememos/memos/store\"\n)\n\nfunc (d *DB) CreateInbox(ctx context.Context, create *store.Inbox) (*store.Inbox, error) {\n\tmessageString := \"{}\"\n\tif create.Message != nil {\n\t\tbytes, err := protojson.Marshal(create.Message)\n\t\tif err != nil {\n\t\t\treturn nil, errors.Wrap(err, \"failed to marshal inbox message\")\n\t\t}\n\t\tmessageString = string(bytes)\n\t}\n\n\tfields := []string{\"sender_id\", \"receiver_id\", \"status\", \"message\"}\n\targs := []any{create.SenderID, create.ReceiverID, create.Status, messageString}\n\tstmt := \"INSERT INTO inbox (\" + strings.Join(fields, \", \") + \") VALUES (\" + placeholders(len(args)) + \") RETURNING id, created_ts\"\n\tif err := d.db.QueryRowContext(ctx, stmt, args...).Scan(\n\t\t&create.ID,\n\t\t&create.CreatedTs,\n\t); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn create, nil\n}\n\nfunc (d *DB) ListInboxes(ctx context.Context, find *store.FindInbox) ([]*store.Inbox, error) {\n\twhere, args := []string{\"1 = 1\"}, []any{}\n\n\tif find.ID != nil {\n\t\twhere, args = append(where, \"id = \"+placeholder(len(args)+1)), append(args, *find.ID)\n\t}\n\tif find.SenderID != nil {\n\t\twhere, args = append(where, \"sender_id = \"+placeholder(len(args)+1)), append(args, *find.SenderID)\n\t}\n\tif find.ReceiverID != nil {\n\t\twhere, args = append(where, \"receiver_id = \"+placeholder(len(args)+1)), append(args, *find.ReceiverID)\n\t}\n\tif find.Status != nil {\n\t\twhere, args = append(where, \"status = \"+placeholder(len(args)+1)), append(args, *find.Status)\n\t}\n\tif find.MessageType != nil {\n\t\t// Filter by message type using PostgreSQL JSON extraction\n\t\t// Note: The type field in JSON is stored as string representation of the enum name\n\t\t// Cast to JSONB since the column is TEXT\n\t\tif *find.MessageType == storepb.InboxMessage_TYPE_UNSPECIFIED {\n\t\t\twhere, args = append(where, \"(message::JSONB->>'type' IS NULL OR message::JSONB->>'type' = \"+placeholder(len(args)+1)+\")\"), append(args, find.MessageType.String())\n\t\t} else {\n\t\t\twhere, args = append(where, \"message::JSONB->>'type' = \"+placeholder(len(args)+1)), append(args, find.MessageType.String())\n\t\t}\n\t}\n\n\tquery := \"SELECT id, created_ts, sender_id, receiver_id, status, message FROM inbox WHERE \" + strings.Join(where, \" AND \") + \" ORDER BY created_ts DESC\"\n\tif find.Limit != nil {\n\t\tquery = fmt.Sprintf(\"%s LIMIT %d\", query, *find.Limit)\n\t\tif find.Offset != nil {\n\t\t\tquery = fmt.Sprintf(\"%s OFFSET %d\", query, *find.Offset)\n\t\t}\n\t}\n\trows, err := d.db.QueryContext(ctx, query, args...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\n\tlist := []*store.Inbox{}\n\tfor rows.Next() {\n\t\tinbox := &store.Inbox{}\n\t\tvar messageBytes []byte\n\t\tif err := rows.Scan(\n\t\t\t&inbox.ID,\n\t\t\t&inbox.CreatedTs,\n\t\t\t&inbox.SenderID,\n\t\t\t&inbox.ReceiverID,\n\t\t\t&inbox.Status,\n\t\t\t&messageBytes,\n\t\t); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tmessage := &storepb.InboxMessage{}\n\t\tif err := protojsonUnmarshaler.Unmarshal(messageBytes, message); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tinbox.Message = message\n\t\tlist = append(list, inbox)\n\t}\n\n\tif err := rows.Err(); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn list, nil\n}\n\nfunc (d *DB) GetInbox(ctx context.Context, find *store.FindInbox) (*store.Inbox, error) {\n\tlist, err := d.ListInboxes(ctx, find)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"failed to get inbox\")\n\t}\n\tif len(list) != 1 {\n\t\treturn nil, errors.Errorf(\"unexpected inbox count: %d\", len(list))\n\t}\n\treturn list[0], nil\n}\n\nfunc (d *DB) UpdateInbox(ctx context.Context, update *store.UpdateInbox) (*store.Inbox, error) {\n\tset, args := []string{\"status = $1\"}, []any{update.Status.String()}\n\targs = append(args, update.ID)\n\tquery := \"UPDATE inbox SET \" + strings.Join(set, \", \") + \" WHERE id = $2 RETURNING id, created_ts, sender_id, receiver_id, status, message\"\n\tinbox := &store.Inbox{}\n\tvar messageBytes []byte\n\tif err := d.db.QueryRowContext(ctx, query, args...).Scan(\n\t\t&inbox.ID,\n\t\t&inbox.CreatedTs,\n\t\t&inbox.SenderID,\n\t\t&inbox.ReceiverID,\n\t\t&inbox.Status,\n\t\t&messageBytes,\n\t); err != nil {\n\t\treturn nil, err\n\t}\n\tmessage := &storepb.InboxMessage{}\n\tif err := protojsonUnmarshaler.Unmarshal(messageBytes, message); err != nil {\n\t\treturn nil, err\n\t}\n\tinbox.Message = message\n\treturn inbox, nil\n}\n\nfunc (d *DB) DeleteInbox(ctx context.Context, delete *store.DeleteInbox) error {\n\tresult, err := d.db.ExecContext(ctx, \"DELETE FROM inbox WHERE id = $1\", delete.ID)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif _, err := result.RowsAffected(); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "store/db/postgres/instance_setting.go",
    "content": "package postgres\n\nimport (\n\t\"context\"\n\t\"strings\"\n\n\t\"github.com/usememos/memos/store\"\n)\n\nfunc (d *DB) UpsertInstanceSetting(ctx context.Context, upsert *store.InstanceSetting) (*store.InstanceSetting, error) {\n\tstmt := `\n\t\tINSERT INTO system_setting (\n\t\t\tname, value, description\n\t\t)\n\t\tVALUES ($1, $2, $3)\n\t\tON CONFLICT(name) DO UPDATE\n\t\tSET\n\t\t\tvalue = EXCLUDED.value,\n\t\t\tdescription = EXCLUDED.description\n\t`\n\tif _, err := d.db.ExecContext(ctx, stmt, upsert.Name, upsert.Value, upsert.Description); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn upsert, nil\n}\n\nfunc (d *DB) ListInstanceSettings(ctx context.Context, find *store.FindInstanceSetting) ([]*store.InstanceSetting, error) {\n\twhere, args := []string{\"1 = 1\"}, []any{}\n\tif find.Name != \"\" {\n\t\twhere, args = append(where, \"name = \"+placeholder(len(args)+1)), append(args, find.Name)\n\t}\n\n\tquery := `\n\t\tSELECT\n\t\t\tname,\n\t\t\tvalue,\n\t\t\tdescription\n\t\tFROM system_setting\n\t\tWHERE ` + strings.Join(where, \" AND \")\n\n\trows, err := d.db.QueryContext(ctx, query, args...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\n\tlist := []*store.InstanceSetting{}\n\tfor rows.Next() {\n\t\tsystemSettingMessage := &store.InstanceSetting{}\n\t\tif err := rows.Scan(\n\t\t\t&systemSettingMessage.Name,\n\t\t\t&systemSettingMessage.Value,\n\t\t\t&systemSettingMessage.Description,\n\t\t); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tlist = append(list, systemSettingMessage)\n\t}\n\n\tif err := rows.Err(); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn list, nil\n}\n\nfunc (d *DB) DeleteInstanceSetting(ctx context.Context, delete *store.DeleteInstanceSetting) error {\n\tstmt := `DELETE FROM system_setting WHERE name = $1`\n\t_, err := d.db.ExecContext(ctx, stmt, delete.Name)\n\treturn err\n}\n"
  },
  {
    "path": "store/db/postgres/memo.go",
    "content": "package postgres\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/pkg/errors\"\n\t\"google.golang.org/protobuf/encoding/protojson\"\n\n\t\"github.com/usememos/memos/plugin/filter\"\n\tstorepb \"github.com/usememos/memos/proto/gen/store\"\n\t\"github.com/usememos/memos/store\"\n)\n\nfunc (d *DB) CreateMemo(ctx context.Context, create *store.Memo) (*store.Memo, error) {\n\tfields := []string{\"uid\", \"creator_id\", \"content\", \"visibility\", \"payload\"}\n\tpayload := \"{}\"\n\tif create.Payload != nil {\n\t\tpayloadBytes, err := protojson.Marshal(create.Payload)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tpayload = string(payloadBytes)\n\t}\n\targs := []any{create.UID, create.CreatorID, create.Content, create.Visibility, payload}\n\n\t// Add custom timestamps if provided\n\tif create.CreatedTs != 0 {\n\t\tfields = append(fields, \"created_ts\")\n\t\targs = append(args, create.CreatedTs)\n\t}\n\tif create.UpdatedTs != 0 {\n\t\tfields = append(fields, \"updated_ts\")\n\t\targs = append(args, create.UpdatedTs)\n\t}\n\n\tstmt := \"INSERT INTO memo (\" + strings.Join(fields, \", \") + \") VALUES (\" + placeholders(len(args)) + \") RETURNING id, created_ts, updated_ts, row_status\"\n\tif err := d.db.QueryRowContext(ctx, stmt, args...).Scan(\n\t\t&create.ID,\n\t\t&create.CreatedTs,\n\t\t&create.UpdatedTs,\n\t\t&create.RowStatus,\n\t); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn create, nil\n}\n\nfunc (d *DB) ListMemos(ctx context.Context, find *store.FindMemo) ([]*store.Memo, error) {\n\twhere, args := []string{\"1 = 1\"}, []any{}\n\n\tengine, err := filter.DefaultEngine()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif err := filter.AppendConditions(ctx, engine, find.Filters, filter.DialectPostgres, &where, &args); err != nil {\n\t\treturn nil, err\n\t}\n\tif v := find.ID; v != nil {\n\t\twhere, args = append(where, \"memo.id = \"+placeholder(len(args)+1)), append(args, *v)\n\t}\n\tif len(find.IDList) > 0 {\n\t\tholders := make([]string, 0, len(find.IDList))\n\t\tfor _, id := range find.IDList {\n\t\t\tholders = append(holders, placeholder(len(args)+1))\n\t\t\targs = append(args, id)\n\t\t}\n\t\twhere = append(where, \"memo.id IN (\"+strings.Join(holders, \", \")+\")\")\n\t}\n\tif v := find.UID; v != nil {\n\t\twhere, args = append(where, \"memo.uid = \"+placeholder(len(args)+1)), append(args, *v)\n\t}\n\tif len(find.UIDList) > 0 {\n\t\tholders := make([]string, 0, len(find.UIDList))\n\t\tfor _, uid := range find.UIDList {\n\t\t\tholders = append(holders, placeholder(len(args)+1))\n\t\t\targs = append(args, uid)\n\t\t}\n\t\twhere = append(where, \"memo.uid IN (\"+strings.Join(holders, \", \")+\")\")\n\t}\n\tif v := find.CreatorID; v != nil {\n\t\twhere, args = append(where, \"memo.creator_id = \"+placeholder(len(args)+1)), append(args, *v)\n\t}\n\tif v := find.RowStatus; v != nil {\n\t\twhere, args = append(where, \"memo.row_status = \"+placeholder(len(args)+1)), append(args, *v)\n\t}\n\tif v := find.VisibilityList; len(v) != 0 {\n\t\tholders := []string{}\n\t\tfor _, visibility := range v {\n\t\t\tholders = append(holders, placeholder(len(args)+1))\n\t\t\targs = append(args, visibility.String())\n\t\t}\n\t\twhere = append(where, fmt.Sprintf(\"memo.visibility in (%s)\", strings.Join(holders, \", \")))\n\t}\n\tif find.ExcludeComments {\n\t\twhere = append(where, \"memo_relation.related_memo_id IS NULL\")\n\t}\n\n\torder := \"DESC\"\n\tif find.OrderByTimeAsc {\n\t\torder = \"ASC\"\n\t}\n\torderBy := []string{}\n\tif find.OrderByPinned {\n\t\torderBy = append(orderBy, \"pinned DESC\")\n\t}\n\tif find.OrderByUpdatedTs {\n\t\torderBy = append(orderBy, \"updated_ts \"+order)\n\t} else {\n\t\torderBy = append(orderBy, \"created_ts \"+order)\n\t}\n\t// Add id as final tie-breaker\n\torderBy = append(orderBy, \"id DESC\")\n\tfields := []string{\n\t\t`memo.id AS id`,\n\t\t`memo.uid AS uid`,\n\t\t`memo.creator_id AS creator_id`,\n\t\t`memo.created_ts AS created_ts`,\n\t\t`memo.updated_ts AS updated_ts`,\n\t\t`memo.row_status AS row_status`,\n\t\t`memo.visibility AS visibility`,\n\t\t`memo.pinned AS pinned`,\n\t\t`memo.payload AS payload`,\n\t\t`CASE WHEN parent_memo.uid IS NOT NULL THEN parent_memo.uid ELSE NULL END AS parent_uid`,\n\t}\n\tif !find.ExcludeContent {\n\t\tfields = append(fields, `memo.content AS content`)\n\t}\n\n\tquery := `SELECT ` + strings.Join(fields, \", \") + `\n\t\tFROM memo\n\t\tLEFT JOIN memo_relation ON memo.id = memo_relation.memo_id AND memo_relation.type = 'COMMENT'\n\t\tLEFT JOIN memo AS parent_memo ON memo_relation.related_memo_id = parent_memo.id\n\t\tWHERE ` + strings.Join(where, \" AND \") + `\n\t\tORDER BY ` + strings.Join(orderBy, \", \")\n\tif find.Limit != nil {\n\t\tquery = fmt.Sprintf(\"%s LIMIT %d\", query, *find.Limit)\n\t\tif find.Offset != nil {\n\t\t\tquery = fmt.Sprintf(\"%s OFFSET %d\", query, *find.Offset)\n\t\t}\n\t}\n\n\trows, err := d.db.QueryContext(ctx, query, args...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\n\tlist := make([]*store.Memo, 0)\n\tfor rows.Next() {\n\t\tvar memo store.Memo\n\t\tvar payloadBytes []byte\n\t\tdests := []any{\n\t\t\t&memo.ID,\n\t\t\t&memo.UID,\n\t\t\t&memo.CreatorID,\n\t\t\t&memo.CreatedTs,\n\t\t\t&memo.UpdatedTs,\n\t\t\t&memo.RowStatus,\n\t\t\t&memo.Visibility,\n\t\t\t&memo.Pinned,\n\t\t\t&payloadBytes,\n\t\t\t&memo.ParentUID,\n\t\t}\n\t\tif !find.ExcludeContent {\n\t\t\tdests = append(dests, &memo.Content)\n\t\t}\n\t\tif err := rows.Scan(dests...); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tpayload := &storepb.MemoPayload{}\n\t\tif err := protojsonUnmarshaler.Unmarshal(payloadBytes, payload); err != nil {\n\t\t\treturn nil, errors.Wrap(err, \"failed to unmarshal payload\")\n\t\t}\n\t\tmemo.Payload = payload\n\t\tlist = append(list, &memo)\n\t}\n\n\tif err := rows.Err(); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn list, nil\n}\n\nfunc (d *DB) GetMemo(ctx context.Context, find *store.FindMemo) (*store.Memo, error) {\n\tlist, err := d.ListMemos(ctx, find)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif len(list) == 0 {\n\t\treturn nil, nil\n\t}\n\n\tmemo := list[0]\n\treturn memo, nil\n}\n\nfunc (d *DB) UpdateMemo(ctx context.Context, update *store.UpdateMemo) error {\n\tset, args := []string{}, []any{}\n\tif v := update.UID; v != nil {\n\t\tset, args = append(set, \"uid = \"+placeholder(len(args)+1)), append(args, *v)\n\t}\n\tif v := update.CreatedTs; v != nil {\n\t\tset, args = append(set, \"created_ts = \"+placeholder(len(args)+1)), append(args, *v)\n\t}\n\tif v := update.UpdatedTs; v != nil {\n\t\tset, args = append(set, \"updated_ts = \"+placeholder(len(args)+1)), append(args, *v)\n\t}\n\tif v := update.RowStatus; v != nil {\n\t\tset, args = append(set, \"row_status = \"+placeholder(len(args)+1)), append(args, *v)\n\t}\n\tif v := update.Content; v != nil {\n\t\tset, args = append(set, \"content = \"+placeholder(len(args)+1)), append(args, *v)\n\t}\n\tif v := update.Visibility; v != nil {\n\t\tset, args = append(set, \"visibility = \"+placeholder(len(args)+1)), append(args, *v)\n\t}\n\tif v := update.Pinned; v != nil {\n\t\tset, args = append(set, \"pinned = \"+placeholder(len(args)+1)), append(args, *v)\n\t}\n\tif v := update.Payload; v != nil {\n\t\tpayloadBytes, err := protojson.Marshal(v)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tset, args = append(set, \"payload = \"+placeholder(len(args)+1)), append(args, string(payloadBytes))\n\t}\n\tif len(set) == 0 {\n\t\treturn nil\n\t}\n\n\tstmt := `UPDATE memo SET ` + strings.Join(set, \", \") + ` WHERE id = ` + placeholder(len(args)+1)\n\targs = append(args, update.ID)\n\tif _, err := d.db.ExecContext(ctx, stmt, args...); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc (d *DB) DeleteMemo(ctx context.Context, delete *store.DeleteMemo) error {\n\twhere, args := []string{\"id = \" + placeholder(1)}, []any{delete.ID}\n\tstmt := `DELETE FROM memo WHERE ` + strings.Join(where, \" AND \")\n\tresult, err := d.db.ExecContext(ctx, stmt, args...)\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"failed to delete memo\")\n\t}\n\tif _, err := result.RowsAffected(); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "store/db/postgres/memo_relation.go",
    "content": "package postgres\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/usememos/memos/plugin/filter\"\n\t\"github.com/usememos/memos/store\"\n)\n\nfunc (d *DB) UpsertMemoRelation(ctx context.Context, create *store.MemoRelation) (*store.MemoRelation, error) {\n\tstmt := `\n\t\tINSERT INTO memo_relation (\n\t\t\tmemo_id,\n\t\t\trelated_memo_id,\n\t\t\ttype\n\t\t)\n\t\tVALUES (` + placeholders(3) + `)\n\t\tON CONFLICT (memo_id, related_memo_id, type) DO UPDATE SET type = EXCLUDED.type\n\t\tRETURNING memo_id, related_memo_id, type\n\t`\n\tmemoRelation := &store.MemoRelation{}\n\tif err := d.db.QueryRowContext(\n\t\tctx,\n\t\tstmt,\n\t\tcreate.MemoID,\n\t\tcreate.RelatedMemoID,\n\t\tcreate.Type,\n\t).Scan(\n\t\t&memoRelation.MemoID,\n\t\t&memoRelation.RelatedMemoID,\n\t\t&memoRelation.Type,\n\t); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn memoRelation, nil\n}\n\nfunc (d *DB) ListMemoRelations(ctx context.Context, find *store.FindMemoRelation) ([]*store.MemoRelation, error) {\n\twhere, args := []string{\"1 = 1\"}, []any{}\n\tif find.MemoID != nil {\n\t\twhere, args = append(where, \"memo_id = \"+placeholder(len(args)+1)), append(args, find.MemoID)\n\t}\n\tif find.RelatedMemoID != nil {\n\t\twhere, args = append(where, \"related_memo_id = \"+placeholder(len(args)+1)), append(args, find.RelatedMemoID)\n\t}\n\tif find.Type != nil {\n\t\twhere, args = append(where, \"type = \"+placeholder(len(args)+1)), append(args, find.Type)\n\t}\n\tif len(find.MemoIDList) > 0 {\n\t\tmemoPlaceholders := make([]string, len(find.MemoIDList))\n\t\tfor i, id := range find.MemoIDList {\n\t\t\tmemoPlaceholders[i] = placeholder(len(args) + 1)\n\t\t\targs = append(args, id)\n\t\t}\n\t\trelatedPlaceholders := make([]string, len(find.MemoIDList))\n\t\tfor i, id := range find.MemoIDList {\n\t\t\trelatedPlaceholders[i] = placeholder(len(args) + 1)\n\t\t\targs = append(args, id)\n\t\t}\n\t\twhere = append(where, fmt.Sprintf(\"(memo_id IN (%s) OR related_memo_id IN (%s))\",\n\t\t\tstrings.Join(memoPlaceholders, \", \"), strings.Join(relatedPlaceholders, \", \")))\n\t}\n\tif find.MemoFilter != nil {\n\t\tengine, err := filter.DefaultEngine()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tstmt, err := engine.CompileToStatement(ctx, *find.MemoFilter, filter.RenderOptions{\n\t\t\tDialect:           filter.DialectPostgres,\n\t\t\tPlaceholderOffset: len(args),\n\t\t})\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif stmt.SQL != \"\" {\n\t\t\twhere = append(where, fmt.Sprintf(\"memo_id IN (SELECT id FROM memo WHERE %s)\", stmt.SQL))\n\t\t\targs = append(args, stmt.Args...)\n\n\t\t\tstmtRelated, err := engine.CompileToStatement(ctx, *find.MemoFilter, filter.RenderOptions{\n\t\t\t\tDialect:           filter.DialectPostgres,\n\t\t\t\tPlaceholderOffset: len(args),\n\t\t\t})\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tif stmtRelated.SQL != \"\" {\n\t\t\t\twhere = append(where, fmt.Sprintf(\"related_memo_id IN (SELECT id FROM memo WHERE %s)\", stmtRelated.SQL))\n\t\t\t\targs = append(args, stmtRelated.Args...)\n\t\t\t}\n\t\t}\n\t}\n\n\trows, err := d.db.QueryContext(ctx, `\n\t\tSELECT\n\t\t\tmemo_id,\n\t\t\trelated_memo_id,\n\t\t\ttype\n\t\tFROM memo_relation\n\t\tWHERE `+strings.Join(where, \" AND \"), args...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\n\tlist := []*store.MemoRelation{}\n\tfor rows.Next() {\n\t\tmemoRelation := &store.MemoRelation{}\n\t\tif err := rows.Scan(\n\t\t\t&memoRelation.MemoID,\n\t\t\t&memoRelation.RelatedMemoID,\n\t\t\t&memoRelation.Type,\n\t\t); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tlist = append(list, memoRelation)\n\t}\n\n\tif err := rows.Err(); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn list, nil\n}\n\nfunc (d *DB) DeleteMemoRelation(ctx context.Context, delete *store.DeleteMemoRelation) error {\n\twhere, args := []string{\"1 = 1\"}, []any{}\n\tif delete.MemoID != nil {\n\t\twhere, args = append(where, \"memo_id = \"+placeholder(len(args)+1)), append(args, delete.MemoID)\n\t}\n\tif delete.RelatedMemoID != nil {\n\t\twhere, args = append(where, \"related_memo_id = \"+placeholder(len(args)+1)), append(args, delete.RelatedMemoID)\n\t}\n\tif delete.Type != nil {\n\t\twhere, args = append(where, \"type = \"+placeholder(len(args)+1)), append(args, delete.Type)\n\t}\n\tstmt := `DELETE FROM memo_relation WHERE ` + strings.Join(where, \" AND \")\n\tresult, err := d.db.ExecContext(ctx, stmt, args...)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif _, err = result.RowsAffected(); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "store/db/postgres/memo_share.go",
    "content": "package postgres\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"errors\"\n\t\"strings\"\n\n\t\"github.com/usememos/memos/store\"\n)\n\nfunc (d *DB) CreateMemoShare(ctx context.Context, create *store.MemoShare) (*store.MemoShare, error) {\n\tfields := []string{\"uid\", \"memo_id\", \"creator_id\"}\n\targs := []any{create.UID, create.MemoID, create.CreatorID}\n\n\tif create.ExpiresTs != nil {\n\t\tfields = append(fields, \"expires_ts\")\n\t\targs = append(args, *create.ExpiresTs)\n\t}\n\n\tstmt := \"INSERT INTO memo_share (\" + strings.Join(fields, \", \") + \") VALUES (\" + placeholders(len(args)) + \") RETURNING id, created_ts\"\n\tif err := d.db.QueryRowContext(ctx, stmt, args...).Scan(\n\t\t&create.ID,\n\t\t&create.CreatedTs,\n\t); err != nil {\n\t\treturn nil, err\n\t}\n\treturn create, nil\n}\n\nfunc (d *DB) ListMemoShares(ctx context.Context, find *store.FindMemoShare) ([]*store.MemoShare, error) {\n\twhere, args := []string{\"1 = 1\"}, []any{}\n\n\tif find.ID != nil {\n\t\twhere, args = append(where, \"id = \"+placeholder(len(args)+1)), append(args, *find.ID)\n\t}\n\tif find.UID != nil {\n\t\twhere, args = append(where, \"uid = \"+placeholder(len(args)+1)), append(args, *find.UID)\n\t}\n\tif find.MemoID != nil {\n\t\twhere, args = append(where, \"memo_id = \"+placeholder(len(args)+1)), append(args, *find.MemoID)\n\t}\n\n\trows, err := d.db.QueryContext(ctx, `\n\t\tSELECT\n\t\t\tid,\n\t\t\tuid,\n\t\t\tmemo_id,\n\t\t\tcreator_id,\n\t\t\tcreated_ts,\n\t\t\texpires_ts\n\t\tFROM memo_share\n\t\tWHERE `+strings.Join(where, \" AND \")+`\n\t\tORDER BY id ASC`,\n\t\targs...,\n\t)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\n\tlist := []*store.MemoShare{}\n\tfor rows.Next() {\n\t\tms := &store.MemoShare{}\n\t\tif err := rows.Scan(\n\t\t\t&ms.ID,\n\t\t\t&ms.UID,\n\t\t\t&ms.MemoID,\n\t\t\t&ms.CreatorID,\n\t\t\t&ms.CreatedTs,\n\t\t\t&ms.ExpiresTs,\n\t\t); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tlist = append(list, ms)\n\t}\n\n\tif err := rows.Err(); err != nil {\n\t\treturn nil, err\n\t}\n\treturn list, nil\n}\n\nfunc (d *DB) GetMemoShare(ctx context.Context, find *store.FindMemoShare) (*store.MemoShare, error) {\n\twhere, args := []string{\"1 = 1\"}, []any{}\n\n\tif find.ID != nil {\n\t\twhere, args = append(where, \"id = \"+placeholder(len(args)+1)), append(args, *find.ID)\n\t}\n\tif find.UID != nil {\n\t\twhere, args = append(where, \"uid = \"+placeholder(len(args)+1)), append(args, *find.UID)\n\t}\n\tif find.MemoID != nil {\n\t\twhere, args = append(where, \"memo_id = \"+placeholder(len(args)+1)), append(args, *find.MemoID)\n\t}\n\n\tms := &store.MemoShare{}\n\tif err := d.db.QueryRowContext(ctx, `\n\t\tSELECT\n\t\t\tid,\n\t\t\tuid,\n\t\t\tmemo_id,\n\t\t\tcreator_id,\n\t\t\tcreated_ts,\n\t\t\texpires_ts\n\t\tFROM memo_share\n\t\tWHERE `+strings.Join(where, \" AND \")+`\n\t\tLIMIT 1`,\n\t\targs...,\n\t).Scan(\n\t\t&ms.ID,\n\t\t&ms.UID,\n\t\t&ms.MemoID,\n\t\t&ms.CreatorID,\n\t\t&ms.CreatedTs,\n\t\t&ms.ExpiresTs,\n\t); err != nil {\n\t\tif errors.Is(err, sql.ErrNoRows) {\n\t\t\treturn nil, nil\n\t\t}\n\t\treturn nil, err\n\t}\n\treturn ms, nil\n}\n\nfunc (d *DB) DeleteMemoShare(ctx context.Context, delete *store.DeleteMemoShare) error {\n\twhere, args := []string{\"1 = 1\"}, []any{}\n\tif delete.ID != nil {\n\t\twhere, args = append(where, \"id = \"+placeholder(len(args)+1)), append(args, *delete.ID)\n\t}\n\tif delete.UID != nil {\n\t\twhere, args = append(where, \"uid = \"+placeholder(len(args)+1)), append(args, *delete.UID)\n\t}\n\t_, err := d.db.ExecContext(ctx, \"DELETE FROM memo_share WHERE \"+strings.Join(where, \" AND \"), args...)\n\treturn err\n}\n"
  },
  {
    "path": "store/db/postgres/postgres.go",
    "content": "package postgres\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"log\"\n\n\t// Import the PostgreSQL driver.\n\t_ \"github.com/lib/pq\"\n\t\"github.com/pkg/errors\"\n\n\t\"github.com/usememos/memos/internal/profile\"\n\t\"github.com/usememos/memos/store\"\n)\n\ntype DB struct {\n\tdb      *sql.DB\n\tprofile *profile.Profile\n}\n\nfunc NewDB(profile *profile.Profile) (store.Driver, error) {\n\tif profile == nil {\n\t\treturn nil, errors.New(\"profile is nil\")\n\t}\n\n\t// Open the PostgreSQL connection\n\tdb, err := sql.Open(\"postgres\", profile.DSN)\n\tif err != nil {\n\t\tlog.Printf(\"Failed to open database: %s\", err)\n\t\treturn nil, errors.Wrapf(err, \"failed to open database: %s\", profile.DSN)\n\t}\n\n\tvar driver store.Driver = &DB{\n\t\tdb:      db,\n\t\tprofile: profile,\n\t}\n\n\t// Return the DB struct\n\treturn driver, nil\n}\n\nfunc (d *DB) GetDB() *sql.DB {\n\treturn d.db\n}\n\nfunc (d *DB) Close() error {\n\treturn d.db.Close()\n}\n\nfunc (d *DB) IsInitialized(ctx context.Context) (bool, error) {\n\tvar exists bool\n\terr := d.db.QueryRowContext(ctx, \"SELECT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_catalog = current_database() AND table_name = 'memo' AND table_type = 'BASE TABLE')\").Scan(&exists)\n\tif err != nil {\n\t\treturn false, errors.Wrap(err, \"failed to check if database is initialized\")\n\t}\n\treturn exists, nil\n}\n"
  },
  {
    "path": "store/db/postgres/reaction.go",
    "content": "package postgres\n\nimport (\n\t\"context\"\n\t\"strings\"\n\n\t\"github.com/usememos/memos/store\"\n)\n\nfunc (d *DB) UpsertReaction(ctx context.Context, upsert *store.Reaction) (*store.Reaction, error) {\n\tfields := []string{\"creator_id\", \"content_id\", \"reaction_type\"}\n\targs := []interface{}{upsert.CreatorID, upsert.ContentID, upsert.ReactionType}\n\tstmt := \"INSERT INTO reaction (\" + strings.Join(fields, \", \") + \") VALUES (\" + placeholders(len(args)) + \") RETURNING id, created_ts\"\n\tif err := d.db.QueryRowContext(ctx, stmt, args...).Scan(\n\t\t&upsert.ID,\n\t\t&upsert.CreatedTs,\n\t); err != nil {\n\t\treturn nil, err\n\t}\n\n\treaction := upsert\n\treturn reaction, nil\n}\n\nfunc (d *DB) ListReactions(ctx context.Context, find *store.FindReaction) ([]*store.Reaction, error) {\n\twhere, args := []string{\"1 = 1\"}, []any{}\n\n\tif find.ID != nil {\n\t\twhere, args = append(where, \"id = \"+placeholder(len(args)+1)), append(args, *find.ID)\n\t}\n\tif find.CreatorID != nil {\n\t\twhere, args = append(where, \"creator_id = \"+placeholder(len(args)+1)), append(args, *find.CreatorID)\n\t}\n\tif find.ContentID != nil {\n\t\twhere, args = append(where, \"content_id = \"+placeholder(len(args)+1)), append(args, *find.ContentID)\n\t}\n\tif len(find.ContentIDList) > 0 {\n\t\tholders := make([]string, 0, len(find.ContentIDList))\n\t\tfor _, id := range find.ContentIDList {\n\t\t\tholders = append(holders, placeholder(len(args)+1))\n\t\t\targs = append(args, id)\n\t\t}\n\t\twhere = append(where, \"content_id IN (\"+strings.Join(holders, \", \")+\")\")\n\t}\n\n\trows, err := d.db.QueryContext(ctx, `\n\t\tSELECT\n\t\t\tid,\n\t\t\tcreated_ts,\n\t\t\tcreator_id,\n\t\t\tcontent_id,\n\t\t\treaction_type\n\t\tFROM reaction\n\t\tWHERE `+strings.Join(where, \" AND \")+`\n\t\tORDER BY id ASC`,\n\t\targs...,\n\t)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\n\tlist := []*store.Reaction{}\n\tfor rows.Next() {\n\t\treaction := &store.Reaction{}\n\t\tif err := rows.Scan(\n\t\t\t&reaction.ID,\n\t\t\t&reaction.CreatedTs,\n\t\t\t&reaction.CreatorID,\n\t\t\t&reaction.ContentID,\n\t\t\t&reaction.ReactionType,\n\t\t); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tlist = append(list, reaction)\n\t}\n\n\tif err := rows.Err(); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn list, nil\n}\n\nfunc (d *DB) GetReaction(ctx context.Context, find *store.FindReaction) (*store.Reaction, error) {\n\tlist, err := d.ListReactions(ctx, find)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif len(list) == 0 {\n\t\treturn nil, nil\n\t}\n\n\treaction := list[0]\n\treturn reaction, nil\n}\n\nfunc (d *DB) DeleteReaction(ctx context.Context, delete *store.DeleteReaction) error {\n\t_, err := d.db.ExecContext(ctx, \"DELETE FROM reaction WHERE id = $1\", delete.ID)\n\treturn err\n}\n"
  },
  {
    "path": "store/db/postgres/user.go",
    "content": "package postgres\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/pkg/errors\"\n\n\t\"github.com/usememos/memos/store\"\n)\n\nfunc (d *DB) CreateUser(ctx context.Context, create *store.User) (*store.User, error) {\n\tfields := []string{\"username\", \"role\", \"email\", \"nickname\", \"password_hash\", \"avatar_url\"}\n\targs := []any{create.Username, create.Role, create.Email, create.Nickname, create.PasswordHash, create.AvatarURL}\n\tstmt := \"INSERT INTO \\\"user\\\" (\" + strings.Join(fields, \", \") + \") VALUES (\" + placeholders(len(args)) + \") RETURNING id, description, created_ts, updated_ts, row_status\"\n\tif err := d.db.QueryRowContext(ctx, stmt, args...).Scan(\n\t\t&create.ID,\n\t\t&create.Description,\n\t\t&create.CreatedTs,\n\t\t&create.UpdatedTs,\n\t\t&create.RowStatus,\n\t); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn create, nil\n}\n\nfunc (d *DB) UpdateUser(ctx context.Context, update *store.UpdateUser) (*store.User, error) {\n\tset, args := []string{}, []any{}\n\tif v := update.UpdatedTs; v != nil {\n\t\tset, args = append(set, \"updated_ts = \"+placeholder(len(args)+1)), append(args, *v)\n\t}\n\tif v := update.RowStatus; v != nil {\n\t\tset, args = append(set, \"row_status = \"+placeholder(len(args)+1)), append(args, *v)\n\t}\n\tif v := update.Username; v != nil {\n\t\tset, args = append(set, \"username = \"+placeholder(len(args)+1)), append(args, *v)\n\t}\n\tif v := update.Email; v != nil {\n\t\tset, args = append(set, \"email = \"+placeholder(len(args)+1)), append(args, *v)\n\t}\n\tif v := update.Nickname; v != nil {\n\t\tset, args = append(set, \"nickname = \"+placeholder(len(args)+1)), append(args, *v)\n\t}\n\tif v := update.AvatarURL; v != nil {\n\t\tset, args = append(set, \"avatar_url = \"+placeholder(len(args)+1)), append(args, *v)\n\t}\n\tif v := update.PasswordHash; v != nil {\n\t\tset, args = append(set, \"password_hash = \"+placeholder(len(args)+1)), append(args, *v)\n\t}\n\tif v := update.Description; v != nil {\n\t\tset, args = append(set, \"description = \"+placeholder(len(args)+1)), append(args, *v)\n\t}\n\tif v := update.Role; v != nil {\n\t\tset, args = append(set, \"role = \"+placeholder(len(args)+1)), append(args, *v)\n\t}\n\n\tquery := `\n\t\tUPDATE \"user\"\n\t\tSET ` + strings.Join(set, \", \") + `\n\t\tWHERE id = ` + placeholder(len(args)+1) + `\n\t\tRETURNING id, username, role, email, nickname, password_hash, avatar_url, description, created_ts, updated_ts, row_status\n\t`\n\targs = append(args, update.ID)\n\tuser := &store.User{}\n\tif err := d.db.QueryRowContext(ctx, query, args...).Scan(\n\t\t&user.ID,\n\t\t&user.Username,\n\t\t&user.Role,\n\t\t&user.Email,\n\t\t&user.Nickname,\n\t\t&user.PasswordHash,\n\t\t&user.AvatarURL,\n\t\t&user.Description,\n\t\t&user.CreatedTs,\n\t\t&user.UpdatedTs,\n\t\t&user.RowStatus,\n\t); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn user, nil\n}\n\nfunc (d *DB) ListUsers(ctx context.Context, find *store.FindUser) ([]*store.User, error) {\n\twhere, args := []string{\"1 = 1\"}, []any{}\n\n\tif len(find.Filters) > 0 {\n\t\treturn nil, errors.Errorf(\"user filters are not supported\")\n\t}\n\n\tif v := find.ID; v != nil {\n\t\twhere, args = append(where, \"id = \"+placeholder(len(args)+1)), append(args, *v)\n\t}\n\tif v := find.Username; v != nil {\n\t\twhere, args = append(where, \"username = \"+placeholder(len(args)+1)), append(args, *v)\n\t}\n\tif v := find.Role; v != nil {\n\t\twhere, args = append(where, \"role = \"+placeholder(len(args)+1)), append(args, *v)\n\t}\n\tif v := find.Email; v != nil {\n\t\twhere, args = append(where, \"email = \"+placeholder(len(args)+1)), append(args, *v)\n\t}\n\tif v := find.Nickname; v != nil {\n\t\twhere, args = append(where, \"nickname = \"+placeholder(len(args)+1)), append(args, *v)\n\t}\n\n\torderBy := []string{\"created_ts DESC\", \"row_status DESC\"}\n\tquery := `\n\t\tSELECT \n\t\t\tid,\n\t\t\tusername,\n\t\t\trole,\n\t\t\temail,\n\t\t\tnickname,\n\t\t\tpassword_hash,\n\t\t\tavatar_url,\n\t\t\tdescription,\n\t\t\tcreated_ts,\n\t\t\tupdated_ts,\n\t\t\trow_status\n\t\tFROM \"user\"\n\t\tWHERE ` + strings.Join(where, \" AND \") + ` ORDER BY ` + strings.Join(orderBy, \", \")\n\tif v := find.Limit; v != nil {\n\t\tquery += fmt.Sprintf(\" LIMIT %d\", *v)\n\t}\n\trows, err := d.db.QueryContext(ctx, query, args...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\n\tlist := make([]*store.User, 0)\n\tfor rows.Next() {\n\t\tvar user store.User\n\t\tif err := rows.Scan(\n\t\t\t&user.ID,\n\t\t\t&user.Username,\n\t\t\t&user.Role,\n\t\t\t&user.Email,\n\t\t\t&user.Nickname,\n\t\t\t&user.PasswordHash,\n\t\t\t&user.AvatarURL,\n\t\t\t&user.Description,\n\t\t\t&user.CreatedTs,\n\t\t\t&user.UpdatedTs,\n\t\t\t&user.RowStatus,\n\t\t); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tlist = append(list, &user)\n\t}\n\n\tif err := rows.Err(); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn list, nil\n}\n\nfunc (d *DB) DeleteUser(ctx context.Context, delete *store.DeleteUser) error {\n\tresult, err := d.db.ExecContext(ctx, `DELETE FROM \"user\" WHERE id = $1`, delete.ID)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif _, err := result.RowsAffected(); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "store/db/postgres/user_setting.go",
    "content": "package postgres\n\nimport (\n\t\"context\"\n\t\"strings\"\n\n\t\"github.com/pkg/errors\"\n\n\tstorepb \"github.com/usememos/memos/proto/gen/store\"\n\t\"github.com/usememos/memos/store\"\n)\n\nfunc (d *DB) UpsertUserSetting(ctx context.Context, upsert *store.UserSetting) (*store.UserSetting, error) {\n\tstmt := `\n\t\tINSERT INTO user_setting (\n\t\t\tuser_id, key, value\n\t\t)\n\t\tVALUES ($1, $2, $3)\n\t\tON CONFLICT(user_id, key) DO UPDATE \n\t\tSET value = EXCLUDED.value\n\t`\n\tif _, err := d.db.ExecContext(ctx, stmt, upsert.UserID, upsert.Key.String(), upsert.Value); err != nil {\n\t\treturn nil, err\n\t}\n\treturn upsert, nil\n}\n\nfunc (d *DB) ListUserSettings(ctx context.Context, find *store.FindUserSetting) ([]*store.UserSetting, error) {\n\twhere, args := []string{\"1 = 1\"}, []any{}\n\n\tif v := find.Key; v != storepb.UserSetting_KEY_UNSPECIFIED {\n\t\twhere, args = append(where, \"key = \"+placeholder(len(args)+1)), append(args, v.String())\n\t}\n\tif v := find.UserID; v != nil {\n\t\twhere, args = append(where, \"user_id = \"+placeholder(len(args)+1)), append(args, *find.UserID)\n\t}\n\n\tquery := `\n\t\tSELECT\n\t\t\tuser_id,\n\t\t  key,\n\t\t\tvalue\n\t\tFROM user_setting\n\t\tWHERE ` + strings.Join(where, \" AND \")\n\trows, err := d.db.QueryContext(ctx, query, args...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\n\tuserSettingList := make([]*store.UserSetting, 0)\n\tfor rows.Next() {\n\t\tuserSetting := &store.UserSetting{}\n\t\tvar keyString string\n\t\tif err := rows.Scan(\n\t\t\t&userSetting.UserID,\n\t\t\t&keyString,\n\t\t\t&userSetting.Value,\n\t\t); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tuserSetting.Key = storepb.UserSetting_Key(storepb.UserSetting_Key_value[keyString])\n\t\tuserSettingList = append(userSettingList, userSetting)\n\t}\n\n\tif err := rows.Err(); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn userSettingList, nil\n}\n\nfunc (d *DB) GetUserByPATHash(ctx context.Context, tokenHash string) (*store.PATQueryResult, error) {\n\t// Simplified query: fetch all PERSONAL_ACCESS_TOKENS rows and search in Go\n\t// This matches SQLite/MySQL behavior and avoids PostgreSQL's strict JSONB errors\n\tquery := `\n\t\tSELECT\n\t\t\tuser_id,\n\t\t\tvalue\n\t\tFROM user_setting\n\t\tWHERE key = 'PERSONAL_ACCESS_TOKENS'\n\t`\n\n\trows, err := d.db.QueryContext(ctx, query)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\n\t// Iterate through all users with PAT settings\n\tfor rows.Next() {\n\t\tvar userID int32\n\t\tvar tokensJSON string\n\n\t\tif err := rows.Scan(&userID, &tokensJSON); err != nil {\n\t\t\tcontinue // Skip malformed rows\n\t\t}\n\n\t\t// Try to unmarshal - skip if invalid JSON\n\t\tpatsUserSetting := &storepb.PersonalAccessTokensUserSetting{}\n\t\tif err := protojsonUnmarshaler.Unmarshal([]byte(tokensJSON), patsUserSetting); err != nil {\n\t\t\tcontinue // Skip invalid JSON\n\t\t}\n\n\t\t// Search for matching token hash\n\t\tfor _, pat := range patsUserSetting.Tokens {\n\t\t\tif pat.TokenHash == tokenHash {\n\t\t\t\treturn &store.PATQueryResult{\n\t\t\t\t\tUserID: userID,\n\t\t\t\t\tPAT:    pat,\n\t\t\t\t}, nil\n\t\t\t}\n\t\t}\n\t}\n\n\tif err := rows.Err(); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn nil, errors.New(\"PAT not found\")\n}\n"
  },
  {
    "path": "store/db/postgres/user_setting_test.go",
    "content": "package postgres\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\tstorepb \"github.com/usememos/memos/proto/gen/store\"\n\t\"github.com/usememos/memos/store\"\n)\n\n// TestGetUserByPATHashWithMissingData tests the fix for #5611 and #5612.\n// Verifies that GetUserByPATHash handles missing/malformed data gracefully\n// instead of throwing PostgreSQL JSONB errors.\nfunc TestGetUserByPATHashWithMissingData(t *testing.T) {\n\tif testing.Short() {\n\t\tt.Skip(\"Skipping PostgreSQL integration test in short mode\")\n\t}\n\n\t// This test requires a real PostgreSQL connection\n\t// If DSN is not provided, skip the test\n\tdsn := getTestDSN()\n\tif dsn == \"\" {\n\t\tt.Skip(\"PostgreSQL DSN not provided, skipping test\")\n\t}\n\n\tdb, err := sql.Open(\"postgres\", dsn)\n\trequire.NoError(t, err)\n\tdefer db.Close()\n\n\t// Create test database\n\tctx := context.Background()\n\tdriver := &DB{db: db}\n\n\t// Setup: Create user_setting table if needed\n\t_, err = db.ExecContext(ctx, `\n\t\tCREATE TABLE IF NOT EXISTS user_setting (\n\t\t\tuser_id INTEGER NOT NULL,\n\t\t\tkey TEXT NOT NULL,\n\t\t\tvalue TEXT NOT NULL,\n\t\t\tUNIQUE(user_id, key)\n\t\t)\n\t`)\n\trequire.NoError(t, err)\n\n\t// Cleanup\n\tdefer func() {\n\t\tdb.ExecContext(ctx, \"DELETE FROM user_setting WHERE user_id IN (1001, 1002, 1003)\")\n\t}()\n\n\tt.Run(\"NoTokensKeyAtAll\", func(t *testing.T) {\n\t\t// Test case: User has no PERSONAL_ACCESS_TOKENS key\n\t\t// This simulates fresh users or users upgraded from v0.25.3\n\t\tresult, err := driver.GetUserByPATHash(ctx, \"any-hash\")\n\t\tassert.Error(t, err)\n\t\tassert.Nil(t, result)\n\t\tassert.Contains(t, err.Error(), \"PAT not found\")\n\t})\n\n\tt.Run(\"EmptyTokensArray\", func(t *testing.T) {\n\t\t// Insert user with empty tokens array\n\t\t_, err := db.ExecContext(ctx, `\n\t\t\tINSERT INTO user_setting (user_id, key, value)\n\t\t\tVALUES ($1, $2, $3)\n\t\t\tON CONFLICT (user_id, key) DO UPDATE SET value = EXCLUDED.value\n\t\t`, 1001, \"PERSONAL_ACCESS_TOKENS\", `{\"tokens\":[]}`)\n\t\trequire.NoError(t, err)\n\n\t\tresult, err := driver.GetUserByPATHash(ctx, \"any-hash\")\n\t\tassert.Error(t, err)\n\t\tassert.Nil(t, result)\n\t\tassert.Contains(t, err.Error(), \"PAT not found\")\n\t})\n\n\tt.Run(\"MalformedJSON\", func(t *testing.T) {\n\t\t// Insert user with malformed JSON\n\t\t_, err := db.ExecContext(ctx, `\n\t\t\tINSERT INTO user_setting (user_id, key, value)\n\t\t\tVALUES ($1, $2, $3)\n\t\t\tON CONFLICT (user_id, key) DO UPDATE SET value = EXCLUDED.value\n\t\t`, 1002, \"PERSONAL_ACCESS_TOKENS\", `{invalid json}`)\n\t\trequire.NoError(t, err)\n\n\t\t// Should handle gracefully without crashing\n\t\tresult, err := driver.GetUserByPATHash(ctx, \"any-hash\")\n\t\tassert.Error(t, err)\n\t\tassert.Nil(t, result)\n\t\tassert.Contains(t, err.Error(), \"PAT not found\")\n\t})\n\n\tt.Run(\"MissingTokensField\", func(t *testing.T) {\n\t\t// Insert user with valid JSON but missing 'tokens' field\n\t\t_, err := db.ExecContext(ctx, `\n\t\t\tINSERT INTO user_setting (user_id, key, value)\n\t\t\tVALUES ($1, $2, $3)\n\t\t\tON CONFLICT (user_id, key) DO UPDATE SET value = EXCLUDED.value\n\t\t`, 1003, \"PERSONAL_ACCESS_TOKENS\", `{\"someOtherField\":\"value\"}`)\n\t\trequire.NoError(t, err)\n\n\t\t// Should handle gracefully\n\t\tresult, err := driver.GetUserByPATHash(ctx, \"any-hash\")\n\t\tassert.Error(t, err)\n\t\tassert.Nil(t, result)\n\t})\n\n\tt.Run(\"ValidTokenFound\", func(t *testing.T) {\n\t\t// Insert user with valid PAT\n\t\tvalidJSON := `{\n\t\t\t\"tokens\": [\n\t\t\t\t{\n\t\t\t\t\t\"tokenId\": \"pat-test\",\n\t\t\t\t\t\"tokenHash\": \"hash-test-123\",\n\t\t\t\t\t\"description\": \"Test PAT\"\n\t\t\t\t}\n\t\t\t]\n\t\t}`\n\t\t_, err := db.ExecContext(ctx, `\n\t\t\tINSERT INTO user_setting (user_id, key, value)\n\t\t\tVALUES ($1, $2, $3)\n\t\t\tON CONFLICT (user_id, key) DO UPDATE SET value = EXCLUDED.value\n\t\t`, 1001, \"PERSONAL_ACCESS_TOKENS\", validJSON)\n\t\trequire.NoError(t, err)\n\n\t\t// Should find the token\n\t\tresult, err := driver.GetUserByPATHash(ctx, \"hash-test-123\")\n\t\tassert.NoError(t, err)\n\t\tassert.NotNil(t, result)\n\t\tassert.Equal(t, int32(1001), result.UserID)\n\t\tassert.Equal(t, \"pat-test\", result.PAT.TokenId)\n\t\tassert.Equal(t, \"hash-test-123\", result.PAT.TokenHash)\n\t})\n\n\tt.Run(\"MultipleUsersWithMixedData\", func(t *testing.T) {\n\t\t// User 1001: Valid PAT\n\t\tvalidJSON := `{\n\t\t\t\"tokens\": [\n\t\t\t\t{\n\t\t\t\t\t\"tokenId\": \"pat-user1\",\n\t\t\t\t\t\"tokenHash\": \"hash-user1\",\n\t\t\t\t\t\"description\": \"User 1 PAT\"\n\t\t\t\t}\n\t\t\t]\n\t\t}`\n\t\t_, err := db.ExecContext(ctx, `\n\t\t\tINSERT INTO user_setting (user_id, key, value)\n\t\t\tVALUES ($1, $2, $3)\n\t\t\tON CONFLICT (user_id, key) DO UPDATE SET value = EXCLUDED.value\n\t\t`, 1001, \"PERSONAL_ACCESS_TOKENS\", validJSON)\n\t\trequire.NoError(t, err)\n\n\t\t// User 1002: Malformed JSON (should be skipped)\n\t\t_, err = db.ExecContext(ctx, `\n\t\t\tINSERT INTO user_setting (user_id, key, value)\n\t\t\tVALUES ($1, $2, $3)\n\t\t\tON CONFLICT (user_id, key) DO UPDATE SET value = EXCLUDED.value\n\t\t`, 1002, \"PERSONAL_ACCESS_TOKENS\", `{invalid}`)\n\t\trequire.NoError(t, err)\n\n\t\t// User 1003: Empty array (should be skipped)\n\t\t_, err = db.ExecContext(ctx, `\n\t\t\tINSERT INTO user_setting (user_id, key, value)\n\t\t\tVALUES ($1, $2, $3)\n\t\t\tON CONFLICT (user_id, key) DO UPDATE SET value = EXCLUDED.value\n\t\t`, 1003, \"PERSONAL_ACCESS_TOKENS\", `{\"tokens\":[]}`)\n\t\trequire.NoError(t, err)\n\n\t\t// Should still find user 1001's token despite other users having bad data\n\t\tresult, err := driver.GetUserByPATHash(ctx, \"hash-user1\")\n\t\tassert.NoError(t, err)\n\t\tassert.NotNil(t, result)\n\t\tassert.Equal(t, int32(1001), result.UserID)\n\t})\n}\n\n// TestGetUserByPATHashPerformance ensures the simplified query doesn't cause performance issues.\nfunc TestGetUserByPATHashPerformance(t *testing.T) {\n\tif testing.Short() {\n\t\tt.Skip(\"Skipping performance test in short mode\")\n\t}\n\n\tdsn := getTestDSN()\n\tif dsn == \"\" {\n\t\tt.Skip(\"PostgreSQL DSN not provided, skipping test\")\n\t}\n\n\tdb, err := sql.Open(\"postgres\", dsn)\n\trequire.NoError(t, err)\n\tdefer db.Close()\n\n\tctx := context.Background()\n\tdriver := &DB{db: db}\n\n\t// Setup table\n\t_, err = db.ExecContext(ctx, `\n\t\tCREATE TABLE IF NOT EXISTS user_setting (\n\t\t\tuser_id INTEGER NOT NULL,\n\t\t\tkey TEXT NOT NULL,\n\t\t\tvalue TEXT NOT NULL,\n\t\t\tUNIQUE(user_id, key)\n\t\t)\n\t`)\n\trequire.NoError(t, err)\n\n\t// Cleanup\n\tdefer func() {\n\t\tdb.ExecContext(ctx, \"DELETE FROM user_setting WHERE user_id >= 2000 AND user_id < 2100\")\n\t}()\n\n\t// Insert 100 users with PATs\n\tfor i := 2000; i < 2100; i++ {\n\t\tjson := `{\n\t\t\t\"tokens\": [\n\t\t\t\t{\n\t\t\t\t\t\"tokenId\": \"pat-` + string(rune(i)) + `\",\n\t\t\t\t\t\"tokenHash\": \"hash-` + string(rune(i)) + `\",\n\t\t\t\t\t\"description\": \"Test PAT\"\n\t\t\t\t}\n\t\t\t]\n\t\t}`\n\t\t_, err = db.ExecContext(ctx, `\n\t\t\tINSERT INTO user_setting (user_id, key, value)\n\t\t\tVALUES ($1, $2, $3)\n\t\t\tON CONFLICT (user_id, key) DO UPDATE SET value = EXCLUDED.value\n\t\t`, i, \"PERSONAL_ACCESS_TOKENS\", json)\n\t\trequire.NoError(t, err)\n\t}\n\n\t// Query should complete quickly even with 100 users\n\tresult, err := driver.GetUserByPATHash(ctx, \"hash-\"+string(rune(2050)))\n\tassert.NoError(t, err)\n\tassert.NotNil(t, result)\n\tassert.Equal(t, int32(2050), result.UserID)\n}\n\n// getTestDSN returns PostgreSQL DSN from environment or returns empty string.\nfunc getTestDSN() string {\n\t// For unit tests, we expect TEST_POSTGRES_DSN to be set.\n\t// Example: TEST_POSTGRES_DSN=\"postgresql://user:pass@localhost:5432/memos_test?sslmode=disable\".\n\treturn \"\"\n}\n\n// TestUpsertUserSetting tests basic upsert functionality.\nfunc TestUpsertUserSetting(t *testing.T) {\n\tdsn := getTestDSN()\n\tif dsn == \"\" {\n\t\tt.Skip(\"PostgreSQL DSN not provided, skipping test\")\n\t}\n\n\tdb, err := sql.Open(\"postgres\", dsn)\n\trequire.NoError(t, err)\n\tdefer db.Close()\n\n\tctx := context.Background()\n\tdriver := &DB{db: db}\n\n\t// Setup\n\t_, err = db.ExecContext(ctx, `\n\t\tCREATE TABLE IF NOT EXISTS user_setting (\n\t\t\tuser_id INTEGER NOT NULL,\n\t\t\tkey TEXT NOT NULL,\n\t\t\tvalue TEXT NOT NULL,\n\t\t\tUNIQUE(user_id, key)\n\t\t)\n\t`)\n\trequire.NoError(t, err)\n\n\tdefer func() {\n\t\tdb.ExecContext(ctx, \"DELETE FROM user_setting WHERE user_id = 9999\")\n\t}()\n\n\t// Test insert\n\tsetting := &store.UserSetting{\n\t\tUserID: 9999,\n\t\tKey:    storepb.UserSetting_GENERAL,\n\t\tValue:  `{\"locale\":\"en\"}`,\n\t}\n\tresult, err := driver.UpsertUserSetting(ctx, setting)\n\tassert.NoError(t, err)\n\tassert.NotNil(t, result)\n\tassert.Equal(t, int32(9999), result.UserID)\n\n\t// Test update (upsert on conflict)\n\tsetting.Value = `{\"locale\":\"zh\"}`\n\tresult, err = driver.UpsertUserSetting(ctx, setting)\n\tassert.NoError(t, err)\n\tassert.Equal(t, `{\"locale\":\"zh\"}`, result.Value)\n}\n"
  },
  {
    "path": "store/db/sqlite/attachment.go",
    "content": "package sqlite\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/pkg/errors\"\n\t\"google.golang.org/protobuf/encoding/protojson\"\n\n\t\"github.com/usememos/memos/plugin/filter\"\n\tstorepb \"github.com/usememos/memos/proto/gen/store\"\n\t\"github.com/usememos/memos/store\"\n)\n\nfunc (d *DB) CreateAttachment(ctx context.Context, create *store.Attachment) (*store.Attachment, error) {\n\tfields := []string{\"`uid`\", \"`filename`\", \"`blob`\", \"`type`\", \"`size`\", \"`creator_id`\", \"`memo_id`\", \"`storage_type`\", \"`reference`\", \"`payload`\"}\n\tplaceholder := []string{\"?\", \"?\", \"?\", \"?\", \"?\", \"?\", \"?\", \"?\", \"?\", \"?\"}\n\tstorageType := \"\"\n\tif create.StorageType != storepb.AttachmentStorageType_ATTACHMENT_STORAGE_TYPE_UNSPECIFIED {\n\t\tstorageType = create.StorageType.String()\n\t}\n\tpayloadString := \"{}\"\n\tif create.Payload != nil {\n\t\tbytes, err := protojson.Marshal(create.Payload)\n\t\tif err != nil {\n\t\t\treturn nil, errors.Wrap(err, \"failed to marshal attachment payload\")\n\t\t}\n\t\tpayloadString = string(bytes)\n\t}\n\targs := []any{create.UID, create.Filename, create.Blob, create.Type, create.Size, create.CreatorID, create.MemoID, storageType, create.Reference, payloadString}\n\n\tstmt := \"INSERT INTO `attachment` (\" + strings.Join(fields, \", \") + \") VALUES (\" + strings.Join(placeholder, \", \") + \") RETURNING `id`, `created_ts`, `updated_ts`\"\n\tif err := d.db.QueryRowContext(ctx, stmt, args...).Scan(&create.ID, &create.CreatedTs, &create.UpdatedTs); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn create, nil\n}\n\nfunc (d *DB) ListAttachments(ctx context.Context, find *store.FindAttachment) ([]*store.Attachment, error) {\n\twhere, args := []string{\"1 = 1\"}, []any{}\n\n\tif v := find.ID; v != nil {\n\t\twhere, args = append(where, \"`attachment`.`id` = ?\"), append(args, *v)\n\t}\n\tif v := find.UID; v != nil {\n\t\twhere, args = append(where, \"`attachment`.`uid` = ?\"), append(args, *v)\n\t}\n\tif v := find.CreatorID; v != nil {\n\t\twhere, args = append(where, \"`attachment`.`creator_id` = ?\"), append(args, *v)\n\t}\n\tif v := find.Filename; v != nil {\n\t\twhere, args = append(where, \"`attachment`.`filename` = ?\"), append(args, *v)\n\t}\n\tif v := find.FilenameSearch; v != nil {\n\t\twhere, args = append(where, \"`attachment`.`filename` LIKE ?\"), append(args, fmt.Sprintf(\"%%%s%%\", *v))\n\t}\n\tif v := find.MemoID; v != nil {\n\t\twhere, args = append(where, \"`attachment`.`memo_id` = ?\"), append(args, *v)\n\t}\n\tif len(find.MemoIDList) > 0 {\n\t\tplaceholders := make([]string, 0, len(find.MemoIDList))\n\t\tfor range find.MemoIDList {\n\t\t\tplaceholders = append(placeholders, \"?\")\n\t\t}\n\t\twhere = append(where, \"`attachment`.`memo_id` IN (\"+strings.Join(placeholders, \",\")+\")\")\n\t\tfor _, id := range find.MemoIDList {\n\t\t\targs = append(args, id)\n\t\t}\n\t}\n\tif find.HasRelatedMemo {\n\t\twhere = append(where, \"`attachment`.`memo_id` IS NOT NULL\")\n\t}\n\tif find.StorageType != nil {\n\t\twhere, args = append(where, \"`attachment`.`storage_type` = ?\"), append(args, find.StorageType.String())\n\t}\n\n\tif len(find.Filters) > 0 {\n\t\tengine, err := filter.DefaultAttachmentEngine()\n\t\tif err != nil {\n\t\t\treturn nil, errors.Wrap(err, \"failed to get filter engine\")\n\t\t}\n\t\tif err := filter.AppendConditions(ctx, engine, find.Filters, filter.DialectSQLite, &where, &args); err != nil {\n\t\t\treturn nil, errors.Wrap(err, \"failed to append filter conditions\")\n\t\t}\n\t}\n\n\tfields := []string{\n\t\t\"`attachment`.`id` AS `id`\",\n\t\t\"`attachment`.`uid` AS `uid`\",\n\t\t\"`attachment`.`filename` AS `filename`\",\n\t\t\"`attachment`.`type` AS `type`\",\n\t\t\"`attachment`.`size` AS `size`\",\n\t\t\"`attachment`.`creator_id` AS `creator_id`\",\n\t\t\"`attachment`.`created_ts` AS `created_ts`\",\n\t\t\"`attachment`.`updated_ts` AS `updated_ts`\",\n\t\t\"`attachment`.`memo_id` AS `memo_id`\",\n\t\t\"`attachment`.`storage_type` AS `storage_type`\",\n\t\t\"`attachment`.`reference` AS `reference`\",\n\t\t\"`attachment`.`payload` AS `payload`\",\n\t\t\"CASE WHEN `memo`.`uid` IS NOT NULL THEN `memo`.`uid` ELSE NULL END AS `memo_uid`\",\n\t}\n\tif find.GetBlob {\n\t\tfields = append(fields, \"`attachment`.`blob` AS `blob`\")\n\t}\n\n\tquery := \"SELECT \" + strings.Join(fields, \", \") + \" FROM `attachment`\" + \" \" +\n\t\t\"LEFT JOIN `memo` ON `attachment`.`memo_id` = `memo`.`id`\" + \" \" +\n\t\t\"WHERE \" + strings.Join(where, \" AND \") + \" \" +\n\t\t\"ORDER BY `attachment`.`updated_ts` DESC\"\n\tif find.Limit != nil {\n\t\tquery = fmt.Sprintf(\"%s LIMIT %d\", query, *find.Limit)\n\t\tif find.Offset != nil {\n\t\t\tquery = fmt.Sprintf(\"%s OFFSET %d\", query, *find.Offset)\n\t\t}\n\t}\n\n\trows, err := d.db.QueryContext(ctx, query, args...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\n\tlist := make([]*store.Attachment, 0)\n\tfor rows.Next() {\n\t\tattachment := store.Attachment{}\n\t\tvar memoID sql.NullInt32\n\t\tvar storageType string\n\t\tvar payloadBytes []byte\n\t\tdests := []any{\n\t\t\t&attachment.ID,\n\t\t\t&attachment.UID,\n\t\t\t&attachment.Filename,\n\t\t\t&attachment.Type,\n\t\t\t&attachment.Size,\n\t\t\t&attachment.CreatorID,\n\t\t\t&attachment.CreatedTs,\n\t\t\t&attachment.UpdatedTs,\n\t\t\t&memoID,\n\t\t\t&storageType,\n\t\t\t&attachment.Reference,\n\t\t\t&payloadBytes,\n\t\t\t&attachment.MemoUID,\n\t\t}\n\t\tif find.GetBlob {\n\t\t\tdests = append(dests, &attachment.Blob)\n\t\t}\n\t\tif err := rows.Scan(dests...); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tif memoID.Valid {\n\t\t\tattachment.MemoID = &memoID.Int32\n\t\t}\n\t\tattachment.StorageType = storepb.AttachmentStorageType(storepb.AttachmentStorageType_value[storageType])\n\t\tpayload := &storepb.AttachmentPayload{}\n\t\tif err := protojsonUnmarshaler.Unmarshal(payloadBytes, payload); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tattachment.Payload = payload\n\t\tlist = append(list, &attachment)\n\t}\n\n\tif err := rows.Err(); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn list, nil\n}\n\nfunc (d *DB) UpdateAttachment(ctx context.Context, update *store.UpdateAttachment) error {\n\tset, args := []string{}, []any{}\n\n\tif v := update.UID; v != nil {\n\t\tset, args = append(set, \"`uid` = ?\"), append(args, *v)\n\t}\n\tif v := update.UpdatedTs; v != nil {\n\t\tset, args = append(set, \"`updated_ts` = ?\"), append(args, *v)\n\t}\n\tif v := update.Filename; v != nil {\n\t\tset, args = append(set, \"`filename` = ?\"), append(args, *v)\n\t}\n\tif v := update.MemoID; v != nil {\n\t\tset, args = append(set, \"`memo_id` = ?\"), append(args, *v)\n\t}\n\tif v := update.Reference; v != nil {\n\t\tset, args = append(set, \"`reference` = ?\"), append(args, *v)\n\t}\n\tif v := update.Payload; v != nil {\n\t\tbytes, err := protojson.Marshal(v)\n\t\tif err != nil {\n\t\t\treturn errors.Wrap(err, \"failed to marshal attachment payload\")\n\t\t}\n\t\tset, args = append(set, \"`payload` = ?\"), append(args, string(bytes))\n\t}\n\n\targs = append(args, update.ID)\n\tstmt := \"UPDATE `attachment` SET \" + strings.Join(set, \", \") + \" WHERE `id` = ?\"\n\tresult, err := d.db.ExecContext(ctx, stmt, args...)\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"failed to update attachment\")\n\t}\n\tif _, err := result.RowsAffected(); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc (d *DB) DeleteAttachment(ctx context.Context, delete *store.DeleteAttachment) error {\n\tstmt := \"DELETE FROM `attachment` WHERE `id` = ?\"\n\tresult, err := d.db.ExecContext(ctx, stmt, delete.ID)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif _, err := result.RowsAffected(); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "store/db/sqlite/common.go",
    "content": "package sqlite\n\nimport \"google.golang.org/protobuf/encoding/protojson\"\n\nvar (\n\tprotojsonUnmarshaler = protojson.UnmarshalOptions{\n\t\tDiscardUnknown: true,\n\t}\n)\n"
  },
  {
    "path": "store/db/sqlite/functions.go",
    "content": "// Package sqlite provides SQLite driver implementation with custom functions.\n// Custom functions are registered globally on first use to extend SQLite's\n// limited ASCII-only text operations with proper Unicode support.\npackage sqlite\n\nimport (\n\t\"database/sql/driver\"\n\t\"sync\"\n\n\t\"golang.org/x/text/cases\"\n\tmsqlite \"modernc.org/sqlite\"\n)\n\nvar (\n\tregisterUnicodeLowerOnce sync.Once\n\tregisterUnicodeLowerErr  error\n\t// unicodeFold provides Unicode case folding for case-insensitive comparisons.\n\t// It's safe to use concurrently and reused across all function calls.\n\tunicodeFold = cases.Fold()\n)\n\n// ensureUnicodeLowerRegistered registers the memos_unicode_lower custom function\n// with SQLite. This function provides proper Unicode case folding for case-insensitive\n// text comparisons, overcoming modernc.org/sqlite's lack of ICU extension.\n//\n// The function is registered once globally and is safe to call multiple times.\nfunc ensureUnicodeLowerRegistered() error {\n\tregisterUnicodeLowerOnce.Do(func() {\n\t\tregisterUnicodeLowerErr = msqlite.RegisterScalarFunction(\"memos_unicode_lower\", 1, func(_ *msqlite.FunctionContext, args []driver.Value) (driver.Value, error) {\n\t\t\tif len(args) == 0 || args[0] == nil {\n\t\t\t\treturn nil, nil\n\t\t\t}\n\t\t\tswitch v := args[0].(type) {\n\t\t\tcase string:\n\t\t\t\treturn unicodeFold.String(v), nil\n\t\t\tcase []byte:\n\t\t\t\treturn unicodeFold.String(string(v)), nil\n\t\t\tdefault:\n\t\t\t\treturn v, nil\n\t\t\t}\n\t\t})\n\t})\n\treturn registerUnicodeLowerErr\n}\n"
  },
  {
    "path": "store/db/sqlite/idp.go",
    "content": "package sqlite\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strings\"\n\n\tstorepb \"github.com/usememos/memos/proto/gen/store\"\n\t\"github.com/usememos/memos/store\"\n)\n\nfunc (d *DB) CreateIdentityProvider(ctx context.Context, create *store.IdentityProvider) (*store.IdentityProvider, error) {\n\tplaceholders := []string{\"?\", \"?\", \"?\", \"?\", \"?\"}\n\tfields := []string{\"`uid`\", \"`name`\", \"`type`\", \"`identifier_filter`\", \"`config`\"}\n\targs := []any{create.UID, create.Name, create.Type.String(), create.IdentifierFilter, create.Config}\n\n\tstmt := \"INSERT INTO `idp` (\" + strings.Join(fields, \", \") + \") VALUES (\" + strings.Join(placeholders, \", \") + \") RETURNING `id`\"\n\tif err := d.db.QueryRowContext(ctx, stmt, args...).Scan(&create.ID); err != nil {\n\t\treturn nil, err\n\t}\n\n\tidentityProvider := create\n\treturn identityProvider, nil\n}\n\nfunc (d *DB) ListIdentityProviders(ctx context.Context, find *store.FindIdentityProvider) ([]*store.IdentityProvider, error) {\n\twhere, args := []string{\"1 = 1\"}, []any{}\n\tif v := find.ID; v != nil {\n\t\twhere, args = append(where, fmt.Sprintf(\"id = $%d\", len(args)+1)), append(args, *v)\n\t}\n\tif v := find.UID; v != nil {\n\t\twhere, args = append(where, fmt.Sprintf(\"uid = $%d\", len(args)+1)), append(args, *v)\n\t}\n\n\trows, err := d.db.QueryContext(ctx, `\n\t\tSELECT\n\t\t\tid,\n\t\t\tuid,\n\t\t\tname,\n\t\t\ttype,\n\t\t\tidentifier_filter,\n\t\t\tconfig\n\t\tFROM idp\n\t\tWHERE `+strings.Join(where, \" AND \")+` ORDER BY id ASC`,\n\t\targs...,\n\t)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\n\tvar identityProviders []*store.IdentityProvider\n\tfor rows.Next() {\n\t\tvar identityProvider store.IdentityProvider\n\t\tvar typeString string\n\t\tif err := rows.Scan(\n\t\t\t&identityProvider.ID,\n\t\t\t&identityProvider.UID,\n\t\t\t&identityProvider.Name,\n\t\t\t&typeString,\n\t\t\t&identityProvider.IdentifierFilter,\n\t\t\t&identityProvider.Config,\n\t\t); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tidentityProvider.Type = storepb.IdentityProvider_Type(storepb.IdentityProvider_Type_value[typeString])\n\t\tidentityProviders = append(identityProviders, &identityProvider)\n\t}\n\n\tif err := rows.Err(); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn identityProviders, nil\n}\n\nfunc (d *DB) UpdateIdentityProvider(ctx context.Context, update *store.UpdateIdentityProvider) (*store.IdentityProvider, error) {\n\tset, args := []string{}, []any{}\n\tif v := update.Name; v != nil {\n\t\tset, args = append(set, \"name = ?\"), append(args, *v)\n\t}\n\tif v := update.IdentifierFilter; v != nil {\n\t\tset, args = append(set, \"identifier_filter = ?\"), append(args, *v)\n\t}\n\tif v := update.Config; v != nil {\n\t\tset, args = append(set, \"config = ?\"), append(args, *v)\n\t}\n\targs = append(args, update.ID)\n\n\tstmt := `\n\t\tUPDATE idp\n\t\tSET ` + strings.Join(set, \", \") + `\n\t\tWHERE id = ?\n\t\tRETURNING id, uid, name, type, identifier_filter, config\n\t`\n\tvar identityProvider store.IdentityProvider\n\tvar typeString string\n\tif err := d.db.QueryRowContext(ctx, stmt, args...).Scan(\n\t\t&identityProvider.ID,\n\t\t&identityProvider.UID,\n\t\t&identityProvider.Name,\n\t\t&typeString,\n\t\t&identityProvider.IdentifierFilter,\n\t\t&identityProvider.Config,\n\t); err != nil {\n\t\treturn nil, err\n\t}\n\tidentityProvider.Type = storepb.IdentityProvider_Type(storepb.IdentityProvider_Type_value[typeString])\n\treturn &identityProvider, nil\n}\n\nfunc (d *DB) DeleteIdentityProvider(ctx context.Context, delete *store.DeleteIdentityProvider) error {\n\twhere, args := []string{\"id = ?\"}, []any{delete.ID}\n\tstmt := `DELETE FROM idp WHERE ` + strings.Join(where, \" AND \")\n\tresult, err := d.db.ExecContext(ctx, stmt, args...)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif _, err = result.RowsAffected(); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "store/db/sqlite/inbox.go",
    "content": "package sqlite\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/pkg/errors\"\n\t\"google.golang.org/protobuf/encoding/protojson\"\n\n\tstorepb \"github.com/usememos/memos/proto/gen/store\"\n\t\"github.com/usememos/memos/store\"\n)\n\nfunc (d *DB) CreateInbox(ctx context.Context, create *store.Inbox) (*store.Inbox, error) {\n\tmessageString := \"{}\"\n\tif create.Message != nil {\n\t\tbytes, err := protojson.Marshal(create.Message)\n\t\tif err != nil {\n\t\t\treturn nil, errors.Wrap(err, \"failed to marshal inbox message\")\n\t\t}\n\t\tmessageString = string(bytes)\n\t}\n\n\tfields := []string{\"`sender_id`\", \"`receiver_id`\", \"`status`\", \"`message`\"}\n\tplaceholder := []string{\"?\", \"?\", \"?\", \"?\"}\n\targs := []any{create.SenderID, create.ReceiverID, create.Status, messageString}\n\n\tstmt := \"INSERT INTO `inbox` (\" + strings.Join(fields, \", \") + \") VALUES (\" + strings.Join(placeholder, \", \") + \") RETURNING `id`, `created_ts`\"\n\tif err := d.db.QueryRowContext(ctx, stmt, args...).Scan(\n\t\t&create.ID,\n\t\t&create.CreatedTs,\n\t); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn create, nil\n}\n\nfunc (d *DB) ListInboxes(ctx context.Context, find *store.FindInbox) ([]*store.Inbox, error) {\n\twhere, args := []string{\"1 = 1\"}, []any{}\n\n\tif find.ID != nil {\n\t\twhere, args = append(where, \"`id` = ?\"), append(args, *find.ID)\n\t}\n\tif find.SenderID != nil {\n\t\twhere, args = append(where, \"`sender_id` = ?\"), append(args, *find.SenderID)\n\t}\n\tif find.ReceiverID != nil {\n\t\twhere, args = append(where, \"`receiver_id` = ?\"), append(args, *find.ReceiverID)\n\t}\n\tif find.Status != nil {\n\t\twhere, args = append(where, \"`status` = ?\"), append(args, *find.Status)\n\t}\n\tif find.MessageType != nil {\n\t\t// Filter by message type using JSON extraction\n\t\t// Note: The type field in JSON is stored as string representation of the enum name\n\t\tif *find.MessageType == storepb.InboxMessage_TYPE_UNSPECIFIED {\n\t\t\twhere, args = append(where, \"(JSON_EXTRACT(`message`, '$.type') IS NULL OR JSON_EXTRACT(`message`, '$.type') = ?)\"), append(args, find.MessageType.String())\n\t\t} else {\n\t\t\twhere, args = append(where, \"JSON_EXTRACT(`message`, '$.type') = ?\"), append(args, find.MessageType.String())\n\t\t}\n\t}\n\n\tquery := \"SELECT `id`, `created_ts`, `sender_id`, `receiver_id`, `status`, `message` FROM `inbox` WHERE \" + strings.Join(where, \" AND \") + \" ORDER BY `created_ts` DESC\"\n\tif find.Limit != nil {\n\t\tquery = fmt.Sprintf(\"%s LIMIT %d\", query, *find.Limit)\n\t\tif find.Offset != nil {\n\t\t\tquery = fmt.Sprintf(\"%s OFFSET %d\", query, *find.Offset)\n\t\t}\n\t}\n\trows, err := d.db.QueryContext(ctx, query, args...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\n\tlist := []*store.Inbox{}\n\tfor rows.Next() {\n\t\tinbox := &store.Inbox{}\n\t\tvar messageBytes []byte\n\t\tif err := rows.Scan(\n\t\t\t&inbox.ID,\n\t\t\t&inbox.CreatedTs,\n\t\t\t&inbox.SenderID,\n\t\t\t&inbox.ReceiverID,\n\t\t\t&inbox.Status,\n\t\t\t&messageBytes,\n\t\t); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tmessage := &storepb.InboxMessage{}\n\t\tif err := protojsonUnmarshaler.Unmarshal(messageBytes, message); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tinbox.Message = message\n\t\tlist = append(list, inbox)\n\t}\n\n\tif err := rows.Err(); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn list, nil\n}\n\nfunc (d *DB) UpdateInbox(ctx context.Context, update *store.UpdateInbox) (*store.Inbox, error) {\n\tset, args := []string{\"`status` = ?\"}, []any{update.Status.String()}\n\targs = append(args, update.ID)\n\tquery := \"UPDATE `inbox` SET \" + strings.Join(set, \", \") + \" WHERE `id` = ? RETURNING `id`, `created_ts`, `sender_id`, `receiver_id`, `status`, `message`\"\n\tinbox := &store.Inbox{}\n\tvar messageBytes []byte\n\tif err := d.db.QueryRowContext(ctx, query, args...).Scan(\n\t\t&inbox.ID,\n\t\t&inbox.CreatedTs,\n\t\t&inbox.SenderID,\n\t\t&inbox.ReceiverID,\n\t\t&inbox.Status,\n\t\t&messageBytes,\n\t); err != nil {\n\t\treturn nil, err\n\t}\n\tmessage := &storepb.InboxMessage{}\n\tif err := protojsonUnmarshaler.Unmarshal(messageBytes, message); err != nil {\n\t\treturn nil, err\n\t}\n\tinbox.Message = message\n\treturn inbox, nil\n}\n\nfunc (d *DB) DeleteInbox(ctx context.Context, delete *store.DeleteInbox) error {\n\tresult, err := d.db.ExecContext(ctx, \"DELETE FROM `inbox` WHERE `id` = ?\", delete.ID)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif _, err := result.RowsAffected(); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "store/db/sqlite/instance_setting.go",
    "content": "package sqlite\n\nimport (\n\t\"context\"\n\t\"strings\"\n\n\t\"github.com/usememos/memos/store\"\n)\n\nfunc (d *DB) UpsertInstanceSetting(ctx context.Context, upsert *store.InstanceSetting) (*store.InstanceSetting, error) {\n\tstmt := `\n\t\tINSERT INTO system_setting (\n\t\t\tname, value, description\n\t\t)\n\t\tVALUES (?, ?, ?)\n\t\tON CONFLICT(name) DO UPDATE\n\t\tSET\n\t\t\tvalue = EXCLUDED.value,\n\t\t\tdescription = EXCLUDED.description\n\t`\n\tif _, err := d.db.ExecContext(ctx, stmt, upsert.Name, upsert.Value, upsert.Description); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn upsert, nil\n}\n\nfunc (d *DB) ListInstanceSettings(ctx context.Context, find *store.FindInstanceSetting) ([]*store.InstanceSetting, error) {\n\twhere, args := []string{\"1 = 1\"}, []any{}\n\tif find.Name != \"\" {\n\t\twhere, args = append(where, \"name = ?\"), append(args, find.Name)\n\t}\n\n\tquery := `\n\t\tSELECT\n\t\t\tname,\n\t\t\tvalue,\n\t\t\tdescription\n\t\tFROM system_setting\n\t\tWHERE ` + strings.Join(where, \" AND \")\n\n\trows, err := d.db.QueryContext(ctx, query, args...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\n\tlist := []*store.InstanceSetting{}\n\tfor rows.Next() {\n\t\tsystemSettingMessage := &store.InstanceSetting{}\n\t\tif err := rows.Scan(\n\t\t\t&systemSettingMessage.Name,\n\t\t\t&systemSettingMessage.Value,\n\t\t\t&systemSettingMessage.Description,\n\t\t); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tlist = append(list, systemSettingMessage)\n\t}\n\n\tif err := rows.Err(); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn list, nil\n}\n\nfunc (d *DB) DeleteInstanceSetting(ctx context.Context, delete *store.DeleteInstanceSetting) error {\n\tstmt := \"DELETE FROM system_setting WHERE name = ?\"\n\t_, err := d.db.ExecContext(ctx, stmt, delete.Name)\n\treturn err\n}\n"
  },
  {
    "path": "store/db/sqlite/memo.go",
    "content": "package sqlite\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/pkg/errors\"\n\t\"google.golang.org/protobuf/encoding/protojson\"\n\n\t\"github.com/usememos/memos/plugin/filter\"\n\tstorepb \"github.com/usememos/memos/proto/gen/store\"\n\t\"github.com/usememos/memos/store\"\n)\n\nfunc (d *DB) CreateMemo(ctx context.Context, create *store.Memo) (*store.Memo, error) {\n\tfields := []string{\"`uid`\", \"`creator_id`\", \"`content`\", \"`visibility`\", \"`payload`\"}\n\tplaceholder := []string{\"?\", \"?\", \"?\", \"?\", \"?\"}\n\tpayload := \"{}\"\n\tif create.Payload != nil {\n\t\tpayloadBytes, err := protojson.Marshal(create.Payload)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tpayload = string(payloadBytes)\n\t}\n\targs := []any{create.UID, create.CreatorID, create.Content, create.Visibility, payload}\n\n\t// Add custom timestamps if provided\n\tif create.CreatedTs != 0 {\n\t\tfields = append(fields, \"`created_ts`\")\n\t\tplaceholder = append(placeholder, \"?\")\n\t\targs = append(args, create.CreatedTs)\n\t}\n\tif create.UpdatedTs != 0 {\n\t\tfields = append(fields, \"`updated_ts`\")\n\t\tplaceholder = append(placeholder, \"?\")\n\t\targs = append(args, create.UpdatedTs)\n\t}\n\n\tstmt := \"INSERT INTO `memo` (\" + strings.Join(fields, \", \") + \") VALUES (\" + strings.Join(placeholder, \", \") + \") RETURNING `id`, `created_ts`, `updated_ts`, `row_status`\"\n\tif err := d.db.QueryRowContext(ctx, stmt, args...).Scan(\n\t\t&create.ID,\n\t\t&create.CreatedTs,\n\t\t&create.UpdatedTs,\n\t\t&create.RowStatus,\n\t); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn create, nil\n}\n\nfunc (d *DB) ListMemos(ctx context.Context, find *store.FindMemo) ([]*store.Memo, error) {\n\twhere, args := []string{\"1 = 1\"}, []any{}\n\n\tengine, err := filter.DefaultEngine()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif err := filter.AppendConditions(ctx, engine, find.Filters, filter.DialectSQLite, &where, &args); err != nil {\n\t\treturn nil, err\n\t}\n\tif v := find.ID; v != nil {\n\t\twhere, args = append(where, \"`memo`.`id` = ?\"), append(args, *v)\n\t}\n\tif len(find.IDList) > 0 {\n\t\tplaceholders := make([]string, 0, len(find.IDList))\n\t\tfor range find.IDList {\n\t\t\tplaceholders = append(placeholders, \"?\")\n\t\t}\n\t\twhere = append(where, \"`memo`.`id` IN (\"+strings.Join(placeholders, \",\")+\")\")\n\t\tfor _, id := range find.IDList {\n\t\t\targs = append(args, id)\n\t\t}\n\t}\n\tif v := find.UID; v != nil {\n\t\twhere, args = append(where, \"`memo`.`uid` = ?\"), append(args, *v)\n\t}\n\tif len(find.UIDList) > 0 {\n\t\tplaceholders := make([]string, 0, len(find.UIDList))\n\t\tfor range find.UIDList {\n\t\t\tplaceholders = append(placeholders, \"?\")\n\t\t}\n\t\twhere = append(where, \"`memo`.`uid` IN (\"+strings.Join(placeholders, \",\")+\")\")\n\t\tfor _, uid := range find.UIDList {\n\t\t\targs = append(args, uid)\n\t\t}\n\t}\n\tif v := find.CreatorID; v != nil {\n\t\twhere, args = append(where, \"`memo`.`creator_id` = ?\"), append(args, *v)\n\t}\n\tif v := find.RowStatus; v != nil {\n\t\twhere, args = append(where, \"`memo`.`row_status` = ?\"), append(args, *v)\n\t}\n\tif v := find.VisibilityList; len(v) != 0 {\n\t\tplaceholder := []string{}\n\t\tfor _, visibility := range v {\n\t\t\tplaceholder = append(placeholder, \"?\")\n\t\t\targs = append(args, visibility.String())\n\t\t}\n\t\twhere = append(where, fmt.Sprintf(\"`memo`.`visibility` IN (%s)\", strings.Join(placeholder, \",\")))\n\t}\n\tif find.ExcludeComments {\n\t\twhere = append(where, \"`parent_uid` IS NULL\")\n\t}\n\n\torder := \"DESC\"\n\tif find.OrderByTimeAsc {\n\t\torder = \"ASC\"\n\t}\n\torderBy := []string{}\n\tif find.OrderByPinned {\n\t\torderBy = append(orderBy, \"`pinned` DESC\")\n\t}\n\tif find.OrderByUpdatedTs {\n\t\torderBy = append(orderBy, \"`updated_ts` \"+order)\n\t} else {\n\t\torderBy = append(orderBy, \"`created_ts` \"+order)\n\t}\n\t// Add id as final tie-breaker\n\torderBy = append(orderBy, \"`id` DESC\")\n\tfields := []string{\n\t\t\"`memo`.`id` AS `id`\",\n\t\t\"`memo`.`uid` AS `uid`\",\n\t\t\"`memo`.`creator_id` AS `creator_id`\",\n\t\t\"`memo`.`created_ts` AS `created_ts`\",\n\t\t\"`memo`.`updated_ts` AS `updated_ts`\",\n\t\t\"`memo`.`row_status` AS `row_status`\",\n\t\t\"`memo`.`visibility` AS `visibility`\",\n\t\t\"`memo`.`pinned` AS `pinned`\",\n\t\t\"`memo`.`payload` AS `payload`\",\n\t\t\"CASE WHEN `parent_memo`.`uid` IS NOT NULL THEN `parent_memo`.`uid` ELSE NULL END AS `parent_uid`\",\n\t}\n\tif !find.ExcludeContent {\n\t\tfields = append(fields, \"`memo`.`content` AS `content`\")\n\t}\n\n\tquery := \"SELECT \" + strings.Join(fields, \", \") + \"FROM `memo` \" +\n\t\t\"LEFT JOIN `memo_relation` ON `memo`.`id` = `memo_relation`.`memo_id` AND `memo_relation`.`type` = \\\"COMMENT\\\" \" +\n\t\t\"LEFT JOIN `memo` AS `parent_memo` ON `memo_relation`.`related_memo_id` = `parent_memo`.`id` \" +\n\t\t\"WHERE \" + strings.Join(where, \" AND \") + \" \" +\n\t\t\"ORDER BY \" + strings.Join(orderBy, \", \")\n\tif find.Limit != nil {\n\t\tquery = fmt.Sprintf(\"%s LIMIT %d\", query, *find.Limit)\n\t\tif find.Offset != nil {\n\t\t\tquery = fmt.Sprintf(\"%s OFFSET %d\", query, *find.Offset)\n\t\t}\n\t}\n\n\trows, err := d.db.QueryContext(ctx, query, args...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\n\tlist := make([]*store.Memo, 0)\n\tfor rows.Next() {\n\t\tvar memo store.Memo\n\t\tvar payloadBytes []byte\n\t\tdests := []any{\n\t\t\t&memo.ID,\n\t\t\t&memo.UID,\n\t\t\t&memo.CreatorID,\n\t\t\t&memo.CreatedTs,\n\t\t\t&memo.UpdatedTs,\n\t\t\t&memo.RowStatus,\n\t\t\t&memo.Visibility,\n\t\t\t&memo.Pinned,\n\t\t\t&payloadBytes,\n\t\t\t&memo.ParentUID,\n\t\t}\n\t\tif !find.ExcludeContent {\n\t\t\tdests = append(dests, &memo.Content)\n\t\t}\n\t\tif err := rows.Scan(dests...); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tpayload := &storepb.MemoPayload{}\n\t\tif err := protojsonUnmarshaler.Unmarshal(payloadBytes, payload); err != nil {\n\t\t\treturn nil, errors.Wrap(err, \"failed to unmarshal payload\")\n\t\t}\n\t\tmemo.Payload = payload\n\t\tlist = append(list, &memo)\n\t}\n\n\tif err := rows.Err(); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn list, nil\n}\n\nfunc (d *DB) UpdateMemo(ctx context.Context, update *store.UpdateMemo) error {\n\tset, args := []string{}, []any{}\n\tif v := update.UID; v != nil {\n\t\tset, args = append(set, \"`uid` = ?\"), append(args, *v)\n\t}\n\tif v := update.CreatedTs; v != nil {\n\t\tset, args = append(set, \"`created_ts` = ?\"), append(args, *v)\n\t}\n\tif v := update.UpdatedTs; v != nil {\n\t\tset, args = append(set, \"`updated_ts` = ?\"), append(args, *v)\n\t}\n\tif v := update.RowStatus; v != nil {\n\t\tset, args = append(set, \"`row_status` = ?\"), append(args, *v)\n\t}\n\tif v := update.Content; v != nil {\n\t\tset, args = append(set, \"`content` = ?\"), append(args, *v)\n\t}\n\tif v := update.Visibility; v != nil {\n\t\tset, args = append(set, \"`visibility` = ?\"), append(args, *v)\n\t}\n\tif v := update.Pinned; v != nil {\n\t\tset, args = append(set, \"`pinned` = ?\"), append(args, *v)\n\t}\n\tif v := update.Payload; v != nil {\n\t\tpayloadBytes, err := protojson.Marshal(v)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tset, args = append(set, \"`payload` = ?\"), append(args, string(payloadBytes))\n\t}\n\tif len(set) == 0 {\n\t\treturn nil\n\t}\n\targs = append(args, update.ID)\n\n\tstmt := \"UPDATE `memo` SET \" + strings.Join(set, \", \") + \" WHERE `id` = ?\"\n\tif _, err := d.db.ExecContext(ctx, stmt, args...); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc (d *DB) DeleteMemo(ctx context.Context, delete *store.DeleteMemo) error {\n\twhere, args := []string{\"`id` = ?\"}, []any{delete.ID}\n\tstmt := \"DELETE FROM `memo` WHERE \" + strings.Join(where, \" AND \")\n\tresult, err := d.db.ExecContext(ctx, stmt, args...)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif _, err := result.RowsAffected(); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "store/db/sqlite/memo_relation.go",
    "content": "package sqlite\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/usememos/memos/plugin/filter\"\n\t\"github.com/usememos/memos/store\"\n)\n\nfunc (d *DB) UpsertMemoRelation(ctx context.Context, create *store.MemoRelation) (*store.MemoRelation, error) {\n\tstmt := `\n\t\tINSERT INTO memo_relation (\n\t\t\tmemo_id,\n\t\t\trelated_memo_id,\n\t\t\ttype\n\t\t)\n\t\tVALUES (?, ?, ?)\n\t\tON CONFLICT(memo_id, related_memo_id, type) DO UPDATE SET type = excluded.type\n\t\tRETURNING memo_id, related_memo_id, type\n\t`\n\tmemoRelation := &store.MemoRelation{}\n\tif err := d.db.QueryRowContext(\n\t\tctx,\n\t\tstmt,\n\t\tcreate.MemoID,\n\t\tcreate.RelatedMemoID,\n\t\tcreate.Type,\n\t).Scan(\n\t\t&memoRelation.MemoID,\n\t\t&memoRelation.RelatedMemoID,\n\t\t&memoRelation.Type,\n\t); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn memoRelation, nil\n}\n\nfunc (d *DB) ListMemoRelations(ctx context.Context, find *store.FindMemoRelation) ([]*store.MemoRelation, error) {\n\twhere, args := []string{\"TRUE\"}, []any{}\n\tif find.MemoID != nil {\n\t\twhere, args = append(where, \"memo_id = ?\"), append(args, find.MemoID)\n\t}\n\tif find.RelatedMemoID != nil {\n\t\twhere, args = append(where, \"related_memo_id = ?\"), append(args, find.RelatedMemoID)\n\t}\n\tif find.Type != nil {\n\t\twhere, args = append(where, \"type = ?\"), append(args, find.Type)\n\t}\n\tif len(find.MemoIDList) > 0 {\n\t\tplaceholders := make([]string, len(find.MemoIDList))\n\t\tfor i, id := range find.MemoIDList {\n\t\t\tplaceholders[i] = \"?\"\n\t\t\targs = append(args, id)\n\t\t}\n\t\tinClause := strings.Join(placeholders, \", \")\n\t\t// Duplicate args for the second IN clause.\n\t\tfor _, id := range find.MemoIDList {\n\t\t\targs = append(args, id)\n\t\t}\n\t\twhere = append(where, fmt.Sprintf(\"(memo_id IN (%s) OR related_memo_id IN (%s))\", inClause, inClause))\n\t}\n\tif find.MemoFilter != nil {\n\t\tengine, err := filter.DefaultEngine()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tstmt, err := engine.CompileToStatement(ctx, *find.MemoFilter, filter.RenderOptions{Dialect: filter.DialectSQLite})\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif stmt.SQL != \"\" {\n\t\t\twhere = append(where, fmt.Sprintf(\"memo_id IN (SELECT id FROM memo WHERE %s)\", stmt.SQL))\n\t\t\twhere = append(where, fmt.Sprintf(\"related_memo_id IN (SELECT id FROM memo WHERE %s)\", stmt.SQL))\n\t\t\targs = append(args, append(stmt.Args, stmt.Args...)...)\n\t\t}\n\t}\n\n\trows, err := d.db.QueryContext(ctx, `\n\t\tSELECT\n\t\t\tmemo_id,\n\t\t\trelated_memo_id,\n\t\t\ttype\n\t\tFROM memo_relation\n\t\tWHERE `+strings.Join(where, \" AND \"), args...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\n\tlist := []*store.MemoRelation{}\n\tfor rows.Next() {\n\t\tmemoRelation := &store.MemoRelation{}\n\t\tif err := rows.Scan(\n\t\t\t&memoRelation.MemoID,\n\t\t\t&memoRelation.RelatedMemoID,\n\t\t\t&memoRelation.Type,\n\t\t); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tlist = append(list, memoRelation)\n\t}\n\n\tif err := rows.Err(); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn list, nil\n}\n\nfunc (d *DB) DeleteMemoRelation(ctx context.Context, delete *store.DeleteMemoRelation) error {\n\twhere, args := []string{\"TRUE\"}, []any{}\n\tif delete.MemoID != nil {\n\t\twhere, args = append(where, \"memo_id = ?\"), append(args, delete.MemoID)\n\t}\n\tif delete.RelatedMemoID != nil {\n\t\twhere, args = append(where, \"related_memo_id = ?\"), append(args, delete.RelatedMemoID)\n\t}\n\tif delete.Type != nil {\n\t\twhere, args = append(where, \"type = ?\"), append(args, delete.Type)\n\t}\n\tstmt := `\n\t\tDELETE FROM memo_relation\n\t\tWHERE ` + strings.Join(where, \" AND \")\n\tresult, err := d.db.ExecContext(ctx, stmt, args...)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif _, err = result.RowsAffected(); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "store/db/sqlite/memo_share.go",
    "content": "package sqlite\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"errors\"\n\t\"strings\"\n\n\t\"github.com/usememos/memos/store\"\n)\n\nfunc (d *DB) CreateMemoShare(ctx context.Context, create *store.MemoShare) (*store.MemoShare, error) {\n\tfields := []string{\"`uid`\", \"`memo_id`\", \"`creator_id`\"}\n\tplaceholders := []string{\"?\", \"?\", \"?\"}\n\targs := []any{create.UID, create.MemoID, create.CreatorID}\n\n\tif create.ExpiresTs != nil {\n\t\tfields = append(fields, \"`expires_ts`\")\n\t\tplaceholders = append(placeholders, \"?\")\n\t\targs = append(args, *create.ExpiresTs)\n\t}\n\n\tstmt := \"INSERT INTO `memo_share` (\" + strings.Join(fields, \", \") + \") VALUES (\" + strings.Join(placeholders, \", \") + \") RETURNING `id`, `created_ts`\"\n\tif err := d.db.QueryRowContext(ctx, stmt, args...).Scan(\n\t\t&create.ID,\n\t\t&create.CreatedTs,\n\t); err != nil {\n\t\treturn nil, err\n\t}\n\treturn create, nil\n}\n\nfunc (d *DB) ListMemoShares(ctx context.Context, find *store.FindMemoShare) ([]*store.MemoShare, error) {\n\twhere, args := []string{\"1 = 1\"}, []any{}\n\n\tif find.ID != nil {\n\t\twhere, args = append(where, \"`id` = ?\"), append(args, *find.ID)\n\t}\n\tif find.UID != nil {\n\t\twhere, args = append(where, \"`uid` = ?\"), append(args, *find.UID)\n\t}\n\tif find.MemoID != nil {\n\t\twhere, args = append(where, \"`memo_id` = ?\"), append(args, *find.MemoID)\n\t}\n\n\trows, err := d.db.QueryContext(ctx, `\n\t\tSELECT\n\t\t\tid,\n\t\t\tuid,\n\t\t\tmemo_id,\n\t\t\tcreator_id,\n\t\t\tcreated_ts,\n\t\t\texpires_ts\n\t\tFROM memo_share\n\t\tWHERE `+strings.Join(where, \" AND \")+`\n\t\tORDER BY id ASC`,\n\t\targs...,\n\t)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\n\tlist := []*store.MemoShare{}\n\tfor rows.Next() {\n\t\tms := &store.MemoShare{}\n\t\tif err := rows.Scan(\n\t\t\t&ms.ID,\n\t\t\t&ms.UID,\n\t\t\t&ms.MemoID,\n\t\t\t&ms.CreatorID,\n\t\t\t&ms.CreatedTs,\n\t\t\t&ms.ExpiresTs,\n\t\t); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tlist = append(list, ms)\n\t}\n\n\tif err := rows.Err(); err != nil {\n\t\treturn nil, err\n\t}\n\treturn list, nil\n}\n\nfunc (d *DB) GetMemoShare(ctx context.Context, find *store.FindMemoShare) (*store.MemoShare, error) {\n\twhere, args := []string{\"1 = 1\"}, []any{}\n\n\tif find.ID != nil {\n\t\twhere, args = append(where, \"`id` = ?\"), append(args, *find.ID)\n\t}\n\tif find.UID != nil {\n\t\twhere, args = append(where, \"`uid` = ?\"), append(args, *find.UID)\n\t}\n\tif find.MemoID != nil {\n\t\twhere, args = append(where, \"`memo_id` = ?\"), append(args, *find.MemoID)\n\t}\n\n\tms := &store.MemoShare{}\n\tif err := d.db.QueryRowContext(ctx, `\n\t\tSELECT\n\t\t\tid,\n\t\t\tuid,\n\t\t\tmemo_id,\n\t\t\tcreator_id,\n\t\t\tcreated_ts,\n\t\t\texpires_ts\n\t\tFROM memo_share\n\t\tWHERE `+strings.Join(where, \" AND \")+`\n\t\tLIMIT 1`,\n\t\targs...,\n\t).Scan(\n\t\t&ms.ID,\n\t\t&ms.UID,\n\t\t&ms.MemoID,\n\t\t&ms.CreatorID,\n\t\t&ms.CreatedTs,\n\t\t&ms.ExpiresTs,\n\t); err != nil {\n\t\tif errors.Is(err, sql.ErrNoRows) {\n\t\t\treturn nil, nil\n\t\t}\n\t\treturn nil, err\n\t}\n\treturn ms, nil\n}\n\nfunc (d *DB) DeleteMemoShare(ctx context.Context, delete *store.DeleteMemoShare) error {\n\twhere, args := []string{\"1 = 1\"}, []any{}\n\tif delete.ID != nil {\n\t\twhere, args = append(where, \"`id` = ?\"), append(args, *delete.ID)\n\t}\n\tif delete.UID != nil {\n\t\twhere, args = append(where, \"`uid` = ?\"), append(args, *delete.UID)\n\t}\n\t_, err := d.db.ExecContext(ctx, \"DELETE FROM `memo_share` WHERE \"+strings.Join(where, \" AND \"), args...)\n\treturn err\n}\n"
  },
  {
    "path": "store/db/sqlite/reaction.go",
    "content": "package sqlite\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"errors\"\n\t\"strings\"\n\n\t\"github.com/usememos/memos/store\"\n)\n\nfunc (d *DB) UpsertReaction(ctx context.Context, upsert *store.Reaction) (*store.Reaction, error) {\n\tfields := []string{\"`creator_id`\", \"`content_id`\", \"`reaction_type`\"}\n\tplaceholder := []string{\"?\", \"?\", \"?\"}\n\targs := []interface{}{upsert.CreatorID, upsert.ContentID, upsert.ReactionType}\n\tstmt := \"INSERT INTO `reaction` (\" + strings.Join(fields, \", \") + \") VALUES (\" + strings.Join(placeholder, \", \") + \") RETURNING `id`, `created_ts`\"\n\tif err := d.db.QueryRowContext(ctx, stmt, args...).Scan(\n\t\t&upsert.ID,\n\t\t&upsert.CreatedTs,\n\t); err != nil {\n\t\treturn nil, err\n\t}\n\n\treaction := upsert\n\treturn reaction, nil\n}\n\nfunc (d *DB) ListReactions(ctx context.Context, find *store.FindReaction) ([]*store.Reaction, error) {\n\twhere, args := []string{\"1 = 1\"}, []any{}\n\n\tif find.ID != nil {\n\t\twhere, args = append(where, \"id = ?\"), append(args, *find.ID)\n\t}\n\tif find.CreatorID != nil {\n\t\twhere, args = append(where, \"creator_id = ?\"), append(args, *find.CreatorID)\n\t}\n\tif find.ContentID != nil {\n\t\twhere, args = append(where, \"content_id = ?\"), append(args, *find.ContentID)\n\t}\n\tif len(find.ContentIDList) > 0 {\n\t\tplaceholders := make([]string, 0, len(find.ContentIDList))\n\t\tfor range find.ContentIDList {\n\t\t\tplaceholders = append(placeholders, \"?\")\n\t\t}\n\t\tif len(placeholders) > 0 {\n\t\t\twhere = append(where, \"content_id IN (\"+strings.Join(placeholders, \",\")+\")\")\n\t\t\tfor _, id := range find.ContentIDList {\n\t\t\t\targs = append(args, id)\n\t\t\t}\n\t\t}\n\t}\n\n\trows, err := d.db.QueryContext(ctx, `\n\t\tSELECT\n\t\t\tid,\n\t\t\tcreated_ts,\n\t\t\tcreator_id,\n\t\t\tcontent_id,\n\t\t\treaction_type\n\t\tFROM reaction\n\t\tWHERE `+strings.Join(where, \" AND \")+`\n\t\tORDER BY id ASC`,\n\t\targs...,\n\t)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\n\tlist := []*store.Reaction{}\n\tfor rows.Next() {\n\t\treaction := &store.Reaction{}\n\t\tif err := rows.Scan(\n\t\t\t&reaction.ID,\n\t\t\t&reaction.CreatedTs,\n\t\t\t&reaction.CreatorID,\n\t\t\t&reaction.ContentID,\n\t\t\t&reaction.ReactionType,\n\t\t); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tlist = append(list, reaction)\n\t}\n\n\tif err := rows.Err(); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn list, nil\n}\n\nfunc (d *DB) GetReaction(ctx context.Context, find *store.FindReaction) (*store.Reaction, error) {\n\twhere, args := []string{\"1 = 1\"}, []any{}\n\n\tif find.ID != nil {\n\t\twhere, args = append(where, \"id = ?\"), append(args, *find.ID)\n\t}\n\tif find.CreatorID != nil {\n\t\twhere, args = append(where, \"creator_id = ?\"), append(args, *find.CreatorID)\n\t}\n\tif find.ContentID != nil {\n\t\twhere, args = append(where, \"content_id = ?\"), append(args, *find.ContentID)\n\t}\n\n\treaction := &store.Reaction{}\n\tif err := d.db.QueryRowContext(ctx, `\n\t\tSELECT\n\t\t\tid,\n\t\t\tcreated_ts,\n\t\t\tcreator_id,\n\t\t\tcontent_id,\n\t\t\treaction_type\n\t\tFROM reaction\n\t\tWHERE `+strings.Join(where, \" AND \")+`\n\t\tLIMIT 1`,\n\t\targs...,\n\t).Scan(\n\t\t&reaction.ID,\n\t\t&reaction.CreatedTs,\n\t\t&reaction.CreatorID,\n\t\t&reaction.ContentID,\n\t\t&reaction.ReactionType,\n\t); err != nil {\n\t\tif errors.Is(err, sql.ErrNoRows) {\n\t\t\treturn nil, nil\n\t\t}\n\t\treturn nil, err\n\t}\n\n\treturn reaction, nil\n}\n\nfunc (d *DB) DeleteReaction(ctx context.Context, delete *store.DeleteReaction) error {\n\t_, err := d.db.ExecContext(ctx, \"DELETE FROM `reaction` WHERE `id` = ?\", delete.ID)\n\treturn err\n}\n"
  },
  {
    "path": "store/db/sqlite/sqlite.go",
    "content": "package sqlite\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\n\t\"github.com/pkg/errors\"\n\n\t// Note: modernc.org/sqlite driver is imported in functions.go where\n\t// RegisterScalarFunction is used. No blank import needed here.\n\n\t\"github.com/usememos/memos/internal/profile\"\n\t\"github.com/usememos/memos/store\"\n)\n\ntype DB struct {\n\tdb      *sql.DB\n\tprofile *profile.Profile\n}\n\n// NewDB opens a database specified by its database driver name and a\n// driver-specific data source name, usually consisting of at least a\n// database name and connection information.\nfunc NewDB(profile *profile.Profile) (store.Driver, error) {\n\t// Ensure a DSN is set before attempting to open the database.\n\tif profile.DSN == \"\" {\n\t\treturn nil, errors.New(\"dsn required\")\n\t}\n\n\tif err := ensureUnicodeLowerRegistered(); err != nil {\n\t\treturn nil, errors.Wrap(err, \"failed to register sqlite unicode lower function\")\n\t}\n\n\t// Connect to the database with some sane settings:\n\t// - No shared-cache: it's obsolete; WAL journal mode is a better solution.\n\t// - No foreign key constraints: it's currently disabled by default, but it's a\n\t// good practice to be explicit and prevent future surprises on SQLite upgrades.\n\t// - Journal mode set to WAL: it's the recommended journal mode for most applications\n\t// as it prevents locking issues.\n\t// - mmap size set to 0: it disables memory mapping, which can cause OOM errors on some systems.\n\t//\n\t// Notes:\n\t// - When using the `modernc.org/sqlite` driver, each pragma must be prefixed with `_pragma=`.\n\t//\n\t// References:\n\t// - https://pkg.go.dev/modernc.org/sqlite#Driver.Open\n\t// - https://www.sqlite.org/sharedcache.html\n\t// - https://www.sqlite.org/pragma.html\n\tsqliteDB, err := sql.Open(\"sqlite\", profile.DSN+\"?_pragma=foreign_keys(0)&_pragma=busy_timeout(10000)&_pragma=journal_mode(WAL)&_pragma=mmap_size(0)\")\n\tif err != nil {\n\t\treturn nil, errors.Wrapf(err, \"failed to open db with dsn: %s\", profile.DSN)\n\t}\n\n\tdriver := DB{db: sqliteDB, profile: profile}\n\n\treturn &driver, nil\n}\n\nfunc (d *DB) GetDB() *sql.DB {\n\treturn d.db\n}\n\nfunc (d *DB) Close() error {\n\treturn d.db.Close()\n}\n\nfunc (d *DB) IsInitialized(ctx context.Context) (bool, error) {\n\t// Check if the database is initialized by checking if the memo table exists.\n\tvar exists bool\n\terr := d.db.QueryRowContext(ctx, \"SELECT EXISTS(SELECT 1 FROM sqlite_master WHERE type='table' AND name='memo')\").Scan(&exists)\n\tif err != nil {\n\t\treturn false, errors.Wrap(err, \"failed to check if database is initialized\")\n\t}\n\treturn exists, nil\n}\n"
  },
  {
    "path": "store/db/sqlite/user.go",
    "content": "package sqlite\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/pkg/errors\"\n\n\t\"github.com/usememos/memos/store\"\n)\n\nfunc (d *DB) CreateUser(ctx context.Context, create *store.User) (*store.User, error) {\n\tfields := []string{\"`username`\", \"`role`\", \"`email`\", \"`nickname`\", \"`password_hash`, `avatar_url`\"}\n\tplaceholder := []string{\"?\", \"?\", \"?\", \"?\", \"?\", \"?\"}\n\targs := []any{create.Username, create.Role, create.Email, create.Nickname, create.PasswordHash, create.AvatarURL}\n\tstmt := \"INSERT INTO user (\" + strings.Join(fields, \", \") + \") VALUES (\" + strings.Join(placeholder, \", \") + \") RETURNING id, description, created_ts, updated_ts, row_status\"\n\tif err := d.db.QueryRowContext(ctx, stmt, args...).Scan(\n\t\t&create.ID,\n\t\t&create.Description,\n\t\t&create.CreatedTs,\n\t\t&create.UpdatedTs,\n\t\t&create.RowStatus,\n\t); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn create, nil\n}\n\nfunc (d *DB) UpdateUser(ctx context.Context, update *store.UpdateUser) (*store.User, error) {\n\tset, args := []string{}, []any{}\n\tif v := update.UpdatedTs; v != nil {\n\t\tset, args = append(set, \"updated_ts = ?\"), append(args, *v)\n\t}\n\tif v := update.RowStatus; v != nil {\n\t\tset, args = append(set, \"row_status = ?\"), append(args, *v)\n\t}\n\tif v := update.Username; v != nil {\n\t\tset, args = append(set, \"username = ?\"), append(args, *v)\n\t}\n\tif v := update.Email; v != nil {\n\t\tset, args = append(set, \"email = ?\"), append(args, *v)\n\t}\n\tif v := update.Nickname; v != nil {\n\t\tset, args = append(set, \"nickname = ?\"), append(args, *v)\n\t}\n\tif v := update.AvatarURL; v != nil {\n\t\tset, args = append(set, \"avatar_url = ?\"), append(args, *v)\n\t}\n\tif v := update.PasswordHash; v != nil {\n\t\tset, args = append(set, \"password_hash = ?\"), append(args, *v)\n\t}\n\tif v := update.Description; v != nil {\n\t\tset, args = append(set, \"description = ?\"), append(args, *v)\n\t}\n\tif v := update.Role; v != nil {\n\t\tset, args = append(set, \"role = ?\"), append(args, *v)\n\t}\n\targs = append(args, update.ID)\n\n\tquery := `\n\t\tUPDATE user\n\t\tSET ` + strings.Join(set, \", \") + `\n\t\tWHERE id = ?\n\t\tRETURNING id, username, role, email, nickname, password_hash, avatar_url, description, created_ts, updated_ts, row_status\n\t`\n\tuser := &store.User{}\n\tif err := d.db.QueryRowContext(ctx, query, args...).Scan(\n\t\t&user.ID,\n\t\t&user.Username,\n\t\t&user.Role,\n\t\t&user.Email,\n\t\t&user.Nickname,\n\t\t&user.PasswordHash,\n\t\t&user.AvatarURL,\n\t\t&user.Description,\n\t\t&user.CreatedTs,\n\t\t&user.UpdatedTs,\n\t\t&user.RowStatus,\n\t); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn user, nil\n}\n\nfunc (d *DB) ListUsers(ctx context.Context, find *store.FindUser) ([]*store.User, error) {\n\twhere, args := []string{\"1 = 1\"}, []any{}\n\n\tif len(find.Filters) > 0 {\n\t\treturn nil, errors.Errorf(\"user filters are not supported\")\n\t}\n\n\tif v := find.ID; v != nil {\n\t\twhere, args = append(where, \"id = ?\"), append(args, *v)\n\t}\n\tif v := find.Username; v != nil {\n\t\twhere, args = append(where, \"username = ?\"), append(args, *v)\n\t}\n\tif v := find.Role; v != nil {\n\t\twhere, args = append(where, \"role = ?\"), append(args, *v)\n\t}\n\tif v := find.Email; v != nil {\n\t\twhere, args = append(where, \"email = ?\"), append(args, *v)\n\t}\n\tif v := find.Nickname; v != nil {\n\t\twhere, args = append(where, \"nickname = ?\"), append(args, *v)\n\t}\n\n\torderBy := []string{\"created_ts DESC\", \"row_status DESC\"}\n\tquery := `\n\t\tSELECT \n\t\t\tid,\n\t\t\tusername,\n\t\t\trole,\n\t\t\temail,\n\t\t\tnickname,\n\t\t\tpassword_hash,\n\t\t\tavatar_url,\n\t\t\tdescription,\n\t\t\tcreated_ts,\n\t\t\tupdated_ts,\n\t\t\trow_status\n\t\tFROM user\n\t\tWHERE ` + strings.Join(where, \" AND \") + ` ORDER BY ` + strings.Join(orderBy, \", \")\n\tif v := find.Limit; v != nil {\n\t\tquery += fmt.Sprintf(\" LIMIT %d\", *v)\n\t}\n\n\trows, err := d.db.QueryContext(ctx, query, args...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\n\tlist := make([]*store.User, 0)\n\tfor rows.Next() {\n\t\tvar user store.User\n\t\tif err := rows.Scan(\n\t\t\t&user.ID,\n\t\t\t&user.Username,\n\t\t\t&user.Role,\n\t\t\t&user.Email,\n\t\t\t&user.Nickname,\n\t\t\t&user.PasswordHash,\n\t\t\t&user.AvatarURL,\n\t\t\t&user.Description,\n\t\t\t&user.CreatedTs,\n\t\t\t&user.UpdatedTs,\n\t\t\t&user.RowStatus,\n\t\t); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tlist = append(list, &user)\n\t}\n\n\tif err := rows.Err(); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn list, nil\n}\n\nfunc (d *DB) DeleteUser(ctx context.Context, delete *store.DeleteUser) error {\n\tresult, err := d.db.ExecContext(ctx, `\n\t\tDELETE FROM user WHERE id = ?\n\t`, delete.ID)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif _, err := result.RowsAffected(); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "store/db/sqlite/user_setting.go",
    "content": "package sqlite\n\nimport (\n\t\"context\"\n\t\"strings\"\n\n\t\"github.com/pkg/errors\"\n\n\tstorepb \"github.com/usememos/memos/proto/gen/store\"\n\t\"github.com/usememos/memos/store\"\n)\n\nfunc (d *DB) UpsertUserSetting(ctx context.Context, upsert *store.UserSetting) (*store.UserSetting, error) {\n\tstmt := `\n\t\tINSERT INTO user_setting (\n\t\t\tuser_id, key, value\n\t\t)\n\t\tVALUES (?, ?, ?)\n\t\tON CONFLICT(user_id, key) DO UPDATE \n\t\tSET value = EXCLUDED.value\n\t`\n\tif _, err := d.db.ExecContext(ctx, stmt, upsert.UserID, upsert.Key.String(), upsert.Value); err != nil {\n\t\treturn nil, err\n\t}\n\treturn upsert, nil\n}\n\nfunc (d *DB) ListUserSettings(ctx context.Context, find *store.FindUserSetting) ([]*store.UserSetting, error) {\n\twhere, args := []string{\"1 = 1\"}, []any{}\n\n\tif v := find.Key; v != storepb.UserSetting_KEY_UNSPECIFIED {\n\t\twhere, args = append(where, \"key = ?\"), append(args, v.String())\n\t}\n\tif v := find.UserID; v != nil {\n\t\twhere, args = append(where, \"user_id = ?\"), append(args, *find.UserID)\n\t}\n\n\tquery := `\n\t\tSELECT\n\t\t\tuser_id,\n\t\t  key,\n\t\t\tvalue\n\t\tFROM user_setting\n\t\tWHERE ` + strings.Join(where, \" AND \")\n\trows, err := d.db.QueryContext(ctx, query, args...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\n\tuserSettingList := make([]*store.UserSetting, 0)\n\tfor rows.Next() {\n\t\tuserSetting := &store.UserSetting{}\n\t\tvar keyString string\n\t\tif err := rows.Scan(\n\t\t\t&userSetting.UserID,\n\t\t\t&keyString,\n\t\t\t&userSetting.Value,\n\t\t); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tuserSetting.Key = storepb.UserSetting_Key(storepb.UserSetting_Key_value[keyString])\n\t\tuserSettingList = append(userSettingList, userSetting)\n\t}\n\tif err := rows.Err(); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn userSettingList, nil\n}\n\nfunc (d *DB) GetUserByPATHash(ctx context.Context, tokenHash string) (*store.PATQueryResult, error) {\n\tquery := `\n\t\tSELECT\n\t\t\tuser_setting.user_id,\n\t\t\tuser_setting.value\n\t\tFROM user_setting\n\t\tWHERE user_setting.key = 'PERSONAL_ACCESS_TOKENS'\n\t\t\tAND EXISTS (\n\t\t\t\tSELECT 1\n\t\t\t\tFROM json_each(json_extract(user_setting.value, '$.tokens')) AS token\n\t\t\t\tWHERE json_extract(token.value, '$.tokenHash') = ?\n\t\t\t)\n\t`\n\n\tvar userID int32\n\tvar tokensJSON string\n\n\terr := d.db.QueryRowContext(ctx, query, tokenHash).Scan(&userID, &tokensJSON)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tpatsUserSetting := &storepb.PersonalAccessTokensUserSetting{}\n\tif err := protojsonUnmarshaler.Unmarshal([]byte(tokensJSON), patsUserSetting); err != nil {\n\t\treturn nil, err\n\t}\n\n\tfor _, pat := range patsUserSetting.Tokens {\n\t\tif pat.TokenHash == tokenHash {\n\t\t\treturn &store.PATQueryResult{\n\t\t\t\tUserID: userID,\n\t\t\t\tPAT:    pat,\n\t\t\t}, nil\n\t\t}\n\t}\n\n\treturn nil, errors.New(\"PAT not found\")\n}\n"
  },
  {
    "path": "store/driver.go",
    "content": "package store\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n)\n\n// Driver is an interface for store driver.\n// It contains all methods that store database driver should implement.\ntype Driver interface {\n\tGetDB() *sql.DB\n\tClose() error\n\n\tIsInitialized(ctx context.Context) (bool, error)\n\n\t// Attachment model related methods.\n\tCreateAttachment(ctx context.Context, create *Attachment) (*Attachment, error)\n\tListAttachments(ctx context.Context, find *FindAttachment) ([]*Attachment, error)\n\tUpdateAttachment(ctx context.Context, update *UpdateAttachment) error\n\tDeleteAttachment(ctx context.Context, delete *DeleteAttachment) error\n\n\t// Memo model related methods.\n\tCreateMemo(ctx context.Context, create *Memo) (*Memo, error)\n\tListMemos(ctx context.Context, find *FindMemo) ([]*Memo, error)\n\tUpdateMemo(ctx context.Context, update *UpdateMemo) error\n\tDeleteMemo(ctx context.Context, delete *DeleteMemo) error\n\n\t// MemoRelation model related methods.\n\tUpsertMemoRelation(ctx context.Context, create *MemoRelation) (*MemoRelation, error)\n\tListMemoRelations(ctx context.Context, find *FindMemoRelation) ([]*MemoRelation, error)\n\tDeleteMemoRelation(ctx context.Context, delete *DeleteMemoRelation) error\n\n\t// InstanceSetting model related methods.\n\tUpsertInstanceSetting(ctx context.Context, upsert *InstanceSetting) (*InstanceSetting, error)\n\tListInstanceSettings(ctx context.Context, find *FindInstanceSetting) ([]*InstanceSetting, error)\n\tDeleteInstanceSetting(ctx context.Context, delete *DeleteInstanceSetting) error\n\n\t// User model related methods.\n\tCreateUser(ctx context.Context, create *User) (*User, error)\n\tUpdateUser(ctx context.Context, update *UpdateUser) (*User, error)\n\tListUsers(ctx context.Context, find *FindUser) ([]*User, error)\n\tDeleteUser(ctx context.Context, delete *DeleteUser) error\n\n\t// UserSetting model related methods.\n\tUpsertUserSetting(ctx context.Context, upsert *UserSetting) (*UserSetting, error)\n\tListUserSettings(ctx context.Context, find *FindUserSetting) ([]*UserSetting, error)\n\tGetUserByPATHash(ctx context.Context, tokenHash string) (*PATQueryResult, error)\n\n\t// IdentityProvider model related methods.\n\tCreateIdentityProvider(ctx context.Context, create *IdentityProvider) (*IdentityProvider, error)\n\tListIdentityProviders(ctx context.Context, find *FindIdentityProvider) ([]*IdentityProvider, error)\n\tUpdateIdentityProvider(ctx context.Context, update *UpdateIdentityProvider) (*IdentityProvider, error)\n\tDeleteIdentityProvider(ctx context.Context, delete *DeleteIdentityProvider) error\n\n\t// Inbox model related methods.\n\tCreateInbox(ctx context.Context, create *Inbox) (*Inbox, error)\n\tListInboxes(ctx context.Context, find *FindInbox) ([]*Inbox, error)\n\tUpdateInbox(ctx context.Context, update *UpdateInbox) (*Inbox, error)\n\tDeleteInbox(ctx context.Context, delete *DeleteInbox) error\n\n\t// Reaction model related methods.\n\tUpsertReaction(ctx context.Context, create *Reaction) (*Reaction, error)\n\tListReactions(ctx context.Context, find *FindReaction) ([]*Reaction, error)\n\tGetReaction(ctx context.Context, find *FindReaction) (*Reaction, error)\n\tDeleteReaction(ctx context.Context, delete *DeleteReaction) error\n\n\t// MemoShare model related methods.\n\tCreateMemoShare(ctx context.Context, create *MemoShare) (*MemoShare, error)\n\tListMemoShares(ctx context.Context, find *FindMemoShare) ([]*MemoShare, error)\n\tGetMemoShare(ctx context.Context, find *FindMemoShare) (*MemoShare, error)\n\tDeleteMemoShare(ctx context.Context, delete *DeleteMemoShare) error\n}\n"
  },
  {
    "path": "store/idp.go",
    "content": "package store\n\nimport (\n\t\"context\"\n\n\t\"github.com/pkg/errors\"\n\t\"google.golang.org/protobuf/encoding/protojson\"\n\n\tstorepb \"github.com/usememos/memos/proto/gen/store\"\n)\n\ntype IdentityProvider struct {\n\tID               int32\n\tUID              string\n\tName             string\n\tType             storepb.IdentityProvider_Type\n\tIdentifierFilter string\n\tConfig           string\n}\n\ntype FindIdentityProvider struct {\n\tID  *int32\n\tUID *string\n}\n\ntype UpdateIdentityProvider struct {\n\tID               int32\n\tName             *string\n\tIdentifierFilter *string\n\tConfig           *string\n}\n\ntype DeleteIdentityProvider struct {\n\tID int32\n}\n\nfunc (s *Store) CreateIdentityProvider(ctx context.Context, create *storepb.IdentityProvider) (*storepb.IdentityProvider, error) {\n\traw, err := convertIdentityProviderToRaw(create)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tidentityProviderRaw, err := s.driver.CreateIdentityProvider(ctx, raw)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tidentityProvider, err := convertIdentityProviderFromRaw(identityProviderRaw)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn identityProvider, nil\n}\n\nfunc (s *Store) ListIdentityProviders(ctx context.Context, find *FindIdentityProvider) ([]*storepb.IdentityProvider, error) {\n\tlist, err := s.driver.ListIdentityProviders(ctx, find)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tidentityProviders := []*storepb.IdentityProvider{}\n\tfor _, raw := range list {\n\t\tidentityProvider, err := convertIdentityProviderFromRaw(raw)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tidentityProviders = append(identityProviders, identityProvider)\n\t}\n\treturn identityProviders, nil\n}\n\nfunc (s *Store) GetIdentityProvider(ctx context.Context, find *FindIdentityProvider) (*storepb.IdentityProvider, error) {\n\tlist, err := s.ListIdentityProviders(ctx, find)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif len(list) == 0 {\n\t\treturn nil, nil\n\t}\n\tif len(list) > 1 {\n\t\treturn nil, errors.Errorf(\"Found multiple identity providers with ID %d\", *find.ID)\n\t}\n\n\tidentityProvider := list[0]\n\treturn identityProvider, nil\n}\n\ntype UpdateIdentityProviderV1 struct {\n\tID               int32\n\tType             storepb.IdentityProvider_Type\n\tName             *string\n\tIdentifierFilter *string\n\tConfig           *storepb.IdentityProviderConfig\n}\n\nfunc (s *Store) UpdateIdentityProvider(ctx context.Context, update *UpdateIdentityProviderV1) (*storepb.IdentityProvider, error) {\n\tupdateRaw := &UpdateIdentityProvider{\n\t\tID: update.ID,\n\t}\n\tif update.Name != nil {\n\t\tupdateRaw.Name = update.Name\n\t}\n\tif update.IdentifierFilter != nil {\n\t\tupdateRaw.IdentifierFilter = update.IdentifierFilter\n\t}\n\tif update.Config != nil {\n\t\tconfigRaw, err := convertIdentityProviderConfigToRaw(update.Type, update.Config)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tupdateRaw.Config = &configRaw\n\t}\n\tidentityProviderRaw, err := s.driver.UpdateIdentityProvider(ctx, updateRaw)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tidentityProvider, err := convertIdentityProviderFromRaw(identityProviderRaw)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn identityProvider, nil\n}\n\nfunc (s *Store) DeleteIdentityProvider(ctx context.Context, delete *DeleteIdentityProvider) error {\n\terr := s.driver.DeleteIdentityProvider(ctx, delete)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc convertIdentityProviderFromRaw(raw *IdentityProvider) (*storepb.IdentityProvider, error) {\n\tidentityProvider := &storepb.IdentityProvider{\n\t\tId:               raw.ID,\n\t\tUid:              raw.UID,\n\t\tName:             raw.Name,\n\t\tType:             raw.Type,\n\t\tIdentifierFilter: raw.IdentifierFilter,\n\t}\n\tconfig, err := convertIdentityProviderConfigFromRaw(identityProvider.Type, raw.Config)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tidentityProvider.Config = config\n\treturn identityProvider, nil\n}\n\nfunc convertIdentityProviderToRaw(identityProvider *storepb.IdentityProvider) (*IdentityProvider, error) {\n\traw := &IdentityProvider{\n\t\tID:               identityProvider.Id,\n\t\tUID:              identityProvider.Uid,\n\t\tName:             identityProvider.Name,\n\t\tType:             identityProvider.Type,\n\t\tIdentifierFilter: identityProvider.IdentifierFilter,\n\t}\n\tconfigRaw, err := convertIdentityProviderConfigToRaw(identityProvider.Type, identityProvider.Config)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\traw.Config = configRaw\n\treturn raw, nil\n}\n\nfunc convertIdentityProviderConfigFromRaw(identityProviderType storepb.IdentityProvider_Type, raw string) (*storepb.IdentityProviderConfig, error) {\n\tconfig := &storepb.IdentityProviderConfig{}\n\tif identityProviderType == storepb.IdentityProvider_OAUTH2 {\n\t\toauth2Config := &storepb.OAuth2Config{}\n\t\tif err := protojsonUnmarshaler.Unmarshal([]byte(raw), oauth2Config); err != nil {\n\t\t\treturn nil, errors.Wrap(err, \"Failed to unmarshal OAuth2Config\")\n\t\t}\n\t\tconfig.Config = &storepb.IdentityProviderConfig_Oauth2Config{Oauth2Config: oauth2Config}\n\t}\n\treturn config, nil\n}\n\nfunc convertIdentityProviderConfigToRaw(identityProviderType storepb.IdentityProvider_Type, config *storepb.IdentityProviderConfig) (string, error) {\n\traw := \"\"\n\tif identityProviderType == storepb.IdentityProvider_OAUTH2 {\n\t\tbytes, err := protojson.Marshal(config.GetOauth2Config())\n\t\tif err != nil {\n\t\t\treturn \"\", errors.Wrap(err, \"Failed to marshal OAuth2Config\")\n\t\t}\n\t\traw = string(bytes)\n\t}\n\treturn raw, nil\n}\n"
  },
  {
    "path": "store/inbox.go",
    "content": "package store\n\nimport (\n\t\"context\"\n\n\tstorepb \"github.com/usememos/memos/proto/gen/store\"\n)\n\n// InboxStatus represents the status of an inbox notification.\ntype InboxStatus string\n\nconst (\n\t// UNREAD indicates the notification has not been read by the user.\n\tUNREAD InboxStatus = \"UNREAD\"\n\t// ARCHIVED indicates the notification has been archived/dismissed by the user.\n\tARCHIVED InboxStatus = \"ARCHIVED\"\n)\n\nfunc (s InboxStatus) String() string {\n\treturn string(s)\n}\n\n// Inbox represents a notification in a user's inbox.\ntype Inbox struct {\n\tID         int32\n\tCreatedTs  int64\n\tSenderID   int32                 // The user who triggered the notification\n\tReceiverID int32                 // The user who receives the notification\n\tStatus     InboxStatus           // Current status (unread/archived)\n\tMessage    *storepb.InboxMessage // The notification message content\n}\n\n// UpdateInbox contains fields that can be updated for an inbox item.\ntype UpdateInbox struct {\n\tID     int32\n\tStatus InboxStatus\n}\n\n// FindInbox specifies filter criteria for querying inbox items.\ntype FindInbox struct {\n\tID          *int32\n\tSenderID    *int32\n\tReceiverID  *int32\n\tStatus      *InboxStatus\n\tMessageType *storepb.InboxMessage_Type\n\n\t// Pagination\n\tLimit  *int\n\tOffset *int\n}\n\n// DeleteInbox specifies which inbox item to delete.\ntype DeleteInbox struct {\n\tID int32\n}\n\n// CreateInbox creates a new inbox notification.\nfunc (s *Store) CreateInbox(ctx context.Context, create *Inbox) (*Inbox, error) {\n\treturn s.driver.CreateInbox(ctx, create)\n}\n\n// ListInboxes retrieves inbox items matching the filter criteria.\nfunc (s *Store) ListInboxes(ctx context.Context, find *FindInbox) ([]*Inbox, error) {\n\treturn s.driver.ListInboxes(ctx, find)\n}\n\n// UpdateInbox updates an existing inbox item.\nfunc (s *Store) UpdateInbox(ctx context.Context, update *UpdateInbox) (*Inbox, error) {\n\treturn s.driver.UpdateInbox(ctx, update)\n}\n\n// DeleteInbox permanently removes an inbox item.\nfunc (s *Store) DeleteInbox(ctx context.Context, delete *DeleteInbox) error {\n\treturn s.driver.DeleteInbox(ctx, delete)\n}\n"
  },
  {
    "path": "store/instance_setting.go",
    "content": "package store\n\nimport (\n\t\"context\"\n\n\t\"github.com/pkg/errors\"\n\t\"google.golang.org/protobuf/encoding/protojson\"\n\n\tstorepb \"github.com/usememos/memos/proto/gen/store\"\n)\n\ntype InstanceSetting struct {\n\tName        string\n\tValue       string\n\tDescription string\n}\n\ntype FindInstanceSetting struct {\n\tName string\n}\n\ntype DeleteInstanceSetting struct {\n\tName string\n}\n\nfunc (s *Store) UpsertInstanceSetting(ctx context.Context, upsert *storepb.InstanceSetting) (*storepb.InstanceSetting, error) {\n\tinstanceSettingRaw := &InstanceSetting{\n\t\tName: upsert.Key.String(),\n\t}\n\tvar valueBytes []byte\n\tvar err error\n\tif upsert.Key == storepb.InstanceSettingKey_BASIC {\n\t\tvalueBytes, err = protojson.Marshal(upsert.GetBasicSetting())\n\t} else if upsert.Key == storepb.InstanceSettingKey_GENERAL {\n\t\tvalueBytes, err = protojson.Marshal(upsert.GetGeneralSetting())\n\t} else if upsert.Key == storepb.InstanceSettingKey_STORAGE {\n\t\tvalueBytes, err = protojson.Marshal(upsert.GetStorageSetting())\n\t} else if upsert.Key == storepb.InstanceSettingKey_MEMO_RELATED {\n\t\tvalueBytes, err = protojson.Marshal(upsert.GetMemoRelatedSetting())\n\t} else if upsert.Key == storepb.InstanceSettingKey_TAGS {\n\t\tvalueBytes, err = protojson.Marshal(upsert.GetTagsSetting())\n\t} else if upsert.Key == storepb.InstanceSettingKey_NOTIFICATION {\n\t\tvalueBytes, err = protojson.Marshal(upsert.GetNotificationSetting())\n\t} else {\n\t\treturn nil, errors.Errorf(\"unsupported instance setting key: %v\", upsert.Key)\n\t}\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"failed to marshal instance setting value\")\n\t}\n\tvalueString := string(valueBytes)\n\tinstanceSettingRaw.Value = valueString\n\tinstanceSettingRaw, err = s.driver.UpsertInstanceSetting(ctx, instanceSettingRaw)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"Failed to upsert instance setting\")\n\t}\n\tinstanceSetting, err := convertInstanceSettingFromRaw(instanceSettingRaw)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"Failed to convert instance setting\")\n\t}\n\ts.instanceSettingCache.Set(ctx, instanceSetting.Key.String(), instanceSetting)\n\treturn instanceSetting, nil\n}\n\nfunc (s *Store) ListInstanceSettings(ctx context.Context, find *FindInstanceSetting) ([]*storepb.InstanceSetting, error) {\n\tlist, err := s.driver.ListInstanceSettings(ctx, find)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tinstanceSettings := []*storepb.InstanceSetting{}\n\tfor _, instanceSettingRaw := range list {\n\t\tinstanceSetting, err := convertInstanceSettingFromRaw(instanceSettingRaw)\n\t\tif err != nil {\n\t\t\treturn nil, errors.Wrap(err, \"Failed to convert instance setting\")\n\t\t}\n\t\tif instanceSetting == nil {\n\t\t\tcontinue\n\t\t}\n\t\ts.instanceSettingCache.Set(ctx, instanceSetting.Key.String(), instanceSetting)\n\t\tinstanceSettings = append(instanceSettings, instanceSetting)\n\t}\n\treturn instanceSettings, nil\n}\n\nfunc (s *Store) GetInstanceSetting(ctx context.Context, find *FindInstanceSetting) (*storepb.InstanceSetting, error) {\n\tif cache, ok := s.instanceSettingCache.Get(ctx, find.Name); ok {\n\t\tinstanceSetting, ok := cache.(*storepb.InstanceSetting)\n\t\tif ok {\n\t\t\treturn instanceSetting, nil\n\t\t}\n\t}\n\n\tlist, err := s.ListInstanceSettings(ctx, find)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif len(list) == 0 {\n\t\treturn nil, nil\n\t}\n\tif len(list) > 1 {\n\t\treturn nil, errors.Errorf(\"found multiple instance settings with key %s\", find.Name)\n\t}\n\treturn list[0], nil\n}\n\nfunc (s *Store) GetInstanceBasicSetting(ctx context.Context) (*storepb.InstanceBasicSetting, error) {\n\tinstanceSetting, err := s.GetInstanceSetting(ctx, &FindInstanceSetting{\n\t\tName: storepb.InstanceSettingKey_BASIC.String(),\n\t})\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"failed to get instance basic setting\")\n\t}\n\n\tinstanceBasicSetting := &storepb.InstanceBasicSetting{}\n\tif instanceSetting != nil {\n\t\tinstanceBasicSetting = instanceSetting.GetBasicSetting()\n\t}\n\ts.instanceSettingCache.Set(ctx, storepb.InstanceSettingKey_BASIC.String(), &storepb.InstanceSetting{\n\t\tKey:   storepb.InstanceSettingKey_BASIC,\n\t\tValue: &storepb.InstanceSetting_BasicSetting{BasicSetting: instanceBasicSetting},\n\t})\n\treturn instanceBasicSetting, nil\n}\n\nfunc (s *Store) GetInstanceGeneralSetting(ctx context.Context) (*storepb.InstanceGeneralSetting, error) {\n\tinstanceSetting, err := s.GetInstanceSetting(ctx, &FindInstanceSetting{\n\t\tName: storepb.InstanceSettingKey_GENERAL.String(),\n\t})\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"failed to get instance general setting\")\n\t}\n\n\tinstanceGeneralSetting := &storepb.InstanceGeneralSetting{}\n\tif instanceSetting != nil {\n\t\tinstanceGeneralSetting = instanceSetting.GetGeneralSetting()\n\t}\n\ts.instanceSettingCache.Set(ctx, storepb.InstanceSettingKey_GENERAL.String(), &storepb.InstanceSetting{\n\t\tKey:   storepb.InstanceSettingKey_GENERAL,\n\t\tValue: &storepb.InstanceSetting_GeneralSetting{GeneralSetting: instanceGeneralSetting},\n\t})\n\treturn instanceGeneralSetting, nil\n}\n\n// DefaultContentLengthLimit is the default limit of content length in bytes. 8KB.\nconst DefaultContentLengthLimit = 8 * 1024\n\n// DefaultReactions is the default reactions for memo related setting.\nvar DefaultReactions = []string{\"👍\", \"👎\", \"❤️\", \"🎉\", \"😄\", \"😕\", \"😢\", \"😡\"}\n\nfunc (s *Store) GetInstanceMemoRelatedSetting(ctx context.Context) (*storepb.InstanceMemoRelatedSetting, error) {\n\tinstanceSetting, err := s.GetInstanceSetting(ctx, &FindInstanceSetting{\n\t\tName: storepb.InstanceSettingKey_MEMO_RELATED.String(),\n\t})\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"failed to get instance general setting\")\n\t}\n\n\tinstanceMemoRelatedSetting := &storepb.InstanceMemoRelatedSetting{}\n\tif instanceSetting != nil {\n\t\tinstanceMemoRelatedSetting = instanceSetting.GetMemoRelatedSetting()\n\t}\n\tif instanceMemoRelatedSetting.ContentLengthLimit < DefaultContentLengthLimit {\n\t\tinstanceMemoRelatedSetting.ContentLengthLimit = DefaultContentLengthLimit\n\t}\n\tif len(instanceMemoRelatedSetting.Reactions) == 0 {\n\t\tinstanceMemoRelatedSetting.Reactions = append(instanceMemoRelatedSetting.Reactions, DefaultReactions...)\n\t}\n\ts.instanceSettingCache.Set(ctx, storepb.InstanceSettingKey_MEMO_RELATED.String(), &storepb.InstanceSetting{\n\t\tKey:   storepb.InstanceSettingKey_MEMO_RELATED,\n\t\tValue: &storepb.InstanceSetting_MemoRelatedSetting{MemoRelatedSetting: instanceMemoRelatedSetting},\n\t})\n\treturn instanceMemoRelatedSetting, nil\n}\n\nfunc (s *Store) GetInstanceTagsSetting(ctx context.Context) (*storepb.InstanceTagsSetting, error) {\n\tinstanceSetting, err := s.GetInstanceSetting(ctx, &FindInstanceSetting{\n\t\tName: storepb.InstanceSettingKey_TAGS.String(),\n\t})\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"failed to get instance tags setting\")\n\t}\n\n\tinstanceTagsSetting := &storepb.InstanceTagsSetting{}\n\tif instanceSetting != nil {\n\t\tinstanceTagsSetting = instanceSetting.GetTagsSetting()\n\t}\n\tif instanceTagsSetting.Tags == nil {\n\t\tinstanceTagsSetting.Tags = map[string]*storepb.InstanceTagMetadata{}\n\t}\n\ts.instanceSettingCache.Set(ctx, storepb.InstanceSettingKey_TAGS.String(), &storepb.InstanceSetting{\n\t\tKey:   storepb.InstanceSettingKey_TAGS,\n\t\tValue: &storepb.InstanceSetting_TagsSetting{TagsSetting: instanceTagsSetting},\n\t})\n\treturn instanceTagsSetting, nil\n}\n\nfunc (s *Store) GetInstanceNotificationSetting(ctx context.Context) (*storepb.InstanceNotificationSetting, error) {\n\tinstanceSetting, err := s.GetInstanceSetting(ctx, &FindInstanceSetting{\n\t\tName: storepb.InstanceSettingKey_NOTIFICATION.String(),\n\t})\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"failed to get instance notification setting\")\n\t}\n\n\tinstanceNotificationSetting := &storepb.InstanceNotificationSetting{}\n\tif instanceSetting != nil {\n\t\tinstanceNotificationSetting = instanceSetting.GetNotificationSetting()\n\t}\n\tif instanceNotificationSetting.Email == nil {\n\t\tinstanceNotificationSetting.Email = &storepb.InstanceNotificationSetting_EmailSetting{}\n\t}\n\ts.instanceSettingCache.Set(ctx, storepb.InstanceSettingKey_NOTIFICATION.String(), &storepb.InstanceSetting{\n\t\tKey:   storepb.InstanceSettingKey_NOTIFICATION,\n\t\tValue: &storepb.InstanceSetting_NotificationSetting{NotificationSetting: instanceNotificationSetting},\n\t})\n\treturn instanceNotificationSetting, nil\n}\n\nconst (\n\tdefaultInstanceStorageType       = storepb.InstanceStorageSetting_LOCAL\n\tdefaultInstanceUploadSizeLimitMb = 30\n\tdefaultInstanceFilepathTemplate  = \"assets/{timestamp}_{filename}\"\n)\n\nfunc (s *Store) GetInstanceStorageSetting(ctx context.Context) (*storepb.InstanceStorageSetting, error) {\n\tinstanceSetting, err := s.GetInstanceSetting(ctx, &FindInstanceSetting{\n\t\tName: storepb.InstanceSettingKey_STORAGE.String(),\n\t})\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"failed to get instance storage setting\")\n\t}\n\n\tinstanceStorageSetting := &storepb.InstanceStorageSetting{}\n\tif instanceSetting != nil {\n\t\tinstanceStorageSetting = instanceSetting.GetStorageSetting()\n\t}\n\tif instanceStorageSetting.StorageType == storepb.InstanceStorageSetting_STORAGE_TYPE_UNSPECIFIED {\n\t\tinstanceStorageSetting.StorageType = defaultInstanceStorageType\n\t}\n\tif instanceStorageSetting.UploadSizeLimitMb == 0 {\n\t\tinstanceStorageSetting.UploadSizeLimitMb = defaultInstanceUploadSizeLimitMb\n\t}\n\tif instanceStorageSetting.FilepathTemplate == \"\" {\n\t\tinstanceStorageSetting.FilepathTemplate = defaultInstanceFilepathTemplate\n\t}\n\ts.instanceSettingCache.Set(ctx, storepb.InstanceSettingKey_STORAGE.String(), &storepb.InstanceSetting{\n\t\tKey:   storepb.InstanceSettingKey_STORAGE,\n\t\tValue: &storepb.InstanceSetting_StorageSetting{StorageSetting: instanceStorageSetting},\n\t})\n\treturn instanceStorageSetting, nil\n}\n\nfunc convertInstanceSettingFromRaw(instanceSettingRaw *InstanceSetting) (*storepb.InstanceSetting, error) {\n\tinstanceSetting := &storepb.InstanceSetting{\n\t\tKey: storepb.InstanceSettingKey(storepb.InstanceSettingKey_value[instanceSettingRaw.Name]),\n\t}\n\tswitch instanceSettingRaw.Name {\n\tcase storepb.InstanceSettingKey_BASIC.String():\n\t\tbasicSetting := &storepb.InstanceBasicSetting{}\n\t\tif err := protojsonUnmarshaler.Unmarshal([]byte(instanceSettingRaw.Value), basicSetting); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tinstanceSetting.Value = &storepb.InstanceSetting_BasicSetting{BasicSetting: basicSetting}\n\tcase storepb.InstanceSettingKey_GENERAL.String():\n\t\tgeneralSetting := &storepb.InstanceGeneralSetting{}\n\t\tif err := protojsonUnmarshaler.Unmarshal([]byte(instanceSettingRaw.Value), generalSetting); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tinstanceSetting.Value = &storepb.InstanceSetting_GeneralSetting{GeneralSetting: generalSetting}\n\tcase storepb.InstanceSettingKey_STORAGE.String():\n\t\tstorageSetting := &storepb.InstanceStorageSetting{}\n\t\tif err := protojsonUnmarshaler.Unmarshal([]byte(instanceSettingRaw.Value), storageSetting); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tinstanceSetting.Value = &storepb.InstanceSetting_StorageSetting{StorageSetting: storageSetting}\n\tcase storepb.InstanceSettingKey_MEMO_RELATED.String():\n\t\tmemoRelatedSetting := &storepb.InstanceMemoRelatedSetting{}\n\t\tif err := protojsonUnmarshaler.Unmarshal([]byte(instanceSettingRaw.Value), memoRelatedSetting); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tinstanceSetting.Value = &storepb.InstanceSetting_MemoRelatedSetting{MemoRelatedSetting: memoRelatedSetting}\n\tcase storepb.InstanceSettingKey_TAGS.String():\n\t\ttagsSetting := &storepb.InstanceTagsSetting{}\n\t\tif err := protojsonUnmarshaler.Unmarshal([]byte(instanceSettingRaw.Value), tagsSetting); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tinstanceSetting.Value = &storepb.InstanceSetting_TagsSetting{TagsSetting: tagsSetting}\n\tcase storepb.InstanceSettingKey_NOTIFICATION.String():\n\t\tnotificationSetting := &storepb.InstanceNotificationSetting{}\n\t\tif err := protojsonUnmarshaler.Unmarshal([]byte(instanceSettingRaw.Value), notificationSetting); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tinstanceSetting.Value = &storepb.InstanceSetting_NotificationSetting{NotificationSetting: notificationSetting}\n\tdefault:\n\t\t// Skip unsupported instance setting key.\n\t\treturn nil, nil\n\t}\n\treturn instanceSetting, nil\n}\n"
  },
  {
    "path": "store/memo.go",
    "content": "package store\n\nimport (\n\t\"context\"\n\t\"errors\"\n\n\t\"github.com/usememos/memos/internal/base\"\n\n\tstorepb \"github.com/usememos/memos/proto/gen/store\"\n)\n\n// Visibility is the type of a visibility.\ntype Visibility string\n\nconst (\n\t// Public is the PUBLIC visibility.\n\tPublic Visibility = \"PUBLIC\"\n\t// Protected is the PROTECTED visibility.\n\tProtected Visibility = \"PROTECTED\"\n\t// Private is the PRIVATE visibility.\n\tPrivate Visibility = \"PRIVATE\"\n)\n\nfunc (v Visibility) String() string {\n\tswitch v {\n\tcase Public:\n\t\treturn \"PUBLIC\"\n\tcase Protected:\n\t\treturn \"PROTECTED\"\n\tdefault:\n\t\treturn \"PRIVATE\"\n\t}\n}\n\ntype Memo struct {\n\t// ID is the system generated unique identifier for the memo.\n\tID int32\n\t// UID is the user defined unique identifier for the memo.\n\tUID string\n\n\t// Standard fields\n\tRowStatus RowStatus\n\tCreatorID int32\n\tCreatedTs int64\n\tUpdatedTs int64\n\n\t// Domain specific fields\n\tContent    string\n\tVisibility Visibility\n\tPinned     bool\n\tPayload    *storepb.MemoPayload\n\n\t// Composed fields\n\tParentUID *string\n}\n\ntype FindMemo struct {\n\tID  *int32\n\tUID *string\n\n\tIDList  []int32\n\tUIDList []string\n\n\t// Standard fields\n\tRowStatus *RowStatus\n\tCreatorID *int32\n\n\t// Domain specific fields\n\tVisibilityList  []Visibility\n\tExcludeContent  bool\n\tExcludeComments bool\n\tFilters         []string\n\n\t// Pagination\n\tLimit  *int\n\tOffset *int\n\n\t// Ordering\n\tOrderByPinned    bool\n\tOrderByUpdatedTs bool\n\tOrderByTimeAsc   bool\n}\n\ntype FindMemoPayload struct {\n\tRaw                *string\n\tTagSearch          []string\n\tHasLink            bool\n\tHasTaskList        bool\n\tHasCode            bool\n\tHasIncompleteTasks bool\n}\n\ntype UpdateMemo struct {\n\tID         int32\n\tUID        *string\n\tCreatedTs  *int64\n\tUpdatedTs  *int64\n\tRowStatus  *RowStatus\n\tContent    *string\n\tVisibility *Visibility\n\tPinned     *bool\n\tPayload    *storepb.MemoPayload\n}\n\ntype DeleteMemo struct {\n\tID int32\n}\n\nfunc (s *Store) CreateMemo(ctx context.Context, create *Memo) (*Memo, error) {\n\tif !base.UIDMatcher.MatchString(create.UID) {\n\t\treturn nil, errors.New(\"invalid uid\")\n\t}\n\treturn s.driver.CreateMemo(ctx, create)\n}\n\nfunc (s *Store) ListMemos(ctx context.Context, find *FindMemo) ([]*Memo, error) {\n\treturn s.driver.ListMemos(ctx, find)\n}\n\nfunc (s *Store) GetMemo(ctx context.Context, find *FindMemo) (*Memo, error) {\n\tlist, err := s.ListMemos(ctx, find)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif len(list) == 0 {\n\t\treturn nil, nil\n\t}\n\n\tmemo := list[0]\n\treturn memo, nil\n}\n\nfunc (s *Store) UpdateMemo(ctx context.Context, update *UpdateMemo) error {\n\tif update.UID != nil && !base.UIDMatcher.MatchString(*update.UID) {\n\t\treturn errors.New(\"invalid uid\")\n\t}\n\treturn s.driver.UpdateMemo(ctx, update)\n}\n\nfunc (s *Store) DeleteMemo(ctx context.Context, delete *DeleteMemo) error {\n\t// Clean up memo_relation records where this memo is either the source or target.\n\tif err := s.driver.DeleteMemoRelation(ctx, &DeleteMemoRelation{MemoID: &delete.ID}); err != nil {\n\t\treturn err\n\t}\n\tif err := s.driver.DeleteMemoRelation(ctx, &DeleteMemoRelation{RelatedMemoID: &delete.ID}); err != nil {\n\t\treturn err\n\t}\n\t// Clean up attachments linked to this memo.\n\tattachments, err := s.ListAttachments(ctx, &FindAttachment{MemoID: &delete.ID})\n\tif err != nil {\n\t\treturn err\n\t}\n\tfor _, attachment := range attachments {\n\t\tif err := s.DeleteAttachment(ctx, &DeleteAttachment{ID: attachment.ID}); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn s.driver.DeleteMemo(ctx, delete)\n}\n"
  },
  {
    "path": "store/memo_relation.go",
    "content": "package store\n\nimport (\n\t\"context\"\n)\n\ntype MemoRelationType string\n\nconst (\n\t// MemoRelationReference is the type for a reference memo relation.\n\tMemoRelationReference MemoRelationType = \"REFERENCE\"\n\t// MemoRelationComment is the type for a comment memo relation.\n\tMemoRelationComment MemoRelationType = \"COMMENT\"\n)\n\ntype MemoRelation struct {\n\tMemoID        int32\n\tRelatedMemoID int32\n\tType          MemoRelationType\n}\n\ntype FindMemoRelation struct {\n\tMemoID        *int32\n\tRelatedMemoID *int32\n\tType          *MemoRelationType\n\tMemoFilter    *string\n\t// MemoIDList matches relations where memo_id OR related_memo_id is in the list.\n\tMemoIDList []int32\n}\n\ntype DeleteMemoRelation struct {\n\tMemoID        *int32\n\tRelatedMemoID *int32\n\tType          *MemoRelationType\n}\n\nfunc (s *Store) UpsertMemoRelation(ctx context.Context, create *MemoRelation) (*MemoRelation, error) {\n\treturn s.driver.UpsertMemoRelation(ctx, create)\n}\n\nfunc (s *Store) ListMemoRelations(ctx context.Context, find *FindMemoRelation) ([]*MemoRelation, error) {\n\treturn s.driver.ListMemoRelations(ctx, find)\n}\n\nfunc (s *Store) DeleteMemoRelation(ctx context.Context, delete *DeleteMemoRelation) error {\n\treturn s.driver.DeleteMemoRelation(ctx, delete)\n}\n"
  },
  {
    "path": "store/memo_share.go",
    "content": "package store\n\nimport \"context\"\n\n// MemoShare is an access grant that permits read-only access to a memo via a bearer token.\ntype MemoShare struct {\n\tID        int32\n\tUID       string\n\tMemoID    int32\n\tCreatorID int32\n\tCreatedTs int64\n\tExpiresTs *int64 // nil means the share never expires\n}\n\n// FindMemoShare is used to filter memo shares in list/get queries.\ntype FindMemoShare struct {\n\tID     *int32\n\tUID    *string\n\tMemoID *int32\n}\n\n// DeleteMemoShare identifies a share grant to remove.\ntype DeleteMemoShare struct {\n\tID  *int32\n\tUID *string\n}\n\n// CreateMemoShare creates a new share grant.\nfunc (s *Store) CreateMemoShare(ctx context.Context, create *MemoShare) (*MemoShare, error) {\n\treturn s.driver.CreateMemoShare(ctx, create)\n}\n\n// ListMemoShares returns all share grants matching the filter.\nfunc (s *Store) ListMemoShares(ctx context.Context, find *FindMemoShare) ([]*MemoShare, error) {\n\treturn s.driver.ListMemoShares(ctx, find)\n}\n\n// GetMemoShare returns the first share grant matching the filter, or nil if none found.\nfunc (s *Store) GetMemoShare(ctx context.Context, find *FindMemoShare) (*MemoShare, error) {\n\treturn s.driver.GetMemoShare(ctx, find)\n}\n\n// DeleteMemoShare removes a share grant.\nfunc (s *Store) DeleteMemoShare(ctx context.Context, delete *DeleteMemoShare) error {\n\treturn s.driver.DeleteMemoShare(ctx, delete)\n}\n"
  },
  {
    "path": "store/migration/mysql/0.17/00__inbox.sql",
    "content": "-- inbox\nCREATE TABLE `inbox` (\n  `id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY,\n  `created_ts` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,\n  `sender_id` INT NOT NULL,\n  `receiver_id` INT NOT NULL,\n  `status` TEXT NOT NULL,\n  `message` TEXT NOT NULL\n);\n"
  },
  {
    "path": "store/migration/mysql/0.17/01__delete_activity.sql",
    "content": "DELETE FROM `activity`;\n"
  },
  {
    "path": "store/migration/mysql/0.18/00__extend_text.sql",
    "content": "ALTER TABLE `system_setting` MODIFY `value` LONGTEXT NOT NULL;\nALTER TABLE `user_setting` MODIFY `value` LONGTEXT NOT NULL;\nALTER TABLE `user` MODIFY `avatar_url` LONGTEXT NOT NULL;\n"
  },
  {
    "path": "store/migration/mysql/0.18/01__webhook.sql",
    "content": "-- webhook\nCREATE TABLE `webhook` (\n  `id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY,\n  `created_ts` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,\n  `updated_ts` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,\n  `row_status` VARCHAR(256) NOT NULL DEFAULT 'NORMAL',\n  `creator_id` INT NOT NULL,\n  `name` TEXT NOT NULL,\n  `url` TEXT NOT NULL\n);\n"
  },
  {
    "path": "store/migration/mysql/0.18/02__user_setting.sql",
    "content": "UPDATE `user_setting` SET `key` = 'USER_SETTING_LOCALE', `value` = REPLACE(`value`, '\"', '') WHERE `key` = 'locale';\nUPDATE `user_setting` SET `key` = 'USER_SETTING_APPEARANCE', `value` = REPLACE(`value`, '\"', '') WHERE `key` = 'appearance';\nUPDATE `user_setting` SET `key` = 'USER_SETTING_MEMO_VISIBILITY', `value` = REPLACE(`value`, '\"', '') WHERE `key` = 'memo-visibility';\nUPDATE `user_setting` SET `key` = 'USER_SETTING_TELEGRAM_USER_ID', `value` = REPLACE(`value`, '\"', '') WHERE `key` = 'telegram-user-id';\n"
  },
  {
    "path": "store/migration/mysql/0.19/00__add_resource_name.sql",
    "content": "ALTER TABLE `memo` ADD COLUMN `resource_name` VARCHAR(256) AFTER `id`;\n\nUPDATE `memo` SET `resource_name` = uuid();\n\nALTER TABLE `memo` MODIFY COLUMN `resource_name` VARCHAR(256) NOT NULL;\n\nCREATE UNIQUE INDEX idx_memo_resource_name ON `memo` (`resource_name`);\n\nALTER TABLE `resource` ADD COLUMN `resource_name` VARCHAR(256) AFTER `id`;\n\nUPDATE `resource` SET `resource_name` = uuid();\n\nALTER TABLE `resource` MODIFY COLUMN `resource_name` VARCHAR(256) NOT NULL;\n\nCREATE UNIQUE INDEX idx_resource_resource_name ON `resource` (`resource_name`);\n"
  },
  {
    "path": "store/migration/mysql/0.20/00__reaction.sql",
    "content": "-- reaction\nCREATE TABLE `reaction` (\n  `id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY,\n  `created_ts` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,\n  `creator_id` INT NOT NULL,\n  `content_id` VARCHAR(256) NOT NULL,\n  `reaction_type` VARCHAR(256) NOT NULL,\n  UNIQUE(`creator_id`,`content_id`,`reaction_type`)  \n);\n"
  },
  {
    "path": "store/migration/mysql/0.21/00__user_description.sql",
    "content": "ALTER TABLE `user` ADD COLUMN `description` VARCHAR(256) NOT NULL DEFAULT '';\n"
  },
  {
    "path": "store/migration/mysql/0.21/01__rename_uid.sql",
    "content": "ALTER TABLE `memo` RENAME COLUMN `resource_name` TO `uid`;\n\nALTER TABLE `resource` RENAME COLUMN `resource_name` TO `uid`;\n"
  },
  {
    "path": "store/migration/mysql/0.22/00__resource_storage_type.sql",
    "content": "ALTER TABLE `resource` ADD COLUMN `storage_type` VARCHAR(256) NOT NULL DEFAULT '';\nALTER TABLE `resource` ADD COLUMN `reference` VARCHAR(256) NOT NULL DEFAULT '';\nALTER TABLE `resource` ADD COLUMN `payload` TEXT NOT NULL;\n\nUPDATE `resource` SET `payload` = '{}';\n\nUPDATE `resource` SET `storage_type` = 'LOCAL', `reference` = `internal_path` WHERE `internal_path` IS NOT NULL AND `internal_path` != '';\nUPDATE `resource` SET `storage_type` = 'EXTERNAL', `reference` = `external_link` WHERE `external_link` IS NOT NULL AND `external_link` != '';\n\nALTER TABLE `resource` DROP COLUMN `internal_path`;\nALTER TABLE `resource` DROP COLUMN `external_link`;\n"
  },
  {
    "path": "store/migration/mysql/0.22/01__memo_tags.sql",
    "content": "ALTER TABLE `memo` ADD COLUMN `tags_temp` JSON;\nUPDATE `memo` SET `tags_temp` = '[]';\nALTER TABLE `memo` CHANGE COLUMN `tags_temp` `tags` JSON NOT NULL;\n"
  },
  {
    "path": "store/migration/mysql/0.22/02__memo_payload.sql",
    "content": "ALTER TABLE `memo` ADD COLUMN `payload_temp` JSON;\nUPDATE `memo` SET `payload_temp` = '{}';\nALTER TABLE `memo` CHANGE COLUMN `payload_temp` `payload` JSON NOT NULL;\n"
  },
  {
    "path": "store/migration/mysql/0.22/03__drop_tag.sql",
    "content": "DROP TABLE IF EXISTS `tag`;\n"
  },
  {
    "path": "store/migration/mysql/0.23/00__reactions.sql",
    "content": "UPDATE `reaction` SET `reaction_type` = '👍' WHERE `reaction_type` = 'THUMBS_UP';\nUPDATE `reaction` SET `reaction_type` = '👎' WHERE `reaction_type` = 'THUMBS_DOWN';\nUPDATE `reaction` SET `reaction_type` = '💛' WHERE `reaction_type` = 'HEART';\nUPDATE `reaction` SET `reaction_type` = '🔥' WHERE `reaction_type` = 'FIRE';\nUPDATE `reaction` SET `reaction_type` = '👏' WHERE `reaction_type` = 'CLAPPING_HANDS';\nUPDATE `reaction` SET `reaction_type` = '😂' WHERE `reaction_type` = 'LAUGH';\nUPDATE `reaction` SET `reaction_type` = '👌' WHERE `reaction_type` = 'OK_HAND';\nUPDATE `reaction` SET `reaction_type` = '🚀' WHERE `reaction_type` = 'ROCKET';\nUPDATE `reaction` SET `reaction_type` = '👀' WHERE `reaction_type` = 'EYES';\nUPDATE `reaction` SET `reaction_type` = '🤔' WHERE `reaction_type` = 'THINKING_FACE';\nUPDATE `reaction` SET `reaction_type` = '🤡' WHERE `reaction_type` = 'CLOWN_FACE';\nUPDATE `reaction` SET `reaction_type` = '❓' WHERE `reaction_type` = 'QUESTION_MARK';\n"
  },
  {
    "path": "store/migration/mysql/0.24/00__memo.sql",
    "content": "-- Drop deprecated tags column.\nALTER TABLE `memo` DROP COLUMN `tags`;"
  },
  {
    "path": "store/migration/mysql/0.24/01__memo_pinned.sql",
    "content": "-- Add pinned column.\nALTER TABLE `memo` ADD COLUMN `pinned` BOOLEAN NOT NULL DEFAULT FALSE;\n\n-- Update pinned column from memo_organizer.\nUPDATE memo\nJOIN memo_organizer ON memo.id = memo_organizer.memo_id\nSET memo.pinned = TRUE\nWHERE memo_organizer.pinned = 1;\n"
  },
  {
    "path": "store/migration/mysql/0.24/02__s3_reference_length.sql",
    "content": "-- https://github.com/usememos/memos/issues/4322\nALTER TABLE `resource` MODIFY `reference` TEXT NOT NULL DEFAULT ('');\n"
  },
  {
    "path": "store/migration/mysql/0.25/00__remove_webhook.sql",
    "content": "DROP TABLE IF EXISTS webhook;\n"
  },
  {
    "path": "store/migration/mysql/0.26/00__rename_resource_to_attachment.sql",
    "content": "RENAME TABLE resource TO attachment;\n"
  },
  {
    "path": "store/migration/mysql/0.26/01__drop_memo_organizer.sql",
    "content": "DROP TABLE IF EXISTS memo_organizer;\n"
  },
  {
    "path": "store/migration/mysql/0.26/02__migrate_host_to_admin.sql",
    "content": "UPDATE `user` SET `role` = 'ADMIN' WHERE `role` = 'HOST';\n"
  },
  {
    "path": "store/migration/mysql/0.27/00__migrate_storage_setting.sql",
    "content": "-- Set storage type to DATABASE for existing instances that have no storage setting configured.\n-- This preserves backward-compatible behavior before the default was changed to LOCAL.\nINSERT INTO system_setting (name, value, description)\nSELECT 'STORAGE', '{\"storageType\":\"DATABASE\"}', ''\nFROM DUAL\nWHERE NOT EXISTS (SELECT 1 FROM system_setting WHERE name = 'STORAGE');\n"
  },
  {
    "path": "store/migration/mysql/0.27/01__add_idp_uid.sql",
    "content": "-- Add uid column to idp table\nALTER TABLE `idp` ADD COLUMN `uid` VARCHAR(256) NOT NULL DEFAULT '';\n\n-- Populate uid for existing rows using hex of id as a fallback\nUPDATE `idp` SET `uid` = LOWER(LPAD(HEX(`id`), 8, '0')) WHERE `uid` = '';\n\n-- Create unique index on uid\nALTER TABLE `idp` ADD UNIQUE INDEX `idx_idp_uid` (`uid`);\n"
  },
  {
    "path": "store/migration/mysql/0.27/02__migrate_inbox_message_payload.sql",
    "content": "UPDATE `inbox` AS i\nJOIN `activity` AS a\n  ON a.`id` = CAST(JSON_UNQUOTE(JSON_EXTRACT(i.`message`, '$.activityId')) AS UNSIGNED)\nSET i.`message` = JSON_SET(\n  JSON_REMOVE(i.`message`, '$.activityId'),\n  '$.memoComment',\n  JSON_OBJECT(\n    'memoId',\n    JSON_EXTRACT(a.`payload`, '$.memoComment.memoId'),\n    'relatedMemoId',\n    JSON_EXTRACT(a.`payload`, '$.memoComment.relatedMemoId')\n  )\n)\nWHERE JSON_EXTRACT(i.`message`, '$.activityId') IS NOT NULL;\n"
  },
  {
    "path": "store/migration/mysql/0.27/03__drop_activity.sql",
    "content": "DROP TABLE `activity`;\n"
  },
  {
    "path": "store/migration/mysql/0.27/04__memo_share.sql",
    "content": "-- memo_share stores per-memo share grants (one row per share link).\n-- uid is the opaque bearer token included in the share URL.\n-- ON DELETE CASCADE ensures grants are cleaned up when the parent memo is deleted.\nCREATE TABLE memo_share (\n  id         INT          NOT NULL AUTO_INCREMENT PRIMARY KEY,\n  uid        VARCHAR(255) NOT NULL UNIQUE,\n  memo_id    INT          NOT NULL,\n  creator_id INT          NOT NULL,\n  created_ts BIGINT       NOT NULL DEFAULT (UNIX_TIMESTAMP()),\n  expires_ts BIGINT       DEFAULT NULL,\n  FOREIGN KEY (memo_id) REFERENCES memo(id) ON DELETE CASCADE\n);\n\nCREATE INDEX idx_memo_share_memo_id ON memo_share(memo_id);\n"
  },
  {
    "path": "store/migration/mysql/LATEST.sql",
    "content": "-- system_setting\nCREATE TABLE `system_setting` (\n  `name` VARCHAR(256) NOT NULL PRIMARY KEY,\n  `value` LONGTEXT NOT NULL,\n  `description` TEXT NOT NULL\n);\n\n-- user\nCREATE TABLE `user` (\n  `id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY,\n  `created_ts` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,\n  `updated_ts` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,\n  `row_status` VARCHAR(256) NOT NULL DEFAULT 'NORMAL',\n  `username` VARCHAR(256) NOT NULL UNIQUE,\n  `role` VARCHAR(256) NOT NULL DEFAULT 'USER',\n  `email` VARCHAR(256) NOT NULL DEFAULT '',\n  `nickname` VARCHAR(256) NOT NULL DEFAULT '',\n  `password_hash` VARCHAR(256) NOT NULL,\n  `avatar_url` LONGTEXT NOT NULL,\n  `description` VARCHAR(256) NOT NULL DEFAULT ''\n);\n\n-- user_setting\nCREATE TABLE `user_setting` (\n  `user_id` INT NOT NULL,\n  `key` VARCHAR(256) NOT NULL,\n  `value` LONGTEXT NOT NULL,\n  UNIQUE(`user_id`,`key`)\n);\n\n-- memo\nCREATE TABLE `memo` (\n  `id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY,\n  `uid` VARCHAR(256) NOT NULL UNIQUE,\n  `creator_id` INT NOT NULL,\n  `created_ts` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,\n  `updated_ts` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,\n  `row_status` VARCHAR(256) NOT NULL DEFAULT 'NORMAL',\n  `content` TEXT NOT NULL,\n  `visibility` VARCHAR(256) NOT NULL DEFAULT 'PRIVATE',\n  `pinned` BOOLEAN NOT NULL DEFAULT FALSE,\n  `payload` JSON NOT NULL\n);\n\n-- memo_relation\nCREATE TABLE `memo_relation` (\n  `memo_id` INT NOT NULL,\n  `related_memo_id` INT NOT NULL,\n  `type` VARCHAR(256) NOT NULL,\n  UNIQUE(`memo_id`,`related_memo_id`,`type`)\n);\n\n-- attachment\nCREATE TABLE `attachment` (\n  `id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY,\n  `uid` VARCHAR(256) NOT NULL UNIQUE,\n  `creator_id` INT NOT NULL,\n  `created_ts` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,\n  `updated_ts` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,\n  `filename` TEXT NOT NULL,\n  `blob` MEDIUMBLOB,\n  `type` VARCHAR(256) NOT NULL DEFAULT '',\n  `size` INT NOT NULL DEFAULT '0',\n  `memo_id` INT DEFAULT NULL,\n  `storage_type` VARCHAR(256) NOT NULL DEFAULT '',\n  `reference` TEXT NOT NULL DEFAULT (''),\n  `payload` TEXT NOT NULL\n);\n\n-- idp\nCREATE TABLE `idp` (\n  `id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY,\n  `uid` VARCHAR(256) NOT NULL UNIQUE,\n  `name` TEXT NOT NULL,\n  `type` TEXT NOT NULL,\n  `identifier_filter` VARCHAR(256) NOT NULL DEFAULT '',\n  `config` TEXT NOT NULL\n);\n\n-- inbox\nCREATE TABLE `inbox` (\n  `id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY,\n  `created_ts` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,\n  `sender_id` INT NOT NULL,\n  `receiver_id` INT NOT NULL,\n  `status` TEXT NOT NULL,\n  `message` TEXT NOT NULL\n);\n\n-- reaction\nCREATE TABLE `reaction` (\n  `id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY,\n  `created_ts` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,\n  `creator_id` INT NOT NULL,\n  `content_id` VARCHAR(256) NOT NULL,\n  `reaction_type` VARCHAR(256) NOT NULL,\n  UNIQUE(`creator_id`,`content_id`,`reaction_type`)  \n);\n\n-- memo_share\nCREATE TABLE `memo_share` (\n  `id`         INT          NOT NULL AUTO_INCREMENT PRIMARY KEY,\n  `uid`        VARCHAR(255) NOT NULL UNIQUE,\n  `memo_id`    INT          NOT NULL,\n  `creator_id` INT          NOT NULL,\n  `created_ts` BIGINT       NOT NULL DEFAULT (UNIX_TIMESTAMP()),\n  `expires_ts` BIGINT       DEFAULT NULL,\n  FOREIGN KEY (`memo_id`) REFERENCES `memo`(`id`) ON DELETE CASCADE\n);\n\nCREATE INDEX `idx_memo_share_memo_id` ON `memo_share`(`memo_id`);\n"
  },
  {
    "path": "store/migration/postgres/0.19/00__add_resource_name.sql",
    "content": "ALTER TABLE memo ADD COLUMN resource_name TEXT;\n\nUPDATE memo SET resource_name = uuid_in(md5(random()::text || random()::text)::cstring);\n\nALTER TABLE memo ALTER COLUMN resource_name SET NOT NULL;\n\nCREATE UNIQUE INDEX idx_memo_resource_name ON memo (resource_name);\n\nALTER TABLE resource ADD COLUMN resource_name TEXT;\n\nUPDATE resource SET resource_name = uuid_in(md5(random()::text || random()::text)::cstring);\n\nALTER TABLE resource ALTER COLUMN resource_name SET NOT NULL;\n\nCREATE UNIQUE INDEX idx_resource_resource_name ON resource (resource_name);\n"
  },
  {
    "path": "store/migration/postgres/0.20/00__reaction.sql",
    "content": "-- reaction\nCREATE TABLE reaction (\n  id SERIAL PRIMARY KEY,\n  created_ts BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW()),\n  creator_id INTEGER NOT NULL,\n  content_id TEXT NOT NULL,\n  reaction_type TEXT NOT NULL,\n  UNIQUE(creator_id, content_id, reaction_type)\n);\n"
  },
  {
    "path": "store/migration/postgres/0.21/00__user_description.sql",
    "content": "ALTER TABLE \"user\" ADD COLUMN description TEXT NOT NULL DEFAULT '';\n"
  },
  {
    "path": "store/migration/postgres/0.21/01__rename_uid.sql",
    "content": "ALTER TABLE memo RENAME COLUMN resource_name TO uid;\n\nALTER TABLE resource RENAME COLUMN resource_name TO uid;\n"
  },
  {
    "path": "store/migration/postgres/0.22/00__resource_storage_type.sql",
    "content": "ALTER TABLE resource ADD COLUMN storage_type TEXT NOT NULL DEFAULT '';\nALTER TABLE resource ADD COLUMN reference TEXT NOT NULL DEFAULT '';\nALTER TABLE resource ADD COLUMN payload TEXT NOT NULL DEFAULT '{}';\n\nUPDATE resource SET storage_type = 'LOCAL', reference = internal_path WHERE internal_path IS NOT NULL AND internal_path != '';\n\nUPDATE resource SET storage_type = 'EXTERNAL', reference = external_link WHERE external_link IS NOT NULL AND external_link != '';\n\nALTER TABLE resource DROP COLUMN internal_path;\n\nALTER TABLE resource DROP COLUMN external_link;\n"
  },
  {
    "path": "store/migration/postgres/0.22/01__memo_tags.sql",
    "content": "ALTER TABLE memo ADD COLUMN tags JSONB NOT NULL DEFAULT '[]';\n"
  },
  {
    "path": "store/migration/postgres/0.22/02__memo_payload.sql",
    "content": "ALTER TABLE memo ADD COLUMN payload JSONB NOT NULL DEFAULT '{}';\n"
  },
  {
    "path": "store/migration/postgres/0.22/03__drop_tag.sql",
    "content": "DROP TABLE IF EXISTS tag;\n"
  },
  {
    "path": "store/migration/postgres/0.23/00__reactions.sql",
    "content": "UPDATE \"reaction\" SET \"reaction_type\" = '👍' WHERE \"reaction_type\" = 'THUMBS_UP';\nUPDATE \"reaction\" SET \"reaction_type\" = '👎' WHERE \"reaction_type\" = 'THUMBS_DOWN';\nUPDATE \"reaction\" SET \"reaction_type\" = '💛' WHERE \"reaction_type\" = 'HEART';\nUPDATE \"reaction\" SET \"reaction_type\" = '🔥' WHERE \"reaction_type\" = 'FIRE';\nUPDATE \"reaction\" SET \"reaction_type\" = '👏' WHERE \"reaction_type\" = 'CLAPPING_HANDS';\nUPDATE \"reaction\" SET \"reaction_type\" = '😂' WHERE \"reaction_type\" = 'LAUGH';\nUPDATE \"reaction\" SET \"reaction_type\" = '👌' WHERE \"reaction_type\" = 'OK_HAND';\nUPDATE \"reaction\" SET \"reaction_type\" = '🚀' WHERE \"reaction_type\" = 'ROCKET';\nUPDATE \"reaction\" SET \"reaction_type\" = '👀' WHERE \"reaction_type\" = 'EYES';\nUPDATE \"reaction\" SET \"reaction_type\" = '🤔' WHERE \"reaction_type\" = 'THINKING_FACE';\nUPDATE \"reaction\" SET \"reaction_type\" = '🤡' WHERE \"reaction_type\" = 'CLOWN_FACE';\nUPDATE \"reaction\" SET \"reaction_type\" = '❓' WHERE \"reaction_type\" = 'QUESTION_MARK';\n"
  },
  {
    "path": "store/migration/postgres/0.24/00__memo.sql",
    "content": "-- Drop deprecated tags column.\nALTER TABLE memo DROP COLUMN IF EXISTS tags;"
  },
  {
    "path": "store/migration/postgres/0.24/01__memo_pinned.sql",
    "content": "-- Add pinned column.\nALTER TABLE memo ADD COLUMN pinned BOOLEAN NOT NULL DEFAULT FALSE;\n\n-- Update pinned column from memo_organizer.\nUPDATE memo\nSET pinned = TRUE\nFROM memo_organizer\nWHERE memo.id = memo_organizer.memo_id AND memo_organizer.pinned = 1;"
  },
  {
    "path": "store/migration/postgres/0.25/00__remove_webhook.sql",
    "content": "DROP TABLE IF EXISTS webhook;\n"
  },
  {
    "path": "store/migration/postgres/0.26/00__rename_resource_to_attachment.sql",
    "content": "ALTER TABLE resource RENAME TO attachment;\n"
  },
  {
    "path": "store/migration/postgres/0.26/01__drop_memo_organizer.sql",
    "content": "DROP TABLE IF EXISTS memo_organizer;\n"
  },
  {
    "path": "store/migration/postgres/0.26/02__migrate_host_to_admin.sql",
    "content": "UPDATE \"user\" SET role = 'ADMIN' WHERE role = 'HOST';\n"
  },
  {
    "path": "store/migration/postgres/0.27/00__migrate_storage_setting.sql",
    "content": "-- Set storage type to DATABASE for existing instances that have no storage setting configured.\n-- This preserves backward-compatible behavior before the default was changed to LOCAL.\nINSERT INTO system_setting (name, value, description)\nSELECT 'STORAGE', '{\"storageType\":\"DATABASE\"}', ''\nWHERE NOT EXISTS (SELECT 1 FROM system_setting WHERE name = 'STORAGE');\n"
  },
  {
    "path": "store/migration/postgres/0.27/01__add_idp_uid.sql",
    "content": "-- Add uid column to idp table\nALTER TABLE idp ADD COLUMN uid TEXT NOT NULL DEFAULT '';\n\n-- Populate uid for existing rows using hex of id as a fallback\nUPDATE idp SET uid = LPAD(TO_HEX(id), 8, '0') WHERE uid = '';\n\n-- Create unique index on uid\nCREATE UNIQUE INDEX IF NOT EXISTS idx_idp_uid ON idp (uid);\n"
  },
  {
    "path": "store/migration/postgres/0.27/02__migrate_inbox_message_payload.sql",
    "content": "UPDATE inbox AS i\nSET message = jsonb_set(\n  i.message::jsonb - 'activityId',\n  '{memoComment}',\n  jsonb_build_object(\n    'memoId',\n    a.payload->'memoComment'->'memoId',\n    'relatedMemoId',\n    a.payload->'memoComment'->'relatedMemoId'\n  )\n)::text\nFROM activity AS a\nWHERE (i.message::jsonb->>'activityId')::integer = a.id;\n"
  },
  {
    "path": "store/migration/postgres/0.27/03__drop_activity.sql",
    "content": "DROP TABLE activity;\n"
  },
  {
    "path": "store/migration/postgres/0.27/04__memo_share.sql",
    "content": "-- memo_share stores per-memo share grants (one row per share link).\n-- uid is the opaque bearer token included in the share URL.\n-- ON DELETE CASCADE ensures grants are cleaned up when the parent memo is deleted.\nCREATE TABLE memo_share (\n  id         SERIAL  PRIMARY KEY,\n  uid        TEXT    NOT NULL UNIQUE,\n  memo_id    INTEGER NOT NULL,\n  creator_id INTEGER NOT NULL,\n  created_ts BIGINT  NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW()),\n  expires_ts BIGINT  DEFAULT NULL,\n  FOREIGN KEY (memo_id) REFERENCES memo(id) ON DELETE CASCADE\n);\n\nCREATE INDEX idx_memo_share_memo_id ON memo_share(memo_id);\n"
  },
  {
    "path": "store/migration/postgres/LATEST.sql",
    "content": "-- system_setting\nCREATE TABLE system_setting (\n  name TEXT NOT NULL PRIMARY KEY,\n  value TEXT NOT NULL,\n  description TEXT NOT NULL\n);\n\n-- user\nCREATE TABLE \"user\" (\n  id SERIAL PRIMARY KEY,\n  created_ts BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW()),\n  updated_ts BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW()),\n  row_status TEXT NOT NULL DEFAULT 'NORMAL',\n  username TEXT NOT NULL UNIQUE,\n  role TEXT NOT NULL DEFAULT 'USER',\n  email TEXT NOT NULL DEFAULT '',\n  nickname TEXT NOT NULL DEFAULT '',\n  password_hash TEXT NOT NULL,\n  avatar_url TEXT NOT NULL,\n  description TEXT NOT NULL DEFAULT ''\n);\n\n-- user_setting\nCREATE TABLE user_setting (\n  user_id INTEGER NOT NULL,\n  key TEXT NOT NULL,\n  value TEXT NOT NULL,\n  UNIQUE(user_id, key)\n);\n\n-- memo\nCREATE TABLE memo (\n  id SERIAL PRIMARY KEY,\n  uid TEXT NOT NULL UNIQUE,\n  creator_id INTEGER NOT NULL,\n  created_ts BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW()),\n  updated_ts BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW()),\n  row_status TEXT NOT NULL DEFAULT 'NORMAL',\n  content TEXT NOT NULL,\n  visibility TEXT NOT NULL DEFAULT 'PRIVATE',\n  pinned BOOLEAN NOT NULL DEFAULT FALSE,\n  payload JSONB NOT NULL DEFAULT '{}'\n);\n\n-- memo_relation\nCREATE TABLE memo_relation (\n  memo_id INTEGER NOT NULL,\n  related_memo_id INTEGER NOT NULL,\n  type TEXT NOT NULL,\n  UNIQUE(memo_id, related_memo_id, type)\n);\n\n-- attachment\nCREATE TABLE attachment (\n  id SERIAL PRIMARY KEY,\n  uid TEXT NOT NULL UNIQUE,\n  creator_id INTEGER NOT NULL,\n  created_ts BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW()),\n  updated_ts BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW()),\n  filename TEXT NOT NULL,\n  blob BYTEA,\n  type TEXT NOT NULL DEFAULT '',\n  size INTEGER NOT NULL DEFAULT 0,\n  memo_id INTEGER DEFAULT NULL,\n  storage_type TEXT NOT NULL DEFAULT '',\n  reference TEXT NOT NULL DEFAULT '',\n  payload TEXT NOT NULL DEFAULT '{}'\n);\n\n-- idp\nCREATE TABLE idp (\n  id SERIAL PRIMARY KEY,\n  uid TEXT NOT NULL UNIQUE,\n  name TEXT NOT NULL,\n  type TEXT NOT NULL,\n  identifier_filter TEXT NOT NULL DEFAULT '',\n  config JSONB NOT NULL DEFAULT '{}'\n);\n\n-- inbox\nCREATE TABLE inbox (\n  id SERIAL PRIMARY KEY,\n  created_ts BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW()),\n  sender_id INTEGER NOT NULL,\n  receiver_id INTEGER NOT NULL,\n  status TEXT NOT NULL,\n  message TEXT NOT NULL\n);\n\n-- reaction\nCREATE TABLE reaction (\n  id SERIAL PRIMARY KEY,\n  created_ts BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW()),\n  creator_id INTEGER NOT NULL,\n  content_id TEXT NOT NULL,\n  reaction_type TEXT NOT NULL,\n  UNIQUE(creator_id, content_id, reaction_type)\n);\n\n-- memo_share\nCREATE TABLE memo_share (\n  id         SERIAL  PRIMARY KEY,\n  uid        TEXT    NOT NULL UNIQUE,\n  memo_id    INTEGER NOT NULL,\n  creator_id INTEGER NOT NULL,\n  created_ts BIGINT  NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW()),\n  expires_ts BIGINT  DEFAULT NULL,\n  FOREIGN KEY (memo_id) REFERENCES memo(id) ON DELETE CASCADE\n);\n\nCREATE INDEX idx_memo_share_memo_id ON memo_share(memo_id);\n"
  },
  {
    "path": "store/migration/sqlite/0.10/00__activity.sql",
    "content": "-- activity\nCREATE TABLE activity (\n  id INTEGER PRIMARY KEY AUTOINCREMENT,\n  creator_id INTEGER NOT NULL,\n  created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),\n  type TEXT NOT NULL DEFAULT '',\n  level TEXT NOT NULL CHECK (level IN ('INFO', 'WARN', 'ERROR')) DEFAULT 'INFO',\n  payload TEXT NOT NULL DEFAULT '{}'\n);"
  },
  {
    "path": "store/migration/sqlite/0.11/00__user_avatar.sql",
    "content": "ALTER TABLE\n  user\nADD\n  COLUMN avatar_url TEXT NOT NULL DEFAULT '';"
  },
  {
    "path": "store/migration/sqlite/0.11/01__idp.sql",
    "content": "-- idp\nCREATE TABLE idp (\n  id INTEGER PRIMARY KEY AUTOINCREMENT,\n  name TEXT NOT NULL,\n  type TEXT NOT NULL,\n  identifier_filter TEXT NOT NULL DEFAULT '',\n  config TEXT NOT NULL DEFAULT '{}'\n);"
  },
  {
    "path": "store/migration/sqlite/0.11/02__storage.sql",
    "content": "-- storage\nCREATE TABLE storage (\n  id INTEGER PRIMARY KEY AUTOINCREMENT,\n  name TEXT NOT NULL,\n  type TEXT NOT NULL,\n  config TEXT NOT NULL DEFAULT '{}'\n);"
  },
  {
    "path": "store/migration/sqlite/0.12/00__user_setting.sql",
    "content": "UPDATE\n  user_setting\nSET\n  key = 'memo-visibility'\nWHERE\n  key = 'memoVisibility';"
  },
  {
    "path": "store/migration/sqlite/0.12/01__system_setting.sql",
    "content": "UPDATE\n  system_setting\nSET\n  name = 'server-id'\nWHERE\n  name = 'serverId';\n\nUPDATE\n  system_setting\nSET\n  name = 'secret-session'\nWHERE\n  name = 'secretSessionName';\n\nUPDATE\n  system_setting\nSET\n  name = 'allow-signup'\nWHERE\n  name = 'allowSignUp';\n\nUPDATE\n  system_setting\nSET\n  name = 'disable-public-memos'\nWHERE\n  name = 'disablePublicMemos';\n\nUPDATE\n  system_setting\nSET\n  name = 'additional-style'\nWHERE\n  name = 'additionalStyle';\n\nUPDATE\n  system_setting\nSET\n  name = 'additional-script'\nWHERE\n  name = 'additionalScript';\n\nUPDATE\n  system_setting\nSET\n  name = 'customized-profile'\nWHERE\n  name = 'customizedProfile';\n\nUPDATE\n  system_setting\nSET\n  name = 'storage-service-id'\nWHERE\n  name = 'storageServiceId';\n\nUPDATE\n  system_setting\nSET\n  name = 'local-storage-path'\nWHERE\n  name = 'localStoragePath';\n\nUPDATE\n  system_setting\nSET\n  name = 'openai-config'\nWHERE\n  name = 'openAIConfig';"
  },
  {
    "path": "store/migration/sqlite/0.12/03__resource_internal_path.sql",
    "content": "ALTER TABLE\n  resource\nADD\n  COLUMN internal_path TEXT NOT NULL DEFAULT '';"
  },
  {
    "path": "store/migration/sqlite/0.12/04__resource_public_id.sql",
    "content": "ALTER TABLE\n  resource\nADD\n  COLUMN public_id TEXT NOT NULL DEFAULT '';\n\nCREATE UNIQUE INDEX resource_id_public_id_unique_index ON resource (id, public_id);\n\nUPDATE\n  resource\nSET\n  public_id = printf (\n    '%s-%s-%s-%s-%s',\n    lower(hex(randomblob(4))),\n    lower(hex(randomblob(2))),\n    lower(hex(randomblob(2))),\n    lower(hex(randomblob(2))),\n    lower(hex(randomblob(6)))\n  );"
  },
  {
    "path": "store/migration/sqlite/0.13/00__memo_relation.sql",
    "content": "-- memo_relation\nCREATE TABLE memo_relation (\n  memo_id INTEGER NOT NULL,\n  related_memo_id INTEGER NOT NULL,\n  type TEXT NOT NULL,\n  UNIQUE(memo_id, related_memo_id, type)\n);"
  },
  {
    "path": "store/migration/sqlite/0.13/01__remove_memo_organizer_id.sql",
    "content": "DROP TABLE IF EXISTS memo_organizer_temp;\n\nCREATE TABLE memo_organizer_temp (\n  memo_id INTEGER NOT NULL,\n  user_id INTEGER NOT NULL,\n  pinned INTEGER NOT NULL CHECK (pinned IN (0, 1)) DEFAULT 0,\n  UNIQUE(memo_id, user_id)\n);\n\nINSERT INTO\n  memo_organizer_temp (memo_id, user_id, pinned)\nSELECT\n  memo_id,\n  user_id,\n  pinned\nFROM\n  memo_organizer;\n\nDROP TABLE memo_organizer;\n\nALTER TABLE\n  memo_organizer_temp RENAME TO memo_organizer;"
  },
  {
    "path": "store/migration/sqlite/0.14/00__drop_resource_public_id.sql",
    "content": "DROP TABLE IF EXISTS resource_temp;\n\nCREATE TABLE resource_temp (\n  id INTEGER PRIMARY KEY AUTOINCREMENT,\n  creator_id INTEGER NOT NULL,\n  created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),\n  updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),\n  filename TEXT NOT NULL DEFAULT '',\n  blob BLOB DEFAULT NULL,\n  external_link TEXT NOT NULL DEFAULT '',\n  type TEXT NOT NULL DEFAULT '',\n  size INTEGER NOT NULL DEFAULT 0,\n  internal_path TEXT NOT NULL DEFAULT ''\n);\n\nINSERT INTO\n  resource_temp (id, creator_id, created_ts, updated_ts, filename, blob, external_link, type, size, internal_path)\nSELECT\n  id, creator_id, created_ts, updated_ts, filename, blob, external_link, type, size, internal_path\nFROM\n  resource;\n\nDROP TABLE resource;\n\nALTER TABLE resource_temp RENAME TO resource;\n"
  },
  {
    "path": "store/migration/sqlite/0.14/01__create_indexes.sql",
    "content": "CREATE INDEX IF NOT EXISTS idx_user_username ON user (username);\nCREATE INDEX IF NOT EXISTS idx_memo_creator_id ON memo (creator_id);\nCREATE INDEX IF NOT EXISTS idx_memo_content ON memo (content);\nCREATE INDEX IF NOT EXISTS idx_memo_visibility ON memo (visibility);\nCREATE INDEX IF NOT EXISTS idx_resource_creator_id ON resource (creator_id);\n"
  },
  {
    "path": "store/migration/sqlite/0.15/00__drop_user_open_id.sql",
    "content": "DROP TABLE IF EXISTS user_temp;\n\nCREATE TABLE user_temp (\n  id INTEGER PRIMARY KEY AUTOINCREMENT,\n  created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),\n  updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),\n  row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',\n  username TEXT NOT NULL UNIQUE,\n  role TEXT NOT NULL CHECK (role IN ('HOST', 'ADMIN', 'USER')) DEFAULT 'USER',\n  email TEXT NOT NULL DEFAULT '',\n  nickname TEXT NOT NULL DEFAULT '',\n  password_hash TEXT NOT NULL,\n  avatar_url TEXT NOT NULL DEFAULT ''\n);\n\nINSERT INTO\n  user_temp (id, created_ts, updated_ts, row_status, username, role, email, nickname, password_hash, avatar_url)\nSELECT\n  id, created_ts, updated_ts, row_status, username, role, email, nickname, password_hash, avatar_url\nFROM\n  user;\n\nDROP TABLE user;\n\nALTER TABLE user_temp RENAME TO user;\n"
  },
  {
    "path": "store/migration/sqlite/0.16/00__add_memo_id_to_resource.sql",
    "content": "ALTER TABLE resource ADD COLUMN memo_id INTEGER;\n\nUPDATE resource\nSET memo_id = (\n  SELECT memo_id\n  FROM memo_resource\n  WHERE resource.id = memo_resource.resource_id\n  LIMIT 1\n);\n\nCREATE INDEX idx_resource_memo_id ON resource (memo_id);\n\nDROP TABLE IF EXISTS memo_resource;\n"
  },
  {
    "path": "store/migration/sqlite/0.16/01__drop_shortcut_table.sql",
    "content": "DROP TABLE IF EXISTS shortcut;\n"
  },
  {
    "path": "store/migration/sqlite/0.17/00__inbox.sql",
    "content": "-- inbox\nCREATE TABLE inbox (\n  id INTEGER PRIMARY KEY AUTOINCREMENT,\n  created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),\n  sender_id INTEGER NOT NULL,\n  receiver_id INTEGER NOT NULL,\n  status TEXT NOT NULL,\n  message TEXT NOT NULL DEFAULT '{}'\n);\n"
  },
  {
    "path": "store/migration/sqlite/0.17/01__delete_activities.sql",
    "content": "DELETE FROM activity;\n"
  },
  {
    "path": "store/migration/sqlite/0.18/00__webhook.sql",
    "content": "-- webhook\nCREATE TABLE webhook (\n  id INTEGER PRIMARY KEY AUTOINCREMENT,\n  created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),\n  updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),\n  row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',\n  creator_id INTEGER NOT NULL,\n  name TEXT NOT NULL,\n  url TEXT NOT NULL\n);\n\nCREATE INDEX idx_webhook_creator_id ON webhook (creator_id);\n"
  },
  {
    "path": "store/migration/sqlite/0.18/01__user_setting.sql",
    "content": "UPDATE user_setting SET key = 'USER_SETTING_LOCALE', value = REPLACE(value, '\"', '') WHERE key = 'locale';\nUPDATE user_setting SET key = 'USER_SETTING_APPEARANCE', value = REPLACE(value, '\"', '') WHERE key = 'appearance';\nUPDATE user_setting SET key = 'USER_SETTING_MEMO_VISIBILITY', value = REPLACE(value, '\"', '') WHERE key = 'memo-visibility';\nUPDATE user_setting SET key = 'USER_SETTING_TELEGRAM_USER_ID', value = REPLACE(value, '\"', '') WHERE key = 'telegram-user-id';\n"
  },
  {
    "path": "store/migration/sqlite/0.19/00__add_resource_name.sql",
    "content": "ALTER TABLE memo ADD COLUMN resource_name TEXT NOT NULL DEFAULT \"\";\n\nUPDATE memo SET resource_name = lower(hex(randomblob(8)));\n\nCREATE UNIQUE INDEX idx_memo_resource_name ON memo (resource_name);\n\nALTER TABLE resource ADD COLUMN resource_name TEXT NOT NULL DEFAULT \"\";\n\nUPDATE resource SET resource_name = lower(hex(randomblob(8)));\n\nCREATE UNIQUE INDEX idx_resource_resource_name ON resource (resource_name);\n"
  },
  {
    "path": "store/migration/sqlite/0.2/00__user_role.sql",
    "content": "-- change user role field from \"OWNER\"/\"USER\" to \"HOST\"/\"USER\".\nPRAGMA foreign_keys = off;\n\nDROP TABLE IF EXISTS _user_old;\n\nALTER TABLE\n  user RENAME TO _user_old;\n\nCREATE TABLE user (\n  id INTEGER PRIMARY KEY AUTOINCREMENT,\n  created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),\n  updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),\n  row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',\n  email TEXT NOT NULL UNIQUE,\n  role TEXT NOT NULL CHECK (role IN ('HOST', 'USER')) DEFAULT 'USER',\n  name TEXT NOT NULL,\n  password_hash TEXT NOT NULL,\n  open_id TEXT NOT NULL UNIQUE\n);\n\nINSERT INTO\n  user (\n    id,\n    created_ts,\n    updated_ts,\n    row_status,\n    email,\n    name,\n    password_hash,\n    open_id\n  )\nSELECT\n  id,\n  created_ts,\n  updated_ts,\n  row_status,\n  email,\n  name,\n  password_hash,\n  open_id\nFROM\n  _user_old;\n\nUPDATE\n  user\nSET\n  role = 'HOST'\nWHERE\n  id IN (\n    SELECT\n      id\n    FROM\n      _user_old\n    WHERE\n      role = 'OWNER'\n  );\n\nDROP TABLE IF EXISTS _user_old;\n\nPRAGMA foreign_keys = on;"
  },
  {
    "path": "store/migration/sqlite/0.2/01__memo_visibility.sql",
    "content": "ALTER TABLE\n  memo\nADD\n  COLUMN visibility TEXT NOT NULL CHECK (visibility IN ('PUBLIC', 'PRIVATE')) DEFAULT 'PRIVATE';"
  },
  {
    "path": "store/migration/sqlite/0.20/00__reaction.sql",
    "content": "-- reaction\nCREATE TABLE reaction (\n  id INTEGER PRIMARY KEY AUTOINCREMENT,\n  created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),\n  creator_id INTEGER NOT NULL,\n  content_id TEXT NOT NULL,\n  reaction_type TEXT NOT NULL,\n  UNIQUE(creator_id, content_id, reaction_type)\n);\n"
  },
  {
    "path": "store/migration/sqlite/0.21/00__user_description.sql",
    "content": "ALTER TABLE user ADD COLUMN description TEXT NOT NULL DEFAULT \"\";\n"
  },
  {
    "path": "store/migration/sqlite/0.21/01__rename_uid.sql",
    "content": "ALTER TABLE memo RENAME COLUMN resource_name TO uid;\n\nALTER TABLE resource RENAME COLUMN resource_name TO uid;\n"
  },
  {
    "path": "store/migration/sqlite/0.22/00__resource_storage_type.sql",
    "content": "ALTER TABLE resource ADD COLUMN storage_type TEXT NOT NULL DEFAULT '';\n\nALTER TABLE resource ADD COLUMN reference TEXT NOT NULL DEFAULT '';\n\nALTER TABLE resource ADD COLUMN payload TEXT NOT NULL DEFAULT '{}';\n\nUPDATE resource\nSET storage_type = 'LOCAL', reference = internal_path\nWHERE internal_path IS NOT NULL AND internal_path != '';\n\nUPDATE resource\nSET storage_type = 'EXTERNAL', reference = external_link\nWHERE external_link IS NOT NULL AND external_link != '';\n\nALTER TABLE resource DROP COLUMN internal_path;\n\nALTER TABLE resource DROP COLUMN external_link;\n"
  },
  {
    "path": "store/migration/sqlite/0.22/01__memo_tags.sql",
    "content": "ALTER TABLE memo ADD COLUMN tags TEXT NOT NULL DEFAULT '[]';\n\nCREATE INDEX idx_memo_tags ON memo (tags);\n"
  },
  {
    "path": "store/migration/sqlite/0.22/02__memo_payload.sql",
    "content": "ALTER TABLE memo ADD COLUMN payload TEXT NOT NULL DEFAULT '{}';\n"
  },
  {
    "path": "store/migration/sqlite/0.22/03__drop_tag.sql",
    "content": "DROP TABLE tag;"
  },
  {
    "path": "store/migration/sqlite/0.23/00__reactions.sql",
    "content": "UPDATE `reaction` SET `reaction_type` = '👍' WHERE `reaction_type` = 'THUMBS_UP';\nUPDATE `reaction` SET `reaction_type` = '👎' WHERE `reaction_type` = 'THUMBS_DOWN';\nUPDATE `reaction` SET `reaction_type` = '💛' WHERE `reaction_type` = 'HEART';\nUPDATE `reaction` SET `reaction_type` = '🔥' WHERE `reaction_type` = 'FIRE';\nUPDATE `reaction` SET `reaction_type` = '👏' WHERE `reaction_type` = 'CLAPPING_HANDS';\nUPDATE `reaction` SET `reaction_type` = '😂' WHERE `reaction_type` = 'LAUGH';\nUPDATE `reaction` SET `reaction_type` = '👌' WHERE `reaction_type` = 'OK_HAND';\nUPDATE `reaction` SET `reaction_type` = '🚀' WHERE `reaction_type` = 'ROCKET';\nUPDATE `reaction` SET `reaction_type` = '👀' WHERE `reaction_type` = 'EYES';\nUPDATE `reaction` SET `reaction_type` = '🤔' WHERE `reaction_type` = 'THINKING_FACE';\nUPDATE `reaction` SET `reaction_type` = '🤡' WHERE `reaction_type` = 'CLOWN_FACE';\nUPDATE `reaction` SET `reaction_type` = '❓' WHERE `reaction_type` = 'QUESTION_MARK';\n"
  },
  {
    "path": "store/migration/sqlite/0.24/00__memo.sql",
    "content": "-- Remove deprecated indexes.\nDROP INDEX IF EXISTS idx_memo_tags;\nDROP INDEX IF EXISTS idx_memo_content;\nDROP INDEX IF EXISTS idx_memo_visibility;\n\n-- Drop deprecated tags column.\nALTER TABLE memo DROP COLUMN tags;"
  },
  {
    "path": "store/migration/sqlite/0.24/01__memo_pinned.sql",
    "content": "-- Add pinned column.\nALTER TABLE memo ADD COLUMN pinned INTEGER NOT NULL CHECK (pinned IN (0, 1)) DEFAULT 0;\n\n-- Update pinned column from memo_organizer.\nUPDATE memo\nSET pinned = 1\nWHERE EXISTS (\n    SELECT 1\n    FROM memo_organizer\n    WHERE memo.id = memo_organizer.memo_id AND memo_organizer.pinned = 1\n);\n"
  },
  {
    "path": "store/migration/sqlite/0.25/00__remove_webhook.sql",
    "content": "DROP TABLE IF EXISTS webhook;\n"
  },
  {
    "path": "store/migration/sqlite/0.26/00__rename_resource_to_attachment.sql",
    "content": "ALTER TABLE `resource` RENAME TO `attachment`;\nDROP INDEX IF EXISTS `idx_resource_creator_id`;\nCREATE INDEX `idx_attachment_creator_id` ON `attachment` (`creator_id`);\nDROP INDEX IF EXISTS `idx_resource_memo_id`;\nCREATE INDEX `idx_attachment_memo_id` ON `attachment` (`memo_id`);\n"
  },
  {
    "path": "store/migration/sqlite/0.26/01__drop_memo_organizer.sql",
    "content": "DROP TABLE IF EXISTS memo_organizer;\n"
  },
  {
    "path": "store/migration/sqlite/0.26/02__drop_indexes.sql",
    "content": "DROP INDEX IF EXISTS idx_user_username;\nDROP INDEX IF EXISTS idx_memo_creator_id;\nDROP INDEX IF EXISTS idx_attachment_creator_id;\nDROP INDEX IF EXISTS idx_attachment_memo_id;\n"
  },
  {
    "path": "store/migration/sqlite/0.26/03__alter_user_role.sql",
    "content": "ALTER TABLE user RENAME TO user_old;\n\nCREATE TABLE user (\n  id INTEGER PRIMARY KEY AUTOINCREMENT,\n  created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),\n  updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),\n  row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',\n  username TEXT NOT NULL UNIQUE,\n  role TEXT NOT NULL DEFAULT 'USER',\n  email TEXT NOT NULL DEFAULT '',\n  nickname TEXT NOT NULL DEFAULT '',\n  password_hash TEXT NOT NULL,\n  avatar_url TEXT NOT NULL DEFAULT '',\n  description TEXT NOT NULL DEFAULT ''\n);\n\nINSERT INTO user (\n  id, created_ts, updated_ts, row_status, username, role, email, nickname, password_hash, avatar_url, description\n)\nSELECT\n  id, created_ts, updated_ts, row_status, username, role, email, nickname, password_hash, avatar_url, description\nFROM user_old;\n\nDROP TABLE user_old;\n"
  },
  {
    "path": "store/migration/sqlite/0.26/04__migrate_host_to_admin.sql",
    "content": "UPDATE user SET role = 'ADMIN' WHERE role = 'HOST';\n"
  },
  {
    "path": "store/migration/sqlite/0.27/00__migrate_storage_setting.sql",
    "content": "-- Set storage type to DATABASE for existing instances that have no storage setting configured.\n-- This preserves backward-compatible behavior before the default was changed to LOCAL.\nINSERT INTO system_setting (name, value, description)\nSELECT 'STORAGE', '{\"storageType\":\"DATABASE\"}', ''\nWHERE NOT EXISTS (SELECT 1 FROM system_setting WHERE name = 'STORAGE');\n"
  },
  {
    "path": "store/migration/sqlite/0.27/01__add_idp_uid.sql",
    "content": "-- Add uid column to idp table\nALTER TABLE idp ADD COLUMN uid TEXT NOT NULL DEFAULT '';\n\n-- Populate uid for existing rows using hex of id as a fallback\nUPDATE idp SET uid = printf('%08x', id) WHERE uid = '';\n\n-- Create unique index on uid\nCREATE UNIQUE INDEX IF NOT EXISTS idx_idp_uid ON idp (uid);\n"
  },
  {
    "path": "store/migration/sqlite/0.27/02__migrate_inbox_message_payload.sql",
    "content": "UPDATE inbox\nSET message = json_set(\n  json_remove(message, '$.activityId'),\n  '$.memoComment',\n  json_object(\n    'memoId',\n    (\n      SELECT json_extract(activity.payload, '$.memoComment.memoId')\n      FROM activity\n      WHERE activity.id = json_extract(inbox.message, '$.activityId')\n    ),\n    'relatedMemoId',\n    (\n      SELECT json_extract(activity.payload, '$.memoComment.relatedMemoId')\n      FROM activity\n      WHERE activity.id = json_extract(inbox.message, '$.activityId')\n    )\n  )\n)\nWHERE json_extract(message, '$.activityId') IS NOT NULL\n  AND EXISTS (\n    SELECT 1\n    FROM activity\n    WHERE activity.id = json_extract(inbox.message, '$.activityId')\n  );\n"
  },
  {
    "path": "store/migration/sqlite/0.27/03__drop_activity.sql",
    "content": "DROP TABLE activity;\n"
  },
  {
    "path": "store/migration/sqlite/0.27/04__memo_share.sql",
    "content": "-- memo_share stores per-memo share grants (one row per share link).\n-- uid is the opaque bearer token included in the share URL.\n-- ON DELETE CASCADE ensures grants are cleaned up when the parent memo is deleted.\nCREATE TABLE memo_share (\n  id         INTEGER PRIMARY KEY AUTOINCREMENT,\n  uid        TEXT    NOT NULL UNIQUE,\n  memo_id    INTEGER NOT NULL,\n  creator_id INTEGER NOT NULL,\n  created_ts BIGINT  NOT NULL DEFAULT (strftime('%s', 'now')),\n  expires_ts BIGINT  DEFAULT NULL,\n  FOREIGN KEY (memo_id) REFERENCES memo(id) ON DELETE CASCADE\n);\n\nCREATE INDEX idx_memo_share_memo_id ON memo_share(memo_id);\n"
  },
  {
    "path": "store/migration/sqlite/0.3/00__memo_visibility_protected.sql",
    "content": "-- change memo visibility field from \"PRIVATE\"/\"PUBLIC\" to \"PRIVATE\"/\"PROTECTED\"/\"PUBLIC\".\nPRAGMA foreign_keys = off;\n\nDROP TABLE IF EXISTS _memo_old;\n\nALTER TABLE\n  memo RENAME TO _memo_old;\n\nCREATE TABLE memo (\n  id INTEGER PRIMARY KEY AUTOINCREMENT,\n  creator_id INTEGER NOT NULL,\n  created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),\n  updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),\n  row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',\n  content TEXT NOT NULL DEFAULT '',\n  visibility TEXT NOT NULL CHECK (visibility IN ('PUBLIC', 'PROTECTED', 'PRIVATE')) DEFAULT 'PRIVATE',\n  FOREIGN KEY(creator_id) REFERENCES user(id) ON DELETE CASCADE\n);\n\nINSERT INTO\n  memo (\n    id,\n    creator_id,\n    created_ts,\n    updated_ts,\n    row_status,\n    content,\n    visibility\n  )\nSELECT\n  id,\n  creator_id,\n  created_ts,\n  updated_ts,\n  row_status,\n  content,\n  visibility\nFROM\n  _memo_old;\n\nDROP TABLE IF EXISTS _memo_old;\n\nPRAGMA foreign_keys = on;"
  },
  {
    "path": "store/migration/sqlite/0.4/00__user_setting.sql",
    "content": "-- user_setting\nCREATE TABLE user_setting (\n  user_id INTEGER NOT NULL,\n  key TEXT NOT NULL,\n  value TEXT NOT NULL,\n  FOREIGN KEY(user_id) REFERENCES user(id) ON DELETE CASCADE\n);\n\nCREATE UNIQUE INDEX user_setting_key_user_id_index ON user_setting(key, user_id);"
  },
  {
    "path": "store/migration/sqlite/0.5/00__regenerate_foreign_keys.sql",
    "content": "PRAGMA foreign_keys = off;\n\nDROP TABLE IF EXISTS _user_old;\n\nALTER TABLE\n  user RENAME TO _user_old;\n\n-- user\nCREATE TABLE user (\n  id INTEGER PRIMARY KEY AUTOINCREMENT,\n  created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),\n  updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),\n  row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',\n  email TEXT NOT NULL UNIQUE,\n  role TEXT NOT NULL CHECK (role IN ('HOST', 'USER')) DEFAULT 'USER',\n  name TEXT NOT NULL,\n  password_hash TEXT NOT NULL,\n  open_id TEXT NOT NULL UNIQUE\n);\n\nINSERT INTO\n  user\nSELECT\n  *\nFROM\n  _user_old;\n\nDROP TABLE IF EXISTS _user_old;\n\nDROP TRIGGER IF EXISTS `trigger_update_user_modification_time`;\n\nCREATE TRIGGER IF NOT EXISTS `trigger_update_user_modification_time`\nAFTER\nUPDATE\n  ON `user` FOR EACH ROW BEGIN\nUPDATE\n  `user`\nSET\n  updated_ts = (strftime('%s', 'now'))\nWHERE\n  rowid = old.rowid;\n\nEND;\n\nDROP TABLE IF EXISTS _memo_old;\n\nALTER TABLE\n  memo RENAME TO _memo_old;\n\n-- memo\nCREATE TABLE memo (\n  id INTEGER PRIMARY KEY AUTOINCREMENT,\n  creator_id INTEGER NOT NULL,\n  created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),\n  updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),\n  row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',\n  content TEXT NOT NULL DEFAULT '',\n  visibility TEXT NOT NULL CHECK (visibility IN ('PUBLIC', 'PROTECTED', 'PRIVATE')) DEFAULT 'PRIVATE',\n  FOREIGN KEY(creator_id) REFERENCES user(id) ON DELETE CASCADE\n);\n\nINSERT INTO\n  memo\nSELECT\n  *\nFROM\n  _memo_old;\n\nDROP TABLE IF EXISTS _memo_old;\n\nDROP TRIGGER IF EXISTS `trigger_update_memo_modification_time`;\n\nCREATE TRIGGER IF NOT EXISTS `trigger_update_memo_modification_time`\nAFTER\nUPDATE\n  ON `memo` FOR EACH ROW BEGIN\nUPDATE\n  `memo`\nSET\n  updated_ts = (strftime('%s', 'now'))\nWHERE\n  rowid = old.rowid;\n\nEND;\n\nDROP TABLE IF EXISTS _memo_organizer_old;\n\nALTER TABLE\n  memo_organizer RENAME TO _memo_organizer_old;\n\n-- memo_organizer\nCREATE TABLE memo_organizer (\n  id INTEGER PRIMARY KEY AUTOINCREMENT,\n  memo_id INTEGER NOT NULL,\n  user_id INTEGER NOT NULL,\n  pinned INTEGER NOT NULL CHECK (pinned IN (0, 1)) DEFAULT 0,\n  FOREIGN KEY(memo_id) REFERENCES memo(id) ON DELETE CASCADE,\n  FOREIGN KEY(user_id) REFERENCES user(id) ON DELETE CASCADE,\n  UNIQUE(memo_id, user_id)\n);\n\nINSERT INTO\n  memo_organizer\nSELECT\n  *\nFROM\n  _memo_organizer_old;\n\nDROP TABLE IF EXISTS _memo_organizer_old;\n\nDROP TABLE IF EXISTS _shortcut_old;\n\nALTER TABLE\n  shortcut RENAME TO _shortcut_old;\n\n-- shortcut\nCREATE TABLE shortcut (\n  id INTEGER PRIMARY KEY AUTOINCREMENT,\n  creator_id INTEGER NOT NULL,\n  created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),\n  updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),\n  row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',\n  title TEXT NOT NULL DEFAULT '',\n  payload TEXT NOT NULL DEFAULT '{}',\n  FOREIGN KEY(creator_id) REFERENCES user(id) ON DELETE CASCADE\n);\n\nINSERT INTO\n  shortcut\nSELECT\n  *\nFROM\n  _shortcut_old;\n\nDROP TABLE IF EXISTS _shortcut_old;\n\nDROP TRIGGER IF EXISTS `trigger_update_shortcut_modification_time`;\n\nCREATE TRIGGER IF NOT EXISTS `trigger_update_shortcut_modification_time`\nAFTER\nUPDATE\n  ON `shortcut` FOR EACH ROW BEGIN\nUPDATE\n  `shortcut`\nSET\n  updated_ts = (strftime('%s', 'now'))\nWHERE\n  rowid = old.rowid;\n\nEND;\n\nDROP TABLE IF EXISTS _resource_old;\n\nALTER TABLE\n  resource RENAME TO _resource_old;\n\n-- resource\nCREATE TABLE resource (\n  id INTEGER PRIMARY KEY AUTOINCREMENT,\n  creator_id INTEGER NOT NULL,\n  created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),\n  updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),\n  filename TEXT NOT NULL DEFAULT '',\n  blob BLOB DEFAULT NULL,\n  type TEXT NOT NULL DEFAULT '',\n  size INTEGER NOT NULL DEFAULT 0,\n  FOREIGN KEY(creator_id) REFERENCES user(id) ON DELETE CASCADE\n);\n\nINSERT INTO\n  resource\nSELECT\n  *\nFROM\n  _resource_old;\n\nDROP TABLE IF EXISTS _resource_old;\n\nDROP TRIGGER IF EXISTS `trigger_update_resource_modification_time`;\n\nCREATE TRIGGER IF NOT EXISTS `trigger_update_resource_modification_time`\nAFTER\nUPDATE\n  ON `resource` FOR EACH ROW BEGIN\nUPDATE\n  `resource`\nSET\n  updated_ts = (strftime('%s', 'now'))\nWHERE\n  rowid = old.rowid;\n\nEND;\n\nDROP TABLE IF EXISTS _user_setting_old;\n\nALTER TABLE\n  user_setting RENAME TO _user_setting_old;\n\n-- user_setting\nCREATE TABLE user_setting (\n  user_id INTEGER NOT NULL,\n  key TEXT NOT NULL,\n  value TEXT NOT NULL,\n  FOREIGN KEY(user_id) REFERENCES user(id) ON DELETE CASCADE,\n  UNIQUE(user_id, key)\n);\n\nINSERT INTO\n  user_setting\nSELECT\n  *\nFROM\n  _user_setting_old;\n\nDROP TABLE IF EXISTS _user_setting_old;\n\nPRAGMA foreign_keys = on;"
  },
  {
    "path": "store/migration/sqlite/0.5/01__memo_resource.sql",
    "content": "-- memo_resource\nCREATE TABLE memo_resource (\n  memo_id INTEGER NOT NULL,\n  resource_id INTEGER NOT NULL,\n  created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),\n  updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),\n  FOREIGN KEY(memo_id) REFERENCES memo(id) ON DELETE CASCADE,\n  FOREIGN KEY(resource_id) REFERENCES resource(id) ON DELETE CASCADE,\n  UNIQUE(memo_id, resource_id)\n);"
  },
  {
    "path": "store/migration/sqlite/0.5/02__system_setting.sql",
    "content": "-- system_setting\nCREATE TABLE system_setting (\n  name TEXT NOT NULL,\n  value TEXT NOT NULL,\n  description TEXT NOT NULL DEFAULT '',\n  UNIQUE(name)\n);"
  },
  {
    "path": "store/migration/sqlite/0.5/03__resource_extermal_link.sql",
    "content": "ALTER TABLE\n  resource\nADD\n  COLUMN external_link TEXT NOT NULL DEFAULT '';"
  },
  {
    "path": "store/migration/sqlite/0.6/00__recreate_triggers.sql",
    "content": "DROP TRIGGER IF EXISTS `trigger_update_user_modification_time`;\n\nCREATE TRIGGER IF NOT EXISTS `trigger_update_user_modification_time`\nAFTER\nUPDATE\n  ON `user` FOR EACH ROW BEGIN\nUPDATE\n  `user`\nSET\n  updated_ts = (strftime('%s', 'now'))\nWHERE\n  rowid = old.rowid;\n\nEND;\n\nDROP TRIGGER IF EXISTS `trigger_update_memo_modification_time`;\n\nCREATE TRIGGER IF NOT EXISTS `trigger_update_memo_modification_time`\nAFTER\nUPDATE\n  ON `memo` FOR EACH ROW BEGIN\nUPDATE\n  `memo`\nSET\n  updated_ts = (strftime('%s', 'now'))\nWHERE\n  rowid = old.rowid;\n\nEND;\n\nDROP TRIGGER IF EXISTS `trigger_update_shortcut_modification_time`;\n\nCREATE TRIGGER IF NOT EXISTS `trigger_update_shortcut_modification_time`\nAFTER\nUPDATE\n  ON `shortcut` FOR EACH ROW BEGIN\nUPDATE\n  `shortcut`\nSET\n  updated_ts = (strftime('%s', 'now'))\nWHERE\n  rowid = old.rowid;\n\nEND;\n\nDROP TRIGGER IF EXISTS `trigger_update_resource_modification_time`;\n\nCREATE TRIGGER IF NOT EXISTS `trigger_update_resource_modification_time`\nAFTER\nUPDATE\n  ON `resource` FOR EACH ROW BEGIN\nUPDATE\n  `resource`\nSET\n  updated_ts = (strftime('%s', 'now'))\nWHERE\n  rowid = old.rowid;\n\nEND;"
  },
  {
    "path": "store/migration/sqlite/0.7/00__remove_fk.sql",
    "content": "PRAGMA foreign_keys = off;\n\nDROP TABLE IF EXISTS _user_old;\n\nALTER TABLE\n  user RENAME TO _user_old;\n\n-- user\nCREATE TABLE user (\n  id INTEGER PRIMARY KEY AUTOINCREMENT,\n  created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),\n  updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),\n  row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',\n  email TEXT NOT NULL UNIQUE,\n  role TEXT NOT NULL CHECK (role IN ('HOST', 'USER')) DEFAULT 'USER',\n  name TEXT NOT NULL,\n  password_hash TEXT NOT NULL,\n  open_id TEXT NOT NULL UNIQUE\n);\n\nINSERT INTO\n  user\nSELECT\n  *\nFROM\n  _user_old;\n\nDROP TABLE IF EXISTS _user_old;\n\nDROP TABLE IF EXISTS _memo_old;\n\nALTER TABLE\n  memo RENAME TO _memo_old;\n\n-- memo\nCREATE TABLE memo (\n  id INTEGER PRIMARY KEY AUTOINCREMENT,\n  creator_id INTEGER NOT NULL,\n  created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),\n  updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),\n  row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',\n  content TEXT NOT NULL DEFAULT '',\n  visibility TEXT NOT NULL CHECK (visibility IN ('PUBLIC', 'PROTECTED', 'PRIVATE')) DEFAULT 'PRIVATE'\n);\n\nINSERT INTO\n  memo\nSELECT\n  *\nFROM\n  _memo_old;\n\nDROP TABLE IF EXISTS _memo_old;\n\nDROP TABLE IF EXISTS _memo_organizer_old;\n\nALTER TABLE\n  memo_organizer RENAME TO _memo_organizer_old;\n\n-- memo_organizer\nCREATE TABLE memo_organizer (\n  id INTEGER PRIMARY KEY AUTOINCREMENT,\n  memo_id INTEGER NOT NULL,\n  user_id INTEGER NOT NULL,\n  pinned INTEGER NOT NULL CHECK (pinned IN (0, 1)) DEFAULT 0,\n  UNIQUE(memo_id, user_id)\n);\n\nINSERT INTO\n  memo_organizer\nSELECT\n  *\nFROM\n  _memo_organizer_old;\n\nDROP TABLE IF EXISTS _memo_organizer_old;\n\nDROP TABLE IF EXISTS _shortcut_old;\n\nALTER TABLE\n  shortcut RENAME TO _shortcut_old;\n\n-- shortcut\nCREATE TABLE shortcut (\n  id INTEGER PRIMARY KEY AUTOINCREMENT,\n  creator_id INTEGER NOT NULL,\n  created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),\n  updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),\n  row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',\n  title TEXT NOT NULL DEFAULT '',\n  payload TEXT NOT NULL DEFAULT '{}'\n);\n\nINSERT INTO\n  shortcut\nSELECT\n  *\nFROM\n  _shortcut_old;\n\nDROP TABLE IF EXISTS _shortcut_old;\n\nDROP TABLE IF EXISTS _resource_old;\n\nALTER TABLE\n  resource RENAME TO _resource_old;\n\n-- resource\nCREATE TABLE resource (\n  id INTEGER PRIMARY KEY AUTOINCREMENT,\n  creator_id INTEGER NOT NULL,\n  created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),\n  updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),\n  filename TEXT NOT NULL DEFAULT '',\n  blob BLOB DEFAULT NULL,\n  external_link TEXT NOT NULL DEFAULT '',\n  type TEXT NOT NULL DEFAULT '',\n  size INTEGER NOT NULL DEFAULT 0\n);\n\nINSERT INTO\n  resource (\n    id,\n    creator_id,\n    created_ts,\n    updated_ts,\n    filename,\n    blob,\n    external_link,\n    type,\n    size\n  )\nSELECT\n  id,\n  creator_id,\n  created_ts,\n  updated_ts,\n  filename,\n  blob,\n  external_link,\n  type,\n  size\nFROM\n  _resource_old;\n\nDROP TABLE IF EXISTS _resource_old;\n\nDROP TABLE IF EXISTS _user_setting_old;\n\nALTER TABLE\n  user_setting RENAME TO _user_setting_old;\n\n-- user_setting\nCREATE TABLE user_setting (\n  user_id INTEGER NOT NULL,\n  key TEXT NOT NULL,\n  value TEXT NOT NULL,\n  UNIQUE(user_id, key)\n);\n\nINSERT INTO\n  user_setting\nSELECT\n  *\nFROM\n  _user_setting_old;\n\nDROP TABLE IF EXISTS _user_setting_old;\n\nDROP TABLE IF EXISTS _memo_resource_old;\n\nALTER TABLE\n  memo_resource RENAME TO _memo_resource_old;\n\n-- memo_resource\nCREATE TABLE memo_resource (\n  memo_id INTEGER NOT NULL,\n  resource_id INTEGER NOT NULL,\n  created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),\n  updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),\n  UNIQUE(memo_id, resource_id)\n);\n\nINSERT INTO\n  memo_resource\nSELECT\n  *\nFROM\n  _memo_resource_old;\n\nDROP TABLE IF EXISTS _memo_resource_old;"
  },
  {
    "path": "store/migration/sqlite/0.7/01__remove_triggers.sql",
    "content": "DROP TRIGGER IF EXISTS `trigger_update_user_modification_time`;\n\nDROP TRIGGER IF EXISTS `trigger_update_memo_modification_time`;\n\nDROP TRIGGER IF EXISTS `trigger_update_shortcut_modification_time`;\n\nDROP TRIGGER IF EXISTS `trigger_update_resource_modification_time`;"
  },
  {
    "path": "store/migration/sqlite/0.8/00__migration_history.sql",
    "content": "-- migration_history\nCREATE TABLE IF NOT EXISTS migration_history (\n  version TEXT NOT NULL PRIMARY KEY,\n  created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now'))\n);"
  },
  {
    "path": "store/migration/sqlite/0.8/01__user_username.sql",
    "content": "-- add column username TEXT NOT NULL UNIQUE\n-- rename column name to nickname\n-- add role `ADMIN`\nDROP TABLE IF EXISTS _user_old;\n\nALTER TABLE\n  user RENAME TO _user_old;\n\n-- user\nCREATE TABLE user (\n  id INTEGER PRIMARY KEY AUTOINCREMENT,\n  created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),\n  updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),\n  row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',\n  username TEXT NOT NULL UNIQUE,\n  role TEXT NOT NULL CHECK (role IN ('HOST', 'ADMIN', 'USER')) DEFAULT 'USER',\n  email TEXT NOT NULL DEFAULT '',\n  nickname TEXT NOT NULL DEFAULT '',\n  password_hash TEXT NOT NULL,\n  open_id TEXT NOT NULL UNIQUE\n);\n\nINSERT INTO\n  user (\n    id,\n    created_ts,\n    updated_ts,\n    row_status,\n    username,\n    role,\n    email,\n    nickname,\n    password_hash,\n    open_id\n  )\nSELECT\n  id,\n  created_ts,\n  updated_ts,\n  row_status,\n  email,\n  role,\n  email,\n  name,\n  password_hash,\n  open_id\nFROM\n  _user_old;\n\nDROP TABLE IF EXISTS _user_old;"
  },
  {
    "path": "store/migration/sqlite/0.9/00__tag.sql",
    "content": "-- tag\nCREATE TABLE tag (\n  name TEXT NOT NULL,\n  creator_id INTEGER NOT NULL,\n  UNIQUE(name, creator_id)\n);"
  },
  {
    "path": "store/migration/sqlite/LATEST.sql",
    "content": "-- system_setting\nCREATE TABLE system_setting (\n  name TEXT NOT NULL,\n  value TEXT NOT NULL,\n  description TEXT NOT NULL DEFAULT '',\n  UNIQUE(name)\n);\n\n-- user\nCREATE TABLE user (\n  id INTEGER PRIMARY KEY AUTOINCREMENT,\n  created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),\n  updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),\n  row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',\n  username TEXT NOT NULL UNIQUE,\n  role TEXT NOT NULL DEFAULT 'USER',\n  email TEXT NOT NULL DEFAULT '',\n  nickname TEXT NOT NULL DEFAULT '',\n  password_hash TEXT NOT NULL,\n  avatar_url TEXT NOT NULL DEFAULT '',\n  description TEXT NOT NULL DEFAULT ''\n);\n\n-- user_setting\nCREATE TABLE user_setting (\n  user_id INTEGER NOT NULL,\n  key TEXT NOT NULL,\n  value TEXT NOT NULL,\n  UNIQUE(user_id, key)\n);\n\n-- memo\nCREATE TABLE memo (\n  id INTEGER PRIMARY KEY AUTOINCREMENT,\n  uid TEXT NOT NULL UNIQUE,\n  creator_id INTEGER NOT NULL,\n  created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),\n  updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),\n  row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',\n  content TEXT NOT NULL DEFAULT '',\n  visibility TEXT NOT NULL CHECK (visibility IN ('PUBLIC', 'PROTECTED', 'PRIVATE')) DEFAULT 'PRIVATE',\n  pinned INTEGER NOT NULL CHECK (pinned IN (0, 1)) DEFAULT 0,\n  payload TEXT NOT NULL DEFAULT '{}'\n);\n\n-- memo_relation\nCREATE TABLE memo_relation (\n  memo_id INTEGER NOT NULL,\n  related_memo_id INTEGER NOT NULL,\n  type TEXT NOT NULL,\n  UNIQUE(memo_id, related_memo_id, type)\n);\n\n-- attachment\nCREATE TABLE attachment (\n  id INTEGER PRIMARY KEY AUTOINCREMENT,\n  uid TEXT NOT NULL UNIQUE,\n  creator_id INTEGER NOT NULL,\n  created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),\n  updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),\n  filename TEXT NOT NULL DEFAULT '',\n  blob BLOB DEFAULT NULL,\n  type TEXT NOT NULL DEFAULT '',\n  size INTEGER NOT NULL DEFAULT 0,\n  memo_id INTEGER,\n  storage_type TEXT NOT NULL DEFAULT '',\n  reference TEXT NOT NULL DEFAULT '',\n  payload TEXT NOT NULL DEFAULT '{}'\n);\n\n-- idp\nCREATE TABLE idp (\n  id INTEGER PRIMARY KEY AUTOINCREMENT,\n  uid TEXT NOT NULL UNIQUE,\n  name TEXT NOT NULL,\n  type TEXT NOT NULL,\n  identifier_filter TEXT NOT NULL DEFAULT '',\n  config TEXT NOT NULL DEFAULT '{}'\n);\n\n-- inbox\nCREATE TABLE inbox (\n  id INTEGER PRIMARY KEY AUTOINCREMENT,\n  created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),\n  sender_id INTEGER NOT NULL,\n  receiver_id INTEGER NOT NULL,\n  status TEXT NOT NULL,\n  message TEXT NOT NULL DEFAULT '{}'\n);\n\n-- reaction\nCREATE TABLE reaction (\n  id INTEGER PRIMARY KEY AUTOINCREMENT,\n  created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),\n  creator_id INTEGER NOT NULL,\n  content_id TEXT NOT NULL,\n  reaction_type TEXT NOT NULL,\n  UNIQUE(creator_id, content_id, reaction_type)\n);\n\n-- memo_share\nCREATE TABLE memo_share (\n  id         INTEGER PRIMARY KEY AUTOINCREMENT,\n  uid        TEXT    NOT NULL UNIQUE,\n  memo_id    INTEGER NOT NULL,\n  creator_id INTEGER NOT NULL,\n  created_ts BIGINT  NOT NULL DEFAULT (strftime('%s', 'now')),\n  expires_ts BIGINT  DEFAULT NULL,\n  FOREIGN KEY (memo_id) REFERENCES memo(id) ON DELETE CASCADE\n);\n\nCREATE INDEX idx_memo_share_memo_id ON memo_share(memo_id);\n"
  },
  {
    "path": "store/migrator.go",
    "content": "package store\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"embed\"\n\t\"fmt\"\n\t\"io/fs\"\n\t\"log/slog\"\n\t\"path/filepath\"\n\t\"slices\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/pkg/errors\"\n\n\t\"github.com/usememos/memos/internal/version\"\n\tstorepb \"github.com/usememos/memos/proto/gen/store\"\n)\n\n// Migration System Overview:\n//\n// The migration system handles database schema versioning and upgrades.\n// Schema version is stored in system_setting.\n//\n// Migration Flow:\n// 1. preMigrate: Check if DB is initialized. If not, apply LATEST.sql\n// 2. checkMinimumUpgradeVersion: Verify installation can be upgraded (reject pre-0.22 installations)\n// 3. Migrate (prod mode): Apply incremental migrations from current to target version\n// 4. Migrate (demo mode): Seed database with demo data\n//\n// Version Tracking:\n// - New installations: Schema version set in system_setting immediately\n// - Existing v0.22+ installations: Schema version tracked in system_setting\n// - Pre-v0.22 installations: Must upgrade to v0.25.x first (migration_history → system_setting migration)\n//\n// Migration Files:\n// - Location: store/migration/{driver}/{version}/NN__description.sql\n// - Naming: NN is zero-padded patch number, description is human-readable\n// - Ordering: Files sorted lexicographically and applied in order\n// - LATEST.sql: Full schema for new installations (faster than incremental migrations)\n\n//go:embed migration\nvar migrationFS embed.FS\n\n//go:embed seed\nvar seedFS embed.FS\n\nconst (\n\t// MigrateFileNameSplit is the split character between the patch version and the description in the migration file name.\n\t// For example, \"1__create_table.sql\".\n\tMigrateFileNameSplit = \"__\"\n\t// LatestSchemaFileName is the name of the latest schema file.\n\t// This file is used to initialize fresh installations with the current schema.\n\tLatestSchemaFileName = \"LATEST.sql\"\n\n\t// defaultSchemaVersion is used when schema version is empty or not set.\n\t// This handles edge cases for old installations without version tracking.\n\tdefaultSchemaVersion = \"0.0.0\"\n)\n\n// getSchemaVersionOrDefault returns the schema version or default if empty.\n// This ensures safe version comparisons and handles old installations.\nfunc getSchemaVersionOrDefault(schemaVersion string) string {\n\tif schemaVersion == \"\" {\n\t\treturn defaultSchemaVersion\n\t}\n\treturn schemaVersion\n}\n\n// isVersionEmpty checks if the schema version is empty or the default value.\nfunc isVersionEmpty(schemaVersion string) bool {\n\treturn schemaVersion == \"\" || schemaVersion == defaultSchemaVersion\n}\n\n// shouldApplyMigration determines if a migration file should be applied.\n// It checks if the file's version is between the current DB version and target version.\nfunc shouldApplyMigration(fileVersion, currentDBVersion, targetVersion string) bool {\n\tcurrentDBVersionSafe := getSchemaVersionOrDefault(currentDBVersion)\n\treturn version.IsVersionGreaterThan(fileVersion, currentDBVersionSafe) &&\n\t\tversion.IsVersionGreaterOrEqualThan(targetVersion, fileVersion)\n}\n\n// validateMigrationFileName checks if a migration file follows the expected naming convention.\n// Expected format: \"NN__description.sql\" where NN is a zero-padded number.\nfunc validateMigrationFileName(filename string) error {\n\tparts := strings.SplitN(filename, MigrateFileNameSplit, 2)\n\tif len(parts) < 2 {\n\t\treturn errors.Errorf(\"invalid migration filename format (missing %s): %s\", MigrateFileNameSplit, filename)\n\t}\n\tif _, err := strconv.Atoi(parts[0]); err != nil {\n\t\treturn errors.Errorf(\"migration filename must start with a number: %s\", filename)\n\t}\n\treturn nil\n}\n\n// Migrate migrates the database schema to the latest version.\n// It checks the current schema version and applies any necessary migrations.\n// It also seeds the database with initial data if in demo mode.\nfunc (s *Store) Migrate(ctx context.Context) error {\n\tif err := s.preMigrate(ctx); err != nil {\n\t\treturn errors.Wrap(err, \"failed to pre-migrate\")\n\t}\n\n\tinstanceBasicSetting, err := s.GetInstanceBasicSetting(ctx)\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"failed to get instance basic setting\")\n\t}\n\tcurrentSchemaVersion, err := s.GetCurrentSchemaVersion()\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"failed to get current schema version\")\n\t}\n\t// Check for downgrade (but skip if schema version is empty - that means fresh/old installation)\n\tif !isVersionEmpty(instanceBasicSetting.SchemaVersion) && version.IsVersionGreaterThan(instanceBasicSetting.SchemaVersion, currentSchemaVersion) {\n\t\tslog.Error(\"cannot downgrade schema version\",\n\t\t\tslog.String(\"databaseVersion\", instanceBasicSetting.SchemaVersion),\n\t\t\tslog.String(\"currentVersion\", currentSchemaVersion),\n\t\t)\n\t\treturn errors.Errorf(\"cannot downgrade schema version from %s to %s\", instanceBasicSetting.SchemaVersion, currentSchemaVersion)\n\t}\n\t// Apply migrations if needed.\n\tif isVersionEmpty(instanceBasicSetting.SchemaVersion) || version.IsVersionGreaterThan(currentSchemaVersion, instanceBasicSetting.SchemaVersion) {\n\t\tif err := s.applyMigrations(ctx, instanceBasicSetting.SchemaVersion, currentSchemaVersion); err != nil {\n\t\t\treturn errors.Wrap(err, \"failed to apply migrations\")\n\t\t}\n\t}\n\n\tif s.profile.Demo {\n\t\t// In demo mode, we should seed the database.\n\t\tif err := s.seed(ctx); err != nil {\n\t\t\treturn errors.Wrap(err, \"failed to seed\")\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// applyMigrations applies all necessary migration files between current and target schema versions.\n// It runs all migrations in a single transaction for atomicity.\nfunc (s *Store) applyMigrations(ctx context.Context, currentSchemaVersion, targetSchemaVersion string) error {\n\tfilePaths, err := fs.Glob(migrationFS, fmt.Sprintf(\"%s*/*.sql\", s.getMigrationBasePath()))\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"failed to read migration files\")\n\t}\n\tslices.Sort(filePaths)\n\n\t// Start a transaction to apply migrations atomically\n\ttx, err := s.driver.GetDB().Begin()\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"failed to start transaction\")\n\t}\n\tdefer tx.Rollback()\n\n\t// Use safe version for comparison (handles empty version case)\n\tschemaVersionForComparison := getSchemaVersionOrDefault(currentSchemaVersion)\n\tif isVersionEmpty(currentSchemaVersion) {\n\t\tslog.Warn(\"schema version is empty, treating as default for migration comparison\",\n\t\t\tslog.String(\"defaultVersion\", defaultSchemaVersion))\n\t}\n\n\tslog.Info(\"start migration\",\n\t\tslog.String(\"currentSchemaVersion\", schemaVersionForComparison),\n\t\tslog.String(\"targetSchemaVersion\", targetSchemaVersion))\n\n\tmigrationsApplied := 0\n\tfor _, filePath := range filePaths {\n\t\tfileSchemaVersion, err := s.getSchemaVersionOfMigrateScript(filePath)\n\t\tif err != nil {\n\t\t\treturn errors.Wrap(err, \"failed to get schema version of migrate script\")\n\t\t}\n\n\t\tif shouldApplyMigration(fileSchemaVersion, currentSchemaVersion, targetSchemaVersion) {\n\t\t\t// Validate migration filename before applying\n\t\t\tfilename := filepath.Base(filePath)\n\t\t\tif err := validateMigrationFileName(filename); err != nil {\n\t\t\t\tslog.Warn(\"migration file has invalid name but will be applied\", slog.String(\"file\", filePath), slog.String(\"error\", err.Error()))\n\t\t\t}\n\n\t\t\tslog.Info(\"applying migration\",\n\t\t\t\tslog.String(\"file\", filePath),\n\t\t\t\tslog.String(\"version\", fileSchemaVersion))\n\n\t\t\tbytes, err := migrationFS.ReadFile(filePath)\n\t\t\tif err != nil {\n\t\t\t\treturn errors.Wrapf(err, \"failed to read migration file: %s\", filePath)\n\t\t\t}\n\n\t\t\tstmt := string(bytes)\n\t\t\tif err := s.execute(ctx, tx, stmt); err != nil {\n\t\t\t\treturn errors.Wrapf(err, \"failed to execute migration %s: %s\", filePath, err)\n\t\t\t}\n\t\t\tmigrationsApplied++\n\t\t}\n\t}\n\n\tif err := tx.Commit(); err != nil {\n\t\treturn errors.Wrap(err, \"failed to commit migration transaction\")\n\t}\n\n\tslog.Info(\"migration completed\", slog.Int(\"migrationsApplied\", migrationsApplied))\n\n\t// Update schema version after successful migration\n\tif err := s.updateCurrentSchemaVersion(ctx, targetSchemaVersion); err != nil {\n\t\treturn errors.Wrap(err, \"failed to update current schema version\")\n\t}\n\n\treturn nil\n}\n\n// preMigrate checks if the database is initialized and applies the latest schema if not.\nfunc (s *Store) preMigrate(ctx context.Context) error {\n\tinitialized, err := s.driver.IsInitialized(ctx)\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"failed to check if database is initialized\")\n\t}\n\n\tif !initialized {\n\t\tfilePath := s.getMigrationBasePath() + LatestSchemaFileName\n\t\tbytes, err := migrationFS.ReadFile(filePath)\n\t\tif err != nil {\n\t\t\treturn errors.Errorf(\"failed to read latest schema file: %s\", err)\n\t\t}\n\t\t// Start a transaction to apply the latest schema.\n\t\ttx, err := s.driver.GetDB().Begin()\n\t\tif err != nil {\n\t\t\treturn errors.Wrap(err, \"failed to start transaction\")\n\t\t}\n\t\tdefer tx.Rollback()\n\t\tslog.Info(\"initializing new database with latest schema\", slog.String(\"file\", filePath))\n\t\tif err := s.execute(ctx, tx, string(bytes)); err != nil {\n\t\t\treturn errors.Errorf(\"failed to execute SQL file %s, err %s\", filePath, err)\n\t\t}\n\t\tif err := tx.Commit(); err != nil {\n\t\t\treturn errors.Wrap(err, \"failed to commit transaction\")\n\t\t}\n\n\t\t// Upsert current schema version to database.\n\t\tschemaVersion, err := s.GetCurrentSchemaVersion()\n\t\tif err != nil {\n\t\t\treturn errors.Wrap(err, \"failed to get current schema version\")\n\t\t}\n\t\tslog.Info(\"database initialized successfully\", slog.String(\"schemaVersion\", schemaVersion))\n\t\tif err := s.updateCurrentSchemaVersion(ctx, schemaVersion); err != nil {\n\t\t\treturn errors.Wrap(err, \"failed to update current schema version\")\n\t\t}\n\t}\n\n\tif err := s.checkMinimumUpgradeVersion(ctx); err != nil {\n\t\treturn err // Error message is already descriptive, don't wrap it\n\t}\n\treturn nil\n}\n\nfunc (s *Store) getMigrationBasePath() string {\n\treturn fmt.Sprintf(\"migration/%s/\", s.profile.Driver)\n}\n\nfunc (s *Store) getSeedBasePath() string {\n\treturn fmt.Sprintf(\"seed/%s/\", s.profile.Driver)\n}\n\n// seed seeds the database with initial data.\n// It reads all seed files from the embedded filesystem and executes them in order.\n// This is only supported for SQLite databases and is used in demo mode.\nfunc (s *Store) seed(ctx context.Context) error {\n\t// Only seed for SQLite - other databases should use production data\n\tif s.profile.Driver != \"sqlite\" {\n\t\tslog.Warn(\"seed is only supported for SQLite, skipping for other databases\")\n\t\treturn nil\n\t}\n\n\tfilenames, err := fs.Glob(seedFS, fmt.Sprintf(\"%s*.sql\", s.getSeedBasePath()))\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"failed to read seed files\")\n\t}\n\n\t// Sort seed files by name. This is important to ensure that seed files are applied in order.\n\tslices.Sort(filenames)\n\t// Start a transaction to apply the seed files.\n\ttx, err := s.driver.GetDB().Begin()\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"failed to start transaction\")\n\t}\n\tdefer tx.Rollback()\n\t// Loop over all seed files and execute them in order.\n\tfor _, filename := range filenames {\n\t\tbytes, err := seedFS.ReadFile(filename)\n\t\tif err != nil {\n\t\t\treturn errors.Wrapf(err, \"failed to read seed file, filename=%s\", filename)\n\t\t}\n\t\tif err := s.execute(ctx, tx, string(bytes)); err != nil {\n\t\t\treturn errors.Wrapf(err, \"seed error: %s\", filename)\n\t\t}\n\t}\n\treturn tx.Commit()\n}\n\nfunc (s *Store) GetCurrentSchemaVersion() (string, error) {\n\tcurrentVersion := version.GetCurrentVersion()\n\tminorVersion := version.GetMinorVersion(currentVersion)\n\tfilePaths, err := fs.Glob(migrationFS, fmt.Sprintf(\"%s%s/*.sql\", s.getMigrationBasePath(), minorVersion))\n\tif err != nil {\n\t\treturn \"\", errors.Wrap(err, \"failed to read migration files\")\n\t}\n\n\tslices.Sort(filePaths)\n\tif len(filePaths) == 0 {\n\t\treturn fmt.Sprintf(\"%s.0\", minorVersion), nil\n\t}\n\treturn s.getSchemaVersionOfMigrateScript(filePaths[len(filePaths)-1])\n}\n\n// getSchemaVersionOfMigrateScript extracts the schema version from the migration script file path.\n// It returns the schema version in the format \"major.minor.patch\".\n// If the file is the latest schema file, it returns the current schema version.\nfunc (s *Store) getSchemaVersionOfMigrateScript(filePath string) (string, error) {\n\t// If the file is the latest schema file, return the current schema version.\n\tif strings.HasSuffix(filePath, LatestSchemaFileName) {\n\t\treturn s.GetCurrentSchemaVersion()\n\t}\n\n\tnormalizedPath := filepath.ToSlash(filePath)\n\telements := strings.Split(normalizedPath, \"/\")\n\tif len(elements) < 2 {\n\t\treturn \"\", errors.Errorf(\"invalid file path: %s\", filePath)\n\t}\n\tminorVersion := elements[len(elements)-2]\n\trawPatchVersion := strings.Split(elements[len(elements)-1], MigrateFileNameSplit)[0]\n\tpatchVersion, err := strconv.Atoi(rawPatchVersion)\n\tif err != nil {\n\t\treturn \"\", errors.Wrapf(err, \"failed to convert patch version to int: %s\", rawPatchVersion)\n\t}\n\treturn fmt.Sprintf(\"%s.%d\", minorVersion, patchVersion+1), nil\n}\n\n// execute executes a SQL statement within a transaction context.\n// It returns an error if the execution fails.\nfunc (*Store) execute(ctx context.Context, tx *sql.Tx, stmt string) error {\n\tif _, err := tx.ExecContext(ctx, stmt); err != nil {\n\t\treturn errors.Wrap(err, \"failed to execute statement\")\n\t}\n\treturn nil\n}\n\n// updateCurrentSchemaVersion updates the current schema version in the instance basic setting.\n// It retrieves the instance basic setting, updates the schema version, and upserts the setting back to the database.\nfunc (s *Store) updateCurrentSchemaVersion(ctx context.Context, schemaVersion string) error {\n\tinstanceBasicSetting, err := s.GetInstanceBasicSetting(ctx)\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"failed to get instance basic setting\")\n\t}\n\tinstanceBasicSetting.SchemaVersion = schemaVersion\n\tif _, err := s.UpsertInstanceSetting(ctx, &storepb.InstanceSetting{\n\t\tKey:   storepb.InstanceSettingKey_BASIC,\n\t\tValue: &storepb.InstanceSetting_BasicSetting{BasicSetting: instanceBasicSetting},\n\t}); err != nil {\n\t\treturn errors.Wrap(err, \"failed to upsert instance setting\")\n\t}\n\treturn nil\n}\n\n// checkMinimumUpgradeVersion verifies the installation meets minimum version requirements for upgrade.\n// For very old installations (< v0.22.0), users must upgrade to v0.25.x first before upgrading to current version.\n// This is necessary because schema version tracking was moved from migration_history to system_setting in v0.22.0.\nfunc (s *Store) checkMinimumUpgradeVersion(ctx context.Context) error {\n\tinstanceBasicSetting, err := s.GetInstanceBasicSetting(ctx)\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"failed to get instance basic setting\")\n\t}\n\tschemaVersion := instanceBasicSetting.SchemaVersion\n\n\t// Modern installation: nothing to check.\n\tif !isVersionEmpty(schemaVersion) && version.IsVersionGreaterOrEqualThan(schemaVersion, \"0.22.0\") {\n\t\treturn nil\n\t}\n\n\t// Schema version is empty for fresh installs too, but preMigrate sets it before we get here.\n\t// So empty schema version on an initialized DB means a pre-v0.22 legacy installation.\n\tif isVersionEmpty(schemaVersion) {\n\t\tinitialized, err := s.driver.IsInitialized(ctx)\n\t\tif err != nil {\n\t\t\treturn errors.Wrap(err, \"failed to check if database is initialized\")\n\t\t}\n\t\tif !initialized {\n\t\t\treturn nil\n\t\t}\n\t}\n\n\t// schemaVersion is either set but < 0.22.0, or empty on an initialized (legacy) DB.\n\tcurrentVersion, _ := s.GetCurrentSchemaVersion()\n\treturn errors.Errorf(\n\t\t\"Your Memos installation is too old to upgrade directly.\\n\\n\"+\n\t\t\t\"Your current version: %s\\n\"+\n\t\t\t\"Target version: %s\\n\"+\n\t\t\t\"Minimum required: v0.22.0 (May 2024)\\n\\n\"+\n\t\t\t\"Upgrade path:\\n\"+\n\t\t\t\"1. First upgrade to v0.25.3: https://github.com/usememos/memos/releases/tag/v0.25.3\\n\"+\n\t\t\t\"2. Start the server and verify it works\\n\"+\n\t\t\t\"3. Then upgrade to the latest version\\n\\n\"+\n\t\t\t\"This is required because schema version tracking was moved from migration_history\\n\"+\n\t\t\t\"to system_setting in v0.22.0. The intermediate upgrade handles this migration safely.\",\n\t\tschemaVersion,\n\t\tcurrentVersion,\n\t)\n}\n"
  },
  {
    "path": "store/reaction.go",
    "content": "package store\n\nimport (\n\t\"context\"\n)\n\ntype Reaction struct {\n\tID        int32\n\tCreatedTs int64\n\tCreatorID int32\n\t// ContentID is the id of the content that the reaction is for.\n\tContentID    string\n\tReactionType string\n}\n\ntype FindReaction struct {\n\tID            *int32\n\tCreatorID     *int32\n\tContentID     *string\n\tContentIDList []string\n}\n\ntype DeleteReaction struct {\n\tID int32\n}\n\nfunc (s *Store) UpsertReaction(ctx context.Context, upsert *Reaction) (*Reaction, error) {\n\treturn s.driver.UpsertReaction(ctx, upsert)\n}\n\nfunc (s *Store) ListReactions(ctx context.Context, find *FindReaction) ([]*Reaction, error) {\n\treturn s.driver.ListReactions(ctx, find)\n}\n\nfunc (s *Store) GetReaction(ctx context.Context, find *FindReaction) (*Reaction, error) {\n\treturn s.driver.GetReaction(ctx, find)\n}\n\nfunc (s *Store) DeleteReaction(ctx context.Context, delete *DeleteReaction) error {\n\treturn s.driver.DeleteReaction(ctx, delete)\n}\n"
  },
  {
    "path": "store/seed/DEMO_DATA_GUIDE.md",
    "content": "# Demo Data Guide\n\nThis document describes the demo data used to showcase Memos features in demo mode.\n\n## Overview\n\nThe demo data includes **6 carefully selected memos** that showcase the key features of Memos without overwhelming new users.\n\n## Demo User\n\n- **Username**: `demo`\n- **Password**: `secret` (default password)\n- **Role**: ADMIN\n- **Nickname**: Demo User\n\n## Demo Memos (6 total)\n\n### 1. Welcome Message (Pinned) ⭐\n**Tags**: `#welcome` `#getting-started`\n\nA welcoming introduction that highlights key features of Memos.\n\n**Features showcased**:\n- H1/H2 headings\n- Bold text\n- Bullet lists\n- Horizontal rules\n- Multiple tags\n\n---\n\n### 2. Task Management Demo\n**Tags**: `#todo/work`\n\nRealistic weekly task list with three categories showing different work contexts.\n\n**Features showcased**:\n- Task lists (checkboxes)\n- Hierarchical tags (`#todo/work`)\n- Mixed completed/incomplete tasks\n- H2/H3 headings\n- Multiple sections\n\n---\n\n### 3. Code Snippet Reference\n**Tags**: `#dev/git`\n\nPractical Git commands reference with code examples in multiple languages.\n\n**Features showcased**:\n- Multiple code blocks\n- Bash syntax highlighting\n- JavaScript syntax highlighting\n- Inline code\n- Hierarchical tags (`#dev/git`)\n\n---\n\n### 4. Meeting Notes with Table\n**Tags**: `#meeting/standup`\n\nProfessional meeting notes with structured data in a table format.\n\n**Features showcased**:\n- Markdown tables\n- Bold text\n- Bullet lists\n- Hierarchical tags (`#meeting/standup`)\n- Organized sections\n\n---\n\n### 5. Quick Idea\n**Tags**: `#ideas/apps` `#ai`\n\nShort-form idea capture demonstrating quick note-taking.\n\n**Features showcased**:\n- Brief memo format\n- Emoji usage\n- Multiple tags\n- Bold text\n\n---\n\n### 6. Sponsor Message (Pinned) ⭐\n**Tags**: `#sponsor`\n\nSponsor message with image and external link.\n\n**Features showcased**:\n- External links\n- Markdown image\n- Pinned memo\n- Clean formatting\n\n---\n\n## Additional Features\n\n### Memo Relations\n- Memo #3 (Git commands) references Memo #1 (Welcome)\n\n### Reactions\nMultiple memos have reactions to showcase the reaction system:\n- Welcome: 🎉 👍\n- Tasks: ✅\n- Quick idea: 💡\n- Sponsor: 🚀\n\n### System Settings\nConfigured with popular reactions:\n- 👍 💛 🔥 👏 😂 👌 🚀 👀 🤔 🤡 ❓ +1 🎉 💡 ✅\n\n## Coverage of Markdown Features\n\n| Feature | Demo Memos |\n|---------|-----------|\n| Headings (H1-H3) | 1, 2, 3, 4 |\n| Bold text | All |\n| Links | 6 |\n| Images | 6 |\n| Code blocks | 3 |\n| Inline code | 3 |\n| Task lists | 2 |\n| Bullet lists | 1, 2, 4 |\n| Tables | 4 |\n| Horizontal rules | 1 |\n| Hierarchical tags | All |\n| Emoji | 5 |\n| Pinned memos | 1, 6 |\n\n## Tag Hierarchy\n\nThe demo showcases hierarchical tag organization:\n\n```\n#welcome\n#getting-started\n#todo\n  └─ #todo/work\n#dev\n  └─ #dev/git\n#meeting\n  └─ #meeting/standup\n#ideas\n  └─ #ideas/apps\n#ai\n#sponsor\n```\n\n## Use Cases Demonstrated\n\n1. **Getting Started**: Welcome message with feature overview\n2. **Work Management**: Tasks and meetings\n3. **Developer Tools**: Code snippet references\n4. **Quick Capture**: Brief idea notes\n5. **Sponsor Content**: Product showcases with images\n\n## Design Principles\n\n1. **Quality over Quantity**: 6 focused memos instead of overwhelming users\n2. **Realistic Content**: All memos use realistic, relatable scenarios\n3. **Diverse Use Cases**: Covers professional, technical, and creative contexts\n4. **Visual Appeal**: Clean formatting with emojis used naturally\n5. **Feature Coverage**: Core features demonstrated without redundancy\n6. **Hierarchical Organization**: Shows multi-level tag organization\n7. **Clean and Scannable**: Easy to browse and understand at a glance\n\n## Testing Demo Mode\n\nTo run with demo data:\n\n```bash\n# Start in demo mode\ngo run ./cmd/memos --demo --port 8081\n\n# Or use the binary\n./memos --demo\n\n# Demo database location\n./build/memos_demo.db\n```\n\nLogin with:\n- Username: `demo`\n- Password: `secret`\n\n## Updating Demo Data\n\n1. Edit `store/seed/sqlite/01__dump.sql`\n2. Delete `build/memos_demo.db` if it exists\n3. Restart server in demo mode\n4. New demo data will be loaded automatically\n\n## Notes\n\n- All memos are set to PUBLIC visibility\n- **Two memos are pinned**: Welcome (#1) and Sponsor (#6)\n- User has ADMIN role to showcase all features\n- Reactions are distributed across memos\n- One memo relation demonstrates linking\n- Content is optimized for the compact markdown styles\n- Demo size is intentionally small (6 memos) to avoid overwhelming new users\n"
  },
  {
    "path": "store/seed/sqlite/01__dump.sql",
    "content": "-- Demo User (Admin) — password: demo\nINSERT INTO user (id,username,role,nickname,password_hash) VALUES(1,'demo','ADMIN','Demo User','$2a$10$c.slEVgf5b/3BnAWlLb/vOu7VVSOKJ4ljwMe9xzlx9IhKnvAsJYM6');\n\n-- Alice (User) — password: demo\nINSERT INTO user (id,username,role,nickname,description,password_hash) VALUES(2,'alice','USER','Alice','Developer & avid reader 📚','$2a$10$c.slEVgf5b/3BnAWlLb/vOu7VVSOKJ4ljwMe9xzlx9IhKnvAsJYM6');\n\n-- 1. Welcome Memo (Pinned)\nINSERT INTO memo (id,uid,creator_id,content,visibility,pinned,payload) VALUES(1,'welcome2memos001',1,replace('# Welcome to Memos!\\n\\nAn open-source, self-hosted note-taking tool. Capture thoughts instantly. Own them completely.\\n\\n## Key Features\\n\\n- **Write anything**: Quick notes, long-form writing, technical docs\\n- **Markdown**: Full CommonMark + GFM syntax\\n- **Task Lists**: Track to-dos inline with `- [ ]` syntax\\n- **Tags**: Use #hashtags to organize your memos\\n- **Attachments**: Images, videos, documents — drag & drop\\n- **Location**: Geotag memos to remember where ideas struck\\n- **Reactions & Comments**: Engage with any memo\\n- **Relations**: Connect and reference related memos\\n\\n---\\n\\nExplore the demo memos below to see what''s possible! #welcome #getting-started','\\n',char(10)),'PUBLIC',1,'{\"tags\":[\"welcome\",\"getting-started\"],\"property\":{\"hasLink\":false}}');\n\n-- 2. Sponsor Memo (Pinned)\nINSERT INTO memo (id,uid,creator_id,content,visibility,pinned,payload) VALUES(2,'sponsor0000001',1,replace('Memos is free and open source, made possible by the generous support of our sponsors. 🙏\\n\\n---\\n\\n**[Warp — The AI-powered terminal built for speed and collaboration](https://go.warp.dev/memos)**\\n\\nWarp is a modern terminal reimagined with AI built in — autocomplete commands, debug errors inline, and collaborate with your team without leaving the terminal.\\n\\n<a href=\"https://go.warp.dev/memos\" target=\"_blank\" rel=\"noopener\"><img src=\"https://raw.githubusercontent.com/warpdotdev/brand-assets/refs/heads/main/Logos/Warp-Wordmark-Black.png\" alt=\"Warp - The AI-powered terminal built for speed and collaboration\" height=\"44\" /></a>\\n\\n---\\n\\n**[TestMu AI — The world''s first full-stack Agentic AI Quality Engineering platform](https://www.testmuai.com/?utm_medium=sponsor&utm_source=memos)**\\n\\nTestMu AI brings autonomous AI agents to your QA pipeline — from test generation to execution and reporting, all without manual scripting.\\n\\n<a href=\"https://www.testmuai.com/?utm_medium=sponsor&utm_source=memos\" target=\"_blank\" rel=\"noopener\"><img src=\"https://usememos.com/sponsors/testmu.svg\" alt=\"TestMu AI\" height=\"36\" /></a>\\n\\n---\\n\\n**[SSD Nodes — Affordable VPS hosting for self-hosters](https://ssdnodes.com/?utm_source=memos&utm_medium=sponsor)**\\n\\nHigh-performance VPS servers at prices that make self-hosting a no-brainer. Perfect for running your own Memos instance.\\n\\n<a href=\"https://ssdnodes.com/?utm_source=memos&utm_medium=sponsor\" target=\"_blank\" rel=\"noopener\"><img src=\"https://usememos.com/sponsors/ssd-nodes.svg\" alt=\"SSD Nodes\" height=\"72\" /></a>\\n\\n---\\n\\nInterested in sponsoring? Visit [GitHub Sponsors](https://github.com/sponsors/usememos) to learn more.\\n\\n#sponsors','\\n',char(10)),'PUBLIC',1,'{\"tags\":[\"sponsors\"],\"property\":{\"hasLink\":true}}');\n\n-- 3. AI Skills — boojack/skills workflow, references the example definition doc\nINSERT INTO memo (id,uid,creator_id,content,visibility,payload) VALUES(3,'aiskillsrepo001',1,replace('Been diving into AI agent programming lately — trying to figure out how to make AI actually reliable for complex dev tasks.\\n\\nThe core problem I keep running into: AI starts writing code before it fully understands the problem, then goes off in the wrong direction. The fix is surprisingly simple — force it through a pipeline: define the issue first, then design, then plan, then execute. Each stage has a concrete artifact, so there''s no room to skip ahead.\\n\\n**[boojack/skills](https://github.com/boojack/skills)** packages exactly this into four slash commands — `/defining-issues`, `/writing-designs`, `/planning-tasks`, `/executing-tasks` — that work with Claude Code, Cursor, Gemini CLI, and more.\\n\\n```bash\\nnpx skills add boojack/skills\\n```\\n\\n> 📄 Linked below: an example issue definition generated with `/defining-issues`.\\n\\n#ai #programming','\\n',char(10)),'PUBLIC','{\"tags\":[\"ai\",\"programming\"],\"property\":{\"hasLink\":true,\"hasCode\":true},\"location\":{\"placeholder\":\"San Francisco, California, United States\",\"latitude\":37.7749,\"longitude\":-122.4194}}');\n\n-- 4. Example issue definition doc produced by /defining-issues (referenced by AI Skills memo)\nINSERT INTO memo (id,uid,creator_id,content,visibility,payload) VALUES(4,'markdownshowcs1',1,replace('## 📄 Issue Definition: Add Full-Text Search to Memos #ai #programming\\n\\n*Generated with `/defining-issues` from [boojack/skills](https://github.com/boojack/skills)*\\n\\n---\\n\\n### Background\\n\\nUsers rely on tag filtering and manual scrolling to find memos. As the memo count grows, discoverability becomes a pain point with no way to search by keyword.\\n\\n### Issue Statement\\n\\nThere is no full-text search capability. Users cannot search memo content by keyword, making it hard to resurface older notes or find related ideas.\\n\\n### Current State\\n\\n- Tag-based filtering works via `#hashtag` syntax\\n- No search index exists in the database\\n- The API has no search endpoint\\n- Browsing is limited to chronological scroll or tag drill-down\\n\\n### Proposed Scope\\n\\n- Add a search input to the main UI\\n- Implement SQLite FTS5 full-text indexing on `memo.content`\\n- Return ranked results via `GET /api/memos?search=<query>`\\n- Highlight matched terms in search results\\n\\n### Non-Goals\\n\\n- Semantic / vector search\\n- Search across attachments or comments\\n- Cross-user search for admins\\n\\n### Open Questions\\n\\n1. Should search respect memo visibility (`PUBLIC` / `PRIVATE`)?\\n2. Do we index archived memos?\\n3. Real-time results as-you-type, or on submit?\\n4. Should tags be weighted higher than body text in ranking?','\\n',char(10)),'PUBLIC','{\"tags\":[\"ai\",\"programming\"],\"property\":{\"hasLink\":true,\"hasCode\":true}}');\n\n-- 5. Travel Bucket List (has location: Paris)\nINSERT INTO memo (id,uid,creator_id,content,visibility,payload) VALUES(5,'travelbucket01',1,replace('## 🌍 My Travel Bucket List #travel #bucketlist\\n\\n### Places I''ve Been\\n- [x] Paris, France — Amazing food and art!\\n- [x] Shanghai, China — Modern skyline meets ancient temples\\n- [x] Grand Canyon, USA — Breathtaking views\\n- [x] Barcelona, Spain — Gaudí''s architecture is incredible\\n\\n### Dream Destinations\\n- [ ] Northern Lights in Iceland\\n- [ ] Safari in Tanzania\\n- [ ] Great Barrier Reef, Australia\\n- [ ] Machu Picchu, Peru\\n- [ ] Santorini, Greece\\n- [ ] New Zealand road trip\\n\\n### 2026 Plans\\n- [ ] Book tickets to Iceland for winter\\n- [ ] Research best time to visit Patagonia\\n- [ ] Save up for Australia trip','\\n',char(10)),'PUBLIC','{\"tags\":[\"travel\",\"bucketlist\"],\"property\":{\"hasTaskList\":true,\"hasIncompleteTasks\":true},\"location\":{\"placeholder\":\"Paris, Île-de-France, France\",\"latitude\":48.8566,\"longitude\":2.3522}}');\n\n-- 6. Movie Watchlist — posted by Alice\nINSERT INTO memo (id,uid,creator_id,content,visibility,payload) VALUES(6,'moviewatch00001',2,replace('## 🎬 February Movie Marathon #movies #watchlist\\n\\nCatching up on films I''ve been meaning to watch!\\n\\n### This Month''s Queue\\n\\n| Movie | Genre | Status | Rating |\\n|-------|-------|--------|--------|\\n| The Grand Budapest Hotel | Comedy/Drama | ✅ Watched | ⭐⭐⭐⭐⭐ |\\n| Inception | Sci-Fi | ✅ Watched | ⭐⭐⭐⭐⭐ |\\n| Spirited Away | Animation | ✅ Watched | ⭐⭐⭐⭐⭐ |\\n| Dune: Part Two | Sci-Fi | 📅 This weekend | — |\\n| Oppenheimer | Biography | 📋 Queued | — |\\n\\n### Notes\\n- Grand Budapest Hotel: Wes Anderson''s visual style is *chef''s kiss* ✨\\n- Inception: Need to watch again to catch all the details\\n- Spirited Away: Studio Ghibli never disappoints!\\n\\n---\\n\\n**Next month**: Planning a full Miyazaki marathon 🎨','\\n',char(10)),'PUBLIC','{\"tags\":[\"movies\",\"watchlist\"],\"property\":{\"hasLink\":false}}');\n\n-- 7. Comment on Welcome (by Alice)\nINSERT INTO memo (id,uid,creator_id,content,visibility,payload) VALUES(7,'welcomecmt00001',2,'Just set up my own instance — this is exactly the note-taking app I''ve been looking for! The interface is so clean 🙌','PUBLIC','{\"property\":{\"hasLink\":false}}');\n\n-- 8. Comment on AI Skills (by Alice)\nINSERT INTO memo (id,uid,creator_id,content,visibility,payload) VALUES(8,'aiskillscmt0001',2,'Just tried `/defining-issues` on a backlog item that''s been vague for weeks — the output `definition.md` was clearer than anything I''d written by hand. The \"no solution language\" constraint really forces you to think. 🤯','PUBLIC','{\"property\":{\"hasLink\":false}}');\n\n-- 9. Reply on AI Skills (by Demo)\nINSERT INTO memo (id,uid,creator_id,content,visibility,payload) VALUES(9,'aiskillscmt0002',1,'Exactly — and once you have a solid `definition.md`, `/writing-designs` is scary good. It actually cites real engineering references instead of just making things up 🚀','PUBLIC','{\"property\":{\"hasLink\":false}}');\n\n-- Memo Relations\nINSERT INTO memo_relation VALUES(3,4,'REFERENCE');   -- AI Skills references the example issue definition doc\nINSERT INTO memo_relation VALUES(7,1,'COMMENT');     -- Alice comments on Welcome\nINSERT INTO memo_relation VALUES(8,3,'COMMENT');     -- Alice comments on AI Skills\nINSERT INTO memo_relation VALUES(9,3,'COMMENT');     -- Demo replies on AI Skills\n\n-- Reactions\nINSERT INTO reaction (id,creator_id,content_id,reaction_type) VALUES(1,1,'memos/welcome2memos001','🎉');\nINSERT INTO reaction (id,creator_id,content_id,reaction_type) VALUES(2,2,'memos/welcome2memos001','👍');\nINSERT INTO reaction (id,creator_id,content_id,reaction_type) VALUES(3,1,'memos/welcome2memos001','👏');\nINSERT INTO reaction (id,creator_id,content_id,reaction_type) VALUES(4,2,'memos/aiskillsrepo001','🔥');\nINSERT INTO reaction (id,creator_id,content_id,reaction_type) VALUES(5,1,'memos/aiskillsrepo001','💡');\nINSERT INTO reaction (id,creator_id,content_id,reaction_type) VALUES(6,2,'memos/aiskillsrepo001','👍');\nINSERT INTO reaction (id,creator_id,content_id,reaction_type) VALUES(7,1,'memos/sponsor0000001','🚀');\nINSERT INTO reaction (id,creator_id,content_id,reaction_type) VALUES(8,2,'memos/sponsor0000001','👍');\nINSERT INTO reaction (id,creator_id,content_id,reaction_type) VALUES(9,2,'memos/markdownshowcs1','💡');\nINSERT INTO reaction (id,creator_id,content_id,reaction_type) VALUES(10,2,'memos/travelbucket01','👀');\n\n-- System Settings\nINSERT INTO system_setting VALUES ('MEMO_RELATED', '{\"contentLengthLimit\":8192,\"enableAutoCompact\":true,\"enableComment\":true,\"enableLocation\":true,\"defaultVisibility\":\"PUBLIC\",\"reactions\":[\"👍\",\"💛\",\"🔥\",\"👏\",\"😂\",\"👌\",\"🚀\",\"👀\",\"🤔\",\"🤡\",\"❓\",\"+1\",\"🎉\",\"💡\",\"✅\"]}', '');\n"
  },
  {
    "path": "store/store.go",
    "content": "package store\n\nimport (\n\t\"time\"\n\n\t\"github.com/usememos/memos/internal/profile\"\n\t\"github.com/usememos/memos/store/cache\"\n)\n\n// Store provides database access to all raw objects.\ntype Store struct {\n\tprofile *profile.Profile\n\tdriver  Driver\n\n\t// Cache settings\n\tcacheConfig cache.Config\n\n\t// Caches\n\tinstanceSettingCache *cache.Cache // cache for instance settings\n\tuserCache            *cache.Cache // cache for users\n\tuserSettingCache     *cache.Cache // cache for user settings\n}\n\n// New creates a new instance of Store.\nfunc New(driver Driver, profile *profile.Profile) *Store {\n\t// Default cache settings\n\tcacheConfig := cache.Config{\n\t\tDefaultTTL:      10 * time.Minute,\n\t\tCleanupInterval: 5 * time.Minute,\n\t\tMaxItems:        1000,\n\t\tOnEviction:      nil,\n\t}\n\n\tstore := &Store{\n\t\tdriver:               driver,\n\t\tprofile:              profile,\n\t\tcacheConfig:          cacheConfig,\n\t\tinstanceSettingCache: cache.New(cacheConfig),\n\t\tuserCache:            cache.New(cacheConfig),\n\t\tuserSettingCache:     cache.New(cacheConfig),\n\t}\n\n\treturn store\n}\n\nfunc (s *Store) GetDriver() Driver {\n\treturn s.driver\n}\n\nfunc (s *Store) Close() error {\n\t// Stop all cache cleanup goroutines\n\ts.instanceSettingCache.Close()\n\ts.userCache.Close()\n\ts.userSettingCache.Close()\n\n\treturn s.driver.Close()\n}\n"
  },
  {
    "path": "store/test/README.md",
    "content": "# Store tests\n\n## How to test store with MySQL?\n\n1. Create a database in your MySQL server.\n2. Run the following command with two environment variables set:\n\n```go\nDRIVER=mysql DSN=root@/memos_test go test -v ./test/store/...\n```\n\n- `DRIVER` should be set to `mysql`.\n- `DSN` should be set to the DSN of your MySQL server.\n"
  },
  {
    "path": "store/test/attachment_filter_test.go",
    "content": "package test\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\n// =============================================================================\n// Filename Field Tests\n// Schema: filename (string, supports contains)\n// =============================================================================\n\nfunc TestAttachmentFilterFilenameContains(t *testing.T) {\n\tt.Parallel()\n\ttc := NewAttachmentFilterTestContext(t)\n\tdefer tc.Close()\n\n\ttc.CreateAttachment(NewAttachmentBuilder(tc.CreatorID).Filename(\"report.pdf\").MimeType(\"application/pdf\"))\n\ttc.CreateAttachment(NewAttachmentBuilder(tc.CreatorID).Filename(\"document.pdf\").MimeType(\"application/pdf\"))\n\ttc.CreateAttachment(NewAttachmentBuilder(tc.CreatorID).Filename(\"image.png\").MimeType(\"image/png\"))\n\n\t// Test: filename.contains(\"report\") - single match\n\tattachments := tc.ListWithFilter(`filename.contains(\"report\")`)\n\trequire.Len(t, attachments, 1)\n\trequire.Contains(t, attachments[0].Filename, \"report\")\n\n\t// Test: filename.contains(\".pdf\") - multiple matches\n\tattachments = tc.ListWithFilter(`filename.contains(\".pdf\")`)\n\trequire.Len(t, attachments, 2)\n\n\t// Test: filename.contains(\"nonexistent\") - no matches\n\tattachments = tc.ListWithFilter(`filename.contains(\"nonexistent\")`)\n\trequire.Len(t, attachments, 0)\n}\n\nfunc TestAttachmentFilterFilenameSpecialCharacters(t *testing.T) {\n\tt.Parallel()\n\ttc := NewAttachmentFilterTestContext(t)\n\tdefer tc.Close()\n\n\ttc.CreateAttachment(NewAttachmentBuilder(tc.CreatorID).\n\t\tFilename(\"file_with-special.chars@2024.pdf\").MimeType(\"application/pdf\"))\n\n\t// Test: filename.contains with underscore\n\tattachments := tc.ListWithFilter(`filename.contains(\"_with\")`)\n\trequire.Len(t, attachments, 1)\n\n\t// Test: filename.contains with @\n\tattachments = tc.ListWithFilter(`filename.contains(\"@2024\")`)\n\trequire.Len(t, attachments, 1)\n}\n\nfunc TestAttachmentFilterFilenameUnicode(t *testing.T) {\n\tt.Parallel()\n\ttc := NewAttachmentFilterTestContext(t)\n\tdefer tc.Close()\n\n\ttc.CreateAttachment(NewAttachmentBuilder(tc.CreatorID).\n\t\tFilename(\"document_报告.pdf\").MimeType(\"application/pdf\"))\n\n\tattachments := tc.ListWithFilter(`filename.contains(\"报告\")`)\n\trequire.Len(t, attachments, 1)\n}\n\n// =============================================================================\n// Mime Type Field Tests\n// Schema: mime_type (string, ==, !=)\n// =============================================================================\n\nfunc TestAttachmentFilterMimeTypeEquals(t *testing.T) {\n\tt.Parallel()\n\ttc := NewAttachmentFilterTestContext(t)\n\tdefer tc.Close()\n\n\ttc.CreateAttachment(NewAttachmentBuilder(tc.CreatorID).Filename(\"image.png\").MimeType(\"image/png\"))\n\ttc.CreateAttachment(NewAttachmentBuilder(tc.CreatorID).Filename(\"photo.jpeg\").MimeType(\"image/jpeg\"))\n\ttc.CreateAttachment(NewAttachmentBuilder(tc.CreatorID).Filename(\"document.pdf\").MimeType(\"application/pdf\"))\n\n\t// Test: mime_type == \"image/png\"\n\tattachments := tc.ListWithFilter(`mime_type == \"image/png\"`)\n\trequire.Len(t, attachments, 1)\n\trequire.Equal(t, \"image/png\", attachments[0].Type)\n\n\t// Test: mime_type == \"application/pdf\"\n\tattachments = tc.ListWithFilter(`mime_type == \"application/pdf\"`)\n\trequire.Len(t, attachments, 1)\n\trequire.Equal(t, \"application/pdf\", attachments[0].Type)\n}\n\nfunc TestAttachmentFilterMimeTypeNotEquals(t *testing.T) {\n\tt.Parallel()\n\ttc := NewAttachmentFilterTestContext(t)\n\tdefer tc.Close()\n\n\ttc.CreateAttachment(NewAttachmentBuilder(tc.CreatorID).Filename(\"image.png\").MimeType(\"image/png\"))\n\ttc.CreateAttachment(NewAttachmentBuilder(tc.CreatorID).Filename(\"document.pdf\").MimeType(\"application/pdf\"))\n\n\tattachments := tc.ListWithFilter(`mime_type != \"image/png\"`)\n\trequire.Len(t, attachments, 1)\n\trequire.Equal(t, \"application/pdf\", attachments[0].Type)\n}\n\nfunc TestAttachmentFilterMimeTypeInList(t *testing.T) {\n\tt.Parallel()\n\ttc := NewAttachmentFilterTestContext(t)\n\tdefer tc.Close()\n\n\ttc.CreateAttachment(NewAttachmentBuilder(tc.CreatorID).Filename(\"image.png\").MimeType(\"image/png\"))\n\ttc.CreateAttachment(NewAttachmentBuilder(tc.CreatorID).Filename(\"photo.jpeg\").MimeType(\"image/jpeg\"))\n\ttc.CreateAttachment(NewAttachmentBuilder(tc.CreatorID).Filename(\"document.pdf\").MimeType(\"application/pdf\"))\n\n\t// Test: mime_type in [\"image/png\", \"image/jpeg\"] - matches images\n\tattachments := tc.ListWithFilter(`mime_type in [\"image/png\", \"image/jpeg\"]`)\n\trequire.Len(t, attachments, 2)\n\n\t// Test: mime_type in [\"video/mp4\"] - no matches\n\tattachments = tc.ListWithFilter(`mime_type in [\"video/mp4\"]`)\n\trequire.Len(t, attachments, 0)\n}\n\n// =============================================================================\n// Create Time Field Tests\n// Schema: create_time (timestamp, all comparison operators)\n// Functions: now(), arithmetic (+, -, *)\n// =============================================================================\n\nfunc TestAttachmentFilterCreateTimeComparison(t *testing.T) {\n\tt.Parallel()\n\ttc := NewAttachmentFilterTestContext(t)\n\tdefer tc.Close()\n\n\tnow := time.Now().Unix()\n\ttc.CreateAttachment(NewAttachmentBuilder(tc.CreatorID).Filename(\"test.png\").MimeType(\"image/png\"))\n\n\t// Test: create_time < future (should match)\n\tattachments := tc.ListWithFilter(`create_time < ` + formatInt64(now+3600))\n\trequire.Len(t, attachments, 1)\n\n\t// Test: create_time > past (should match)\n\tattachments = tc.ListWithFilter(`create_time > ` + formatInt64(now-3600))\n\trequire.Len(t, attachments, 1)\n\n\t// Test: create_time > future (should not match)\n\tattachments = tc.ListWithFilter(`create_time > ` + formatInt64(now+3600))\n\trequire.Len(t, attachments, 0)\n}\n\nfunc TestAttachmentFilterCreateTimeWithNow(t *testing.T) {\n\tt.Parallel()\n\ttc := NewAttachmentFilterTestContext(t)\n\tdefer tc.Close()\n\n\ttc.CreateAttachment(NewAttachmentBuilder(tc.CreatorID).Filename(\"test.png\").MimeType(\"image/png\"))\n\n\t// Test: create_time < now() + 5 (buffer for container clock drift)\n\tattachments := tc.ListWithFilter(`create_time < now() + 5`)\n\trequire.Len(t, attachments, 1)\n\n\t// Test: create_time > now() + 5 (should not match)\n\tattachments = tc.ListWithFilter(`create_time > now() + 5`)\n\trequire.Len(t, attachments, 0)\n}\n\nfunc TestAttachmentFilterCreateTimeArithmetic(t *testing.T) {\n\tt.Parallel()\n\ttc := NewAttachmentFilterTestContext(t)\n\tdefer tc.Close()\n\n\ttc.CreateAttachment(NewAttachmentBuilder(tc.CreatorID).Filename(\"test.png\").MimeType(\"image/png\"))\n\n\t// Test: create_time >= now() - 3600 (attachments created in last hour)\n\tattachments := tc.ListWithFilter(`create_time >= now() - 3600`)\n\trequire.Len(t, attachments, 1)\n\n\t// Test: create_time < now() - 86400 (attachments older than 1 day - should be empty)\n\tattachments = tc.ListWithFilter(`create_time < now() - 86400`)\n\trequire.Len(t, attachments, 0)\n\n\t// Test: Multiplication - create_time >= now() - 60 * 60\n\tattachments = tc.ListWithFilter(`create_time >= now() - 60 * 60`)\n\trequire.Len(t, attachments, 1)\n}\n\nfunc TestAttachmentFilterAllComparisonOperators(t *testing.T) {\n\tt.Parallel()\n\ttc := NewAttachmentFilterTestContext(t)\n\tdefer tc.Close()\n\n\ttc.CreateAttachment(NewAttachmentBuilder(tc.CreatorID).Filename(\"test.png\").MimeType(\"image/png\"))\n\n\t// Test: < (less than)\n\tattachments := tc.ListWithFilter(`create_time < now() + 3600`)\n\trequire.Len(t, attachments, 1)\n\n\t// Test: <= (less than or equal) with buffer for clock drift\n\tattachments = tc.ListWithFilter(`create_time < now() + 5`)\n\trequire.Len(t, attachments, 1)\n\n\t// Test: > (greater than)\n\tattachments = tc.ListWithFilter(`create_time > now() - 3600`)\n\trequire.Len(t, attachments, 1)\n\n\t// Test: >= (greater than or equal)\n\tattachments = tc.ListWithFilter(`create_time >= now() - 60`)\n\trequire.Len(t, attachments, 1)\n}\n\n// =============================================================================\n// Memo ID Field Tests\n// Schema: memo_id (int, ==, !=)\n// =============================================================================\n\nfunc TestAttachmentFilterMemoIdEquals(t *testing.T) {\n\tt.Parallel()\n\ttc := NewAttachmentFilterTestContextWithUser(t)\n\tdefer tc.Close()\n\n\tmemo1 := tc.CreateMemo(\"memo-1\", \"Memo 1\")\n\tmemo2 := tc.CreateMemo(\"memo-2\", \"Memo 2\")\n\n\ttc.CreateAttachment(NewAttachmentBuilder(tc.CreatorID).Filename(\"memo1_attachment.png\").MimeType(\"image/png\").MemoID(&memo1.ID))\n\ttc.CreateAttachment(NewAttachmentBuilder(tc.CreatorID).Filename(\"memo2_attachment.png\").MimeType(\"image/png\").MemoID(&memo2.ID))\n\n\tattachments := tc.ListWithFilter(`memo_id == ` + formatInt32(memo1.ID))\n\trequire.Len(t, attachments, 1)\n\trequire.Equal(t, &memo1.ID, attachments[0].MemoID)\n}\n\nfunc TestAttachmentFilterMemoIdNotEquals(t *testing.T) {\n\tt.Parallel()\n\ttc := NewAttachmentFilterTestContextWithUser(t)\n\tdefer tc.Close()\n\n\tmemo1 := tc.CreateMemo(\"memo-1\", \"Memo 1\")\n\tmemo2 := tc.CreateMemo(\"memo-2\", \"Memo 2\")\n\n\ttc.CreateAttachment(NewAttachmentBuilder(tc.CreatorID).Filename(\"memo1_attachment.png\").MimeType(\"image/png\").MemoID(&memo1.ID))\n\ttc.CreateAttachment(NewAttachmentBuilder(tc.CreatorID).Filename(\"memo2_attachment.png\").MimeType(\"image/png\").MemoID(&memo2.ID))\n\n\tattachments := tc.ListWithFilter(`memo_id != ` + formatInt32(memo1.ID))\n\trequire.Len(t, attachments, 1)\n\trequire.Equal(t, &memo2.ID, attachments[0].MemoID)\n}\n\n// =============================================================================\n// Logical Operator Tests\n// Operators: && (AND), || (OR), ! (NOT)\n// =============================================================================\n\nfunc TestAttachmentFilterLogicalAnd(t *testing.T) {\n\tt.Parallel()\n\ttc := NewAttachmentFilterTestContext(t)\n\tdefer tc.Close()\n\n\ttc.CreateAttachment(NewAttachmentBuilder(tc.CreatorID).Filename(\"image.png\").MimeType(\"image/png\"))\n\ttc.CreateAttachment(NewAttachmentBuilder(tc.CreatorID).Filename(\"photo.png\").MimeType(\"image/png\"))\n\ttc.CreateAttachment(NewAttachmentBuilder(tc.CreatorID).Filename(\"image.pdf\").MimeType(\"application/pdf\"))\n\n\tattachments := tc.ListWithFilter(`mime_type == \"image/png\" && filename.contains(\"image\")`)\n\trequire.Len(t, attachments, 1)\n\trequire.Equal(t, \"image.png\", attachments[0].Filename)\n}\n\nfunc TestAttachmentFilterLogicalOr(t *testing.T) {\n\tt.Parallel()\n\ttc := NewAttachmentFilterTestContext(t)\n\tdefer tc.Close()\n\n\ttc.CreateAttachment(NewAttachmentBuilder(tc.CreatorID).Filename(\"image.png\").MimeType(\"image/png\"))\n\ttc.CreateAttachment(NewAttachmentBuilder(tc.CreatorID).Filename(\"document.pdf\").MimeType(\"application/pdf\"))\n\ttc.CreateAttachment(NewAttachmentBuilder(tc.CreatorID).Filename(\"video.mp4\").MimeType(\"video/mp4\"))\n\n\tattachments := tc.ListWithFilter(`mime_type == \"image/png\" || mime_type == \"application/pdf\"`)\n\trequire.Len(t, attachments, 2)\n}\n\nfunc TestAttachmentFilterLogicalNot(t *testing.T) {\n\tt.Parallel()\n\ttc := NewAttachmentFilterTestContext(t)\n\tdefer tc.Close()\n\n\ttc.CreateAttachment(NewAttachmentBuilder(tc.CreatorID).Filename(\"image.png\").MimeType(\"image/png\"))\n\ttc.CreateAttachment(NewAttachmentBuilder(tc.CreatorID).Filename(\"document.pdf\").MimeType(\"application/pdf\"))\n\n\tattachments := tc.ListWithFilter(`!(mime_type == \"image/png\")`)\n\trequire.Len(t, attachments, 1)\n\trequire.Equal(t, \"application/pdf\", attachments[0].Type)\n}\n\nfunc TestAttachmentFilterComplexLogical(t *testing.T) {\n\tt.Parallel()\n\ttc := NewAttachmentFilterTestContext(t)\n\tdefer tc.Close()\n\n\ttc.CreateAttachment(NewAttachmentBuilder(tc.CreatorID).Filename(\"report.png\").MimeType(\"image/png\"))\n\ttc.CreateAttachment(NewAttachmentBuilder(tc.CreatorID).Filename(\"report.pdf\").MimeType(\"application/pdf\"))\n\ttc.CreateAttachment(NewAttachmentBuilder(tc.CreatorID).Filename(\"other.png\").MimeType(\"image/png\"))\n\n\tattachments := tc.ListWithFilter(`(mime_type == \"image/png\" || mime_type == \"application/pdf\") && filename.contains(\"report\")`)\n\trequire.Len(t, attachments, 2)\n}\n\n// =============================================================================\n// Multiple Filters Tests\n// =============================================================================\n\nfunc TestAttachmentFilterMultipleFilters(t *testing.T) {\n\tt.Parallel()\n\ttc := NewAttachmentFilterTestContext(t)\n\tdefer tc.Close()\n\n\ttc.CreateAttachment(NewAttachmentBuilder(tc.CreatorID).Filename(\"report.png\").MimeType(\"image/png\"))\n\ttc.CreateAttachment(NewAttachmentBuilder(tc.CreatorID).Filename(\"other.png\").MimeType(\"image/png\"))\n\ttc.CreateAttachment(NewAttachmentBuilder(tc.CreatorID).Filename(\"report.pdf\").MimeType(\"application/pdf\"))\n\n\t// Test: Multiple filters (applied as AND)\n\tattachments := tc.ListWithFilters(`filename.contains(\"report\")`, `mime_type == \"image/png\"`)\n\trequire.Len(t, attachments, 1)\n\trequire.Contains(t, attachments[0].Filename, \"report\")\n\trequire.Equal(t, \"image/png\", attachments[0].Type)\n}\n\n// =============================================================================\n// Edge Cases\n// =============================================================================\n\nfunc TestAttachmentFilterNoMatches(t *testing.T) {\n\tt.Parallel()\n\ttc := NewAttachmentFilterTestContext(t)\n\tdefer tc.Close()\n\n\ttc.CreateAttachment(NewAttachmentBuilder(tc.CreatorID).Filename(\"test.png\").MimeType(\"image/png\"))\n\n\tattachments := tc.ListWithFilter(`filename.contains(\"nonexistent12345\")`)\n\trequire.Len(t, attachments, 0)\n}\n\nfunc TestAttachmentFilterNullMemoId(t *testing.T) {\n\tt.Parallel()\n\ttc := NewAttachmentFilterTestContextWithUser(t)\n\tdefer tc.Close()\n\n\tmemo := tc.CreateMemo(\"memo-1\", \"Memo 1\")\n\n\ttc.CreateAttachment(NewAttachmentBuilder(tc.CreatorID).Filename(\"with_memo.png\").MimeType(\"image/png\").MemoID(&memo.ID))\n\ttc.CreateAttachment(NewAttachmentBuilder(tc.CreatorID).Filename(\"no_memo.png\").MimeType(\"image/png\"))\n\n\t// Test: memo_id == null\n\tattachments := tc.ListWithFilter(`memo_id == null`)\n\trequire.Len(t, attachments, 1)\n\trequire.Equal(t, \"no_memo.png\", attachments[0].Filename)\n\trequire.Nil(t, attachments[0].MemoID)\n\n\t// Test: memo_id != null\n\tattachments = tc.ListWithFilter(`memo_id != null`)\n\trequire.Len(t, attachments, 1)\n\trequire.Equal(t, \"with_memo.png\", attachments[0].Filename)\n\trequire.NotNil(t, attachments[0].MemoID)\n\trequire.Equal(t, memo.ID, *attachments[0].MemoID)\n}\n\nfunc TestAttachmentFilterEmptyFilename(t *testing.T) {\n\tt.Parallel()\n\ttc := NewAttachmentFilterTestContext(t)\n\tdefer tc.Close()\n\n\ttc.CreateAttachment(NewAttachmentBuilder(tc.CreatorID).Filename(\"test.png\").MimeType(\"image/png\"))\n\ttc.CreateAttachment(NewAttachmentBuilder(tc.CreatorID).Filename(\"other.pdf\").MimeType(\"application/pdf\"))\n\n\t// Test: filename.contains(\"\") - should match all\n\tattachments := tc.ListWithFilter(`filename.contains(\"\")`)\n\trequire.Len(t, attachments, 2)\n}\n"
  },
  {
    "path": "store/test/attachment_test.go",
    "content": "package test\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/lithammer/shortuuid/v4\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/usememos/memos/store\"\n)\n\nfunc TestAttachmentStore(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\t_, err := ts.CreateAttachment(ctx, &store.Attachment{\n\t\tUID:       shortuuid.New(),\n\t\tCreatorID: 101,\n\t\tFilename:  \"test.epub\",\n\t\tBlob:      []byte(\"test\"),\n\t\tType:      \"application/epub+zip\",\n\t\tSize:      637607,\n\t})\n\trequire.NoError(t, err)\n\n\tcorrectFilename := \"test.epub\"\n\tincorrectFilename := \"test.png\"\n\tattachment, err := ts.GetAttachment(ctx, &store.FindAttachment{\n\t\tFilename: &correctFilename,\n\t})\n\trequire.NoError(t, err)\n\trequire.Equal(t, correctFilename, attachment.Filename)\n\trequire.Equal(t, int32(1), attachment.ID)\n\n\tnotFoundAttachment, err := ts.GetAttachment(ctx, &store.FindAttachment{\n\t\tFilename: &incorrectFilename,\n\t})\n\trequire.NoError(t, err)\n\trequire.Nil(t, notFoundAttachment)\n\n\tvar correctCreatorID int32 = 101\n\tvar incorrectCreatorID int32 = 102\n\t_, err = ts.GetAttachment(ctx, &store.FindAttachment{\n\t\tCreatorID: &correctCreatorID,\n\t})\n\trequire.NoError(t, err)\n\n\tnotFoundAttachment, err = ts.GetAttachment(ctx, &store.FindAttachment{\n\t\tCreatorID: &incorrectCreatorID,\n\t})\n\trequire.NoError(t, err)\n\trequire.Nil(t, notFoundAttachment)\n\n\terr = ts.DeleteAttachment(ctx, &store.DeleteAttachment{\n\t\tID: 1,\n\t})\n\trequire.NoError(t, err)\n\terr = ts.DeleteAttachment(ctx, &store.DeleteAttachment{\n\t\tID: 2,\n\t})\n\trequire.ErrorContains(t, err, \"attachment not found\")\n\tts.Close()\n}\n\nfunc TestAttachmentStoreWithFilter(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\n\t_, err := ts.CreateAttachment(ctx, &store.Attachment{\n\t\tUID:       shortuuid.New(),\n\t\tCreatorID: 101,\n\t\tFilename:  \"test.png\",\n\t\tBlob:      []byte(\"test\"),\n\t\tType:      \"image/png\",\n\t\tSize:      1000,\n\t})\n\trequire.NoError(t, err)\n\n\t_, err = ts.CreateAttachment(ctx, &store.Attachment{\n\t\tUID:       shortuuid.New(),\n\t\tCreatorID: 101,\n\t\tFilename:  \"test.jpg\",\n\t\tBlob:      []byte(\"test\"),\n\t\tType:      \"image/jpeg\",\n\t\tSize:      2000,\n\t})\n\trequire.NoError(t, err)\n\n\t_, err = ts.CreateAttachment(ctx, &store.Attachment{\n\t\tUID:       shortuuid.New(),\n\t\tCreatorID: 101,\n\t\tFilename:  \"test.pdf\",\n\t\tBlob:      []byte(\"test\"),\n\t\tType:      \"application/pdf\",\n\t\tSize:      3000,\n\t})\n\trequire.NoError(t, err)\n\n\tattachments, err := ts.ListAttachments(ctx, &store.FindAttachment{\n\t\tCreatorID: &[]int32{101}[0],\n\t\tFilters:   []string{`mime_type == \"image/png\"`},\n\t})\n\trequire.NoError(t, err)\n\trequire.Len(t, attachments, 1)\n\trequire.Equal(t, \"image/png\", attachments[0].Type)\n\n\tattachments, err = ts.ListAttachments(ctx, &store.FindAttachment{\n\t\tCreatorID: &[]int32{101}[0],\n\t\tFilters:   []string{`mime_type in [\"image/png\", \"image/jpeg\"]`},\n\t})\n\trequire.NoError(t, err)\n\trequire.Len(t, attachments, 2)\n\n\tattachments, err = ts.ListAttachments(ctx, &store.FindAttachment{\n\t\tCreatorID: &[]int32{101}[0],\n\t\tFilters:   []string{`filename.contains(\"test\")`},\n\t})\n\trequire.NoError(t, err)\n\trequire.Len(t, attachments, 3)\n\n\tts.Close()\n}\n\nfunc TestAttachmentUpdate(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\n\tattachment, err := ts.CreateAttachment(ctx, &store.Attachment{\n\t\tUID:       shortuuid.New(),\n\t\tCreatorID: 101,\n\t\tFilename:  \"original.png\",\n\t\tBlob:      []byte(\"test\"),\n\t\tType:      \"image/png\",\n\t\tSize:      1000,\n\t})\n\trequire.NoError(t, err)\n\n\t// Update filename\n\tnewFilename := \"updated.png\"\n\terr = ts.UpdateAttachment(ctx, &store.UpdateAttachment{\n\t\tID:       attachment.ID,\n\t\tFilename: &newFilename,\n\t})\n\trequire.NoError(t, err)\n\n\t// Verify update\n\tfound, err := ts.GetAttachment(ctx, &store.FindAttachment{ID: &attachment.ID})\n\trequire.NoError(t, err)\n\trequire.Equal(t, newFilename, found.Filename)\n\n\tts.Close()\n}\n\nfunc TestAttachmentGetByUID(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\n\tuid := shortuuid.New()\n\t_, err := ts.CreateAttachment(ctx, &store.Attachment{\n\t\tUID:       uid,\n\t\tCreatorID: 101,\n\t\tFilename:  \"test.png\",\n\t\tBlob:      []byte(\"test\"),\n\t\tType:      \"image/png\",\n\t\tSize:      1000,\n\t})\n\trequire.NoError(t, err)\n\n\t// Get by UID\n\tfound, err := ts.GetAttachment(ctx, &store.FindAttachment{UID: &uid})\n\trequire.NoError(t, err)\n\trequire.NotNil(t, found)\n\trequire.Equal(t, uid, found.UID)\n\n\t// Get non-existent UID\n\tnonExistentUID := \"non-existent-uid\"\n\tnotFound, err := ts.GetAttachment(ctx, &store.FindAttachment{UID: &nonExistentUID})\n\trequire.NoError(t, err)\n\trequire.Nil(t, notFound)\n\n\tts.Close()\n}\n\nfunc TestAttachmentListWithPagination(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\n\t// Create 5 attachments\n\tfor i := 0; i < 5; i++ {\n\t\t_, err := ts.CreateAttachment(ctx, &store.Attachment{\n\t\t\tUID:       shortuuid.New(),\n\t\t\tCreatorID: 101,\n\t\t\tFilename:  fmt.Sprintf(\"test%d.png\", i),\n\t\t\tBlob:      []byte(\"test\"),\n\t\t\tType:      \"image/png\",\n\t\t\tSize:      int64(1000 + i),\n\t\t})\n\t\trequire.NoError(t, err)\n\t}\n\n\t// Test limit\n\tlimit := 3\n\tattachments, err := ts.ListAttachments(ctx, &store.FindAttachment{\n\t\tCreatorID: &[]int32{101}[0],\n\t\tLimit:     &limit,\n\t})\n\trequire.NoError(t, err)\n\trequire.Equal(t, 3, len(attachments))\n\n\t// Test offset\n\toffset := 2\n\toffsetAttachments, err := ts.ListAttachments(ctx, &store.FindAttachment{\n\t\tCreatorID: &[]int32{101}[0],\n\t\tLimit:     &limit,\n\t\tOffset:    &offset,\n\t})\n\trequire.NoError(t, err)\n\trequire.Equal(t, 3, len(offsetAttachments))\n\n\tts.Close()\n}\n\nfunc TestAttachmentInvalidUID(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\n\t// Create with invalid UID (contains spaces)\n\t_, err := ts.CreateAttachment(ctx, &store.Attachment{\n\t\tUID:       \"invalid uid with spaces\",\n\t\tCreatorID: 101,\n\t\tFilename:  \"test.png\",\n\t\tBlob:      []byte(\"test\"),\n\t\tType:      \"image/png\",\n\t\tSize:      1000,\n\t})\n\trequire.Error(t, err)\n\trequire.Contains(t, err.Error(), \"invalid uid\")\n\n\tts.Close()\n}\n"
  },
  {
    "path": "store/test/containers.go",
    "content": "package test\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/docker/docker/api/types/container\"\n\t\"github.com/pkg/errors\"\n\t\"github.com/testcontainers/testcontainers-go\"\n\t\"github.com/testcontainers/testcontainers-go/modules/mysql\"\n\t\"github.com/testcontainers/testcontainers-go/modules/postgres\"\n\t\"github.com/testcontainers/testcontainers-go/network\"\n\t\"github.com/testcontainers/testcontainers-go/wait\"\n\n\t// Database drivers for connection verification.\n\t_ \"github.com/go-sql-driver/mysql\"\n\t_ \"github.com/lib/pq\"\n)\n\nconst (\n\ttestUser     = \"root\"\n\ttestPassword = \"test\"\n\n\t// Memos container settings for migration testing.\n\tMemosDockerImage   = \"neosmemo/memos\"\n\tStableMemosVersion = \"stable\" // Always points to the latest stable release\n)\n\nvar (\n\tmysqlContainer    atomic.Pointer[mysql.MySQLContainer]\n\tpostgresContainer atomic.Pointer[postgres.PostgresContainer]\n\tmysqlOnce         sync.Once\n\tpostgresOnce      sync.Once\n\tmysqlBaseDSN      atomic.Value // stores string\n\tpostgresBaseDSN   atomic.Value // stores string\n\tdbCounter         atomic.Int64\n\tdbCreationMutex   sync.Mutex // Protects database creation operations\n\n\t// Network for container communication.\n\ttestDockerNetwork atomic.Pointer[testcontainers.DockerNetwork]\n\ttestNetworkOnce   sync.Once\n)\n\n// getTestNetwork creates or returns the shared Docker network for container communication.\nfunc getTestNetwork(ctx context.Context) (*testcontainers.DockerNetwork, error) {\n\tvar networkErr error\n\ttestNetworkOnce.Do(func() {\n\t\tnw, err := network.New(ctx, network.WithDriver(\"bridge\"))\n\t\tif err != nil {\n\t\t\tnetworkErr = err\n\t\t\treturn\n\t\t}\n\t\ttestDockerNetwork.Store(nw)\n\t})\n\treturn testDockerNetwork.Load(), networkErr\n}\n\n// GetMySQLDSN starts a MySQL container (if not already running) and creates a fresh database for this test.\nfunc GetMySQLDSN(t *testing.T) string {\n\tctx := context.Background()\n\n\tmysqlOnce.Do(func() {\n\t\tnw, err := getTestNetwork(ctx)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"failed to create test network: %v\", err)\n\t\t}\n\n\t\tcontainer, err := mysql.Run(ctx,\n\t\t\t\"mysql:8\",\n\t\t\tmysql.WithDatabase(\"init_db\"),\n\t\t\tmysql.WithUsername(\"root\"),\n\t\t\tmysql.WithPassword(testPassword),\n\t\t\ttestcontainers.WithEnv(map[string]string{\n\t\t\t\t\"MYSQL_ROOT_PASSWORD\": testPassword,\n\t\t\t}),\n\t\t\ttestcontainers.WithWaitStrategy(\n\t\t\t\twait.ForAll(\n\t\t\t\t\twait.ForLog(\"ready for connections\").WithOccurrence(2),\n\t\t\t\t\twait.ForListeningPort(\"3306/tcp\"),\n\t\t\t\t).WithDeadline(120*time.Second),\n\t\t\t),\n\t\t\tnetwork.WithNetwork(nil, nw),\n\t\t)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"failed to start MySQL container: %v\", err)\n\t\t}\n\t\tmysqlContainer.Store(container)\n\n\t\tdsn, err := container.ConnectionString(ctx, \"multiStatements=true\")\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"failed to get MySQL connection string: %v\", err)\n\t\t}\n\n\t\tif err := waitForDB(\"mysql\", dsn, 30*time.Second); err != nil {\n\t\t\tt.Fatalf(\"MySQL not ready for connections: %v\", err)\n\t\t}\n\n\t\tmysqlBaseDSN.Store(dsn)\n\t})\n\n\tdsn, ok := mysqlBaseDSN.Load().(string)\n\tif !ok || dsn == \"\" {\n\t\tt.Fatal(\"MySQL container failed to start in a previous test\")\n\t}\n\n\t// Serialize database creation to avoid \"table already exists\" race conditions\n\tdbCreationMutex.Lock()\n\tdefer dbCreationMutex.Unlock()\n\n\t// Create a fresh database for this test\n\tdbName := fmt.Sprintf(\"memos_test_%d\", dbCounter.Add(1))\n\tdb, err := sql.Open(\"mysql\", dsn)\n\tif err != nil {\n\t\tt.Fatalf(\"failed to connect to MySQL: %v\", err)\n\t}\n\tdefer db.Close()\n\n\tif _, err := db.ExecContext(ctx, fmt.Sprintf(\"CREATE DATABASE `%s`\", dbName)); err != nil {\n\t\tt.Fatalf(\"failed to create database %s: %v\", dbName, err)\n\t}\n\n\t// Return DSN pointing to the new database\n\treturn strings.Replace(dsn, \"/init_db?\", \"/\"+dbName+\"?\", 1)\n}\n\n// waitForDB polls the database until it's ready or timeout is reached.\nfunc waitForDB(driver, dsn string, timeout time.Duration) error {\n\tctx, cancel := context.WithTimeout(context.Background(), timeout)\n\tdefer cancel()\n\n\tticker := time.NewTicker(500 * time.Millisecond)\n\tdefer ticker.Stop()\n\n\tvar lastErr error\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\tif lastErr != nil {\n\t\t\t\treturn errors.Errorf(\"timeout waiting for %s database: %v\", driver, lastErr)\n\t\t\t}\n\t\t\treturn errors.Errorf(\"timeout waiting for %s database to be ready\", driver)\n\t\tcase <-ticker.C:\n\t\t\tdb, err := sql.Open(driver, dsn)\n\t\t\tif err != nil {\n\t\t\t\tlastErr = err\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\terr = db.PingContext(ctx)\n\t\t\tdb.Close()\n\t\t\tif err == nil {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\tlastErr = err\n\t\t}\n\t}\n}\n\n// GetPostgresDSN starts a PostgreSQL container (if not already running) and creates a fresh database for this test.\nfunc GetPostgresDSN(t *testing.T) string {\n\tctx := context.Background()\n\n\tpostgresOnce.Do(func() {\n\t\tnw, err := getTestNetwork(ctx)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"failed to create test network: %v\", err)\n\t\t}\n\n\t\tcontainer, err := postgres.Run(ctx,\n\t\t\t\"postgres:18\",\n\t\t\tpostgres.WithDatabase(\"init_db\"),\n\t\t\tpostgres.WithUsername(testUser),\n\t\t\tpostgres.WithPassword(testPassword),\n\t\t\ttestcontainers.WithWaitStrategy(\n\t\t\t\twait.ForAll(\n\t\t\t\t\twait.ForLog(\"database system is ready to accept connections\").WithOccurrence(2),\n\t\t\t\t\twait.ForListeningPort(\"5432/tcp\"),\n\t\t\t\t).WithDeadline(120*time.Second),\n\t\t\t),\n\t\t\tnetwork.WithNetwork(nil, nw),\n\t\t)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"failed to start PostgreSQL container: %v\", err)\n\t\t}\n\t\tpostgresContainer.Store(container)\n\n\t\tdsn, err := container.ConnectionString(ctx, \"sslmode=disable\")\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"failed to get PostgreSQL connection string: %v\", err)\n\t\t}\n\n\t\tif err := waitForDB(\"postgres\", dsn, 30*time.Second); err != nil {\n\t\t\tt.Fatalf(\"PostgreSQL not ready for connections: %v\", err)\n\t\t}\n\n\t\tpostgresBaseDSN.Store(dsn)\n\t})\n\n\tdsn, ok := postgresBaseDSN.Load().(string)\n\tif !ok || dsn == \"\" {\n\t\tt.Fatal(\"PostgreSQL container failed to start in a previous test\")\n\t}\n\n\t// Serialize database creation to avoid \"table already exists\" race conditions\n\tdbCreationMutex.Lock()\n\tdefer dbCreationMutex.Unlock()\n\n\t// Create a fresh database for this test\n\tdbName := fmt.Sprintf(\"memos_test_%d\", dbCounter.Add(1))\n\tdb, err := sql.Open(\"postgres\", dsn)\n\tif err != nil {\n\t\tt.Fatalf(\"failed to connect to PostgreSQL: %v\", err)\n\t}\n\tdefer db.Close()\n\n\tif _, err := db.ExecContext(ctx, fmt.Sprintf(\"CREATE DATABASE %s\", dbName)); err != nil {\n\t\tt.Fatalf(\"failed to create database %s: %v\", dbName, err)\n\t}\n\n\t// Return DSN pointing to the new database\n\treturn strings.Replace(dsn, \"/init_db?\", \"/\"+dbName+\"?\", 1)\n}\n\n// TerminateContainers cleans up all running containers and network.\n// This is typically called from TestMain.\nfunc TerminateContainers() {\n\tctx := context.Background()\n\tif container := mysqlContainer.Load(); container != nil {\n\t\t_ = container.Terminate(ctx)\n\t}\n\tif container := postgresContainer.Load(); container != nil {\n\t\t_ = container.Terminate(ctx)\n\t}\n\tif network := testDockerNetwork.Load(); network != nil {\n\t\t_ = network.Remove(ctx)\n\t}\n}\n\n// MemosContainerConfig holds configuration for starting a Memos container.\ntype MemosContainerConfig struct {\n\tVersion string // Memos version tag (e.g., \"0.24.0\")\n\tDriver  string // Database driver: sqlite, mysql, postgres\n\tDSN     string // Database DSN (for mysql/postgres)\n\tDataDir string // Host directory to mount for SQLite data\n}\n\n// MemosStartupWaitStrategy defines the wait strategy for Memos container startup.\n// Uses regex to match various log message formats across versions.\nvar MemosStartupWaitStrategy = wait.ForAll(\n\twait.ForLog(\"(started successfully|has been started on port)\").AsRegexp(),\n\twait.ForListeningPort(\"5230/tcp\"),\n).WithDeadline(180 * time.Second)\n\n// StartMemosContainer starts a Memos container for migration testing.\n// For SQLite, it mounts the dataDir to /var/opt/memos.\nfunc StartMemosContainer(ctx context.Context, cfg MemosContainerConfig) (testcontainers.Container, error) {\n\tenv := map[string]string{\n\t\t\"MEMOS_MODE\": \"prod\",\n\t}\n\n\tvar opts []testcontainers.ContainerCustomizer\n\n\tswitch cfg.Driver {\n\tcase \"sqlite\":\n\t\tenv[\"MEMOS_DRIVER\"] = \"sqlite\"\n\t\topts = append(opts, testcontainers.WithHostConfigModifier(func(hc *container.HostConfig) {\n\t\t\thc.Binds = append(hc.Binds, fmt.Sprintf(\"%s:%s\", cfg.DataDir, \"/var/opt/memos\"))\n\t\t}))\n\tdefault:\n\t\treturn nil, errors.Errorf(\"unsupported driver for migration testing: %s\", cfg.Driver)\n\t}\n\n\treq := testcontainers.ContainerRequest{\n\t\tImage:        fmt.Sprintf(\"%s:%s\", MemosDockerImage, cfg.Version),\n\t\tEnv:          env,\n\t\tExposedPorts: []string{\"5230/tcp\"},\n\t\tWaitingFor:   MemosStartupWaitStrategy,\n\t\tUser:         fmt.Sprintf(\"%d:%d\", os.Getuid(), os.Getgid()),\n\t}\n\n\t// Use local image if specified\n\tif cfg.Version == \"local\" {\n\t\tif os.Getenv(\"MEMOS_TEST_IMAGE_BUILT\") == \"1\" {\n\t\t\treq.Image = \"memos-test:local\"\n\t\t} else {\n\t\t\treq.Image = \"\"\n\t\t\treq.FromDockerfile = testcontainers.FromDockerfile{\n\t\t\t\tContext:    \"../../\",\n\t\t\t\tDockerfile: \"scripts/Dockerfile\",\n\t\t\t}\n\t\t}\n\t}\n\n\tgenericReq := testcontainers.GenericContainerRequest{\n\t\tContainerRequest: req,\n\t\tStarted:          true,\n\t}\n\n\t// Apply options\n\tfor _, opt := range opts {\n\t\tif err := opt.Customize(&genericReq); err != nil {\n\t\t\treturn nil, errors.Wrap(err, \"failed to apply container option\")\n\t\t}\n\t}\n\n\tctr, err := testcontainers.GenericContainer(ctx, genericReq)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"failed to start memos container\")\n\t}\n\n\treturn ctr, nil\n}\n"
  },
  {
    "path": "store/test/filter_helpers_test.go",
    "content": "package test\n\nimport (\n\t\"context\"\n\t\"strconv\"\n\t\"testing\"\n\n\t\"github.com/lithammer/shortuuid/v4\"\n\t\"github.com/stretchr/testify/require\"\n\n\tstorepb \"github.com/usememos/memos/proto/gen/store\"\n\t\"github.com/usememos/memos/store\"\n)\n\n// =============================================================================\n// Formatting Helpers\n// =============================================================================\n\nfunc formatInt64(n int64) string {\n\treturn strconv.FormatInt(n, 10)\n}\n\nfunc formatInt32(n int32) string {\n\treturn strconv.FormatInt(int64(n), 10)\n}\n\nfunc formatInt(n int) string {\n\treturn strconv.Itoa(n)\n}\n\n// =============================================================================\n// Pointer Helpers\n// =============================================================================\n\nfunc boolPtr(b bool) *bool {\n\treturn &b\n}\n\n// =============================================================================\n// Test Fixture Builders\n// =============================================================================\n\n// MemoBuilder provides a fluent API for creating test memos.\ntype MemoBuilder struct {\n\tmemo *store.Memo\n}\n\n// NewMemoBuilder creates a new memo builder with required fields.\nfunc NewMemoBuilder(uid string, creatorID int32) *MemoBuilder {\n\treturn &MemoBuilder{\n\t\tmemo: &store.Memo{\n\t\t\tUID:        uid,\n\t\t\tCreatorID:  creatorID,\n\t\t\tVisibility: store.Public,\n\t\t},\n\t}\n}\n\nfunc (b *MemoBuilder) Content(content string) *MemoBuilder {\n\tb.memo.Content = content\n\treturn b\n}\n\nfunc (b *MemoBuilder) Visibility(v store.Visibility) *MemoBuilder {\n\tb.memo.Visibility = v\n\treturn b\n}\n\nfunc (b *MemoBuilder) Tags(tags ...string) *MemoBuilder {\n\tif b.memo.Payload == nil {\n\t\tb.memo.Payload = &storepb.MemoPayload{}\n\t}\n\tb.memo.Payload.Tags = tags\n\treturn b\n}\n\nfunc (b *MemoBuilder) Property(fn func(*storepb.MemoPayload_Property)) *MemoBuilder {\n\tif b.memo.Payload == nil {\n\t\tb.memo.Payload = &storepb.MemoPayload{}\n\t}\n\tif b.memo.Payload.Property == nil {\n\t\tb.memo.Payload.Property = &storepb.MemoPayload_Property{}\n\t}\n\tfn(b.memo.Payload.Property)\n\treturn b\n}\n\nfunc (b *MemoBuilder) Build() *store.Memo {\n\treturn b.memo\n}\n\n// AttachmentBuilder provides a fluent API for creating test attachments.\ntype AttachmentBuilder struct {\n\tattachment *store.Attachment\n}\n\n// NewAttachmentBuilder creates a new attachment builder with required fields.\nfunc NewAttachmentBuilder(creatorID int32) *AttachmentBuilder {\n\treturn &AttachmentBuilder{\n\t\tattachment: &store.Attachment{\n\t\t\tUID:       shortuuid.New(),\n\t\t\tCreatorID: creatorID,\n\t\t\tBlob:      []byte(\"test\"),\n\t\t\tSize:      1000,\n\t\t},\n\t}\n}\n\nfunc (b *AttachmentBuilder) Filename(filename string) *AttachmentBuilder {\n\tb.attachment.Filename = filename\n\treturn b\n}\n\nfunc (b *AttachmentBuilder) MimeType(mimeType string) *AttachmentBuilder {\n\tb.attachment.Type = mimeType\n\treturn b\n}\n\nfunc (b *AttachmentBuilder) MemoID(memoID *int32) *AttachmentBuilder {\n\tb.attachment.MemoID = memoID\n\treturn b\n}\n\nfunc (b *AttachmentBuilder) Size(size int64) *AttachmentBuilder {\n\tb.attachment.Size = size\n\treturn b\n}\n\nfunc (b *AttachmentBuilder) Build() *store.Attachment {\n\treturn b.attachment\n}\n\n// =============================================================================\n// Test Context Helpers\n// =============================================================================\n\n// MemoFilterTestContext holds common test dependencies for memo filter tests.\ntype MemoFilterTestContext struct {\n\tCtx   context.Context\n\tT     *testing.T\n\tStore *store.Store\n\tUser  *store.User\n}\n\n// NewMemoFilterTestContext creates a new test context with store and user.\nfunc NewMemoFilterTestContext(t *testing.T) *MemoFilterTestContext {\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\tuser, err := createTestingHostUser(ctx, ts)\n\trequire.NoError(t, err)\n\n\treturn &MemoFilterTestContext{\n\t\tCtx:   ctx,\n\t\tT:     t,\n\t\tStore: ts,\n\t\tUser:  user,\n\t}\n}\n\n// CreateMemo creates a memo using the builder pattern.\nfunc (tc *MemoFilterTestContext) CreateMemo(b *MemoBuilder) *store.Memo {\n\tmemo, err := tc.Store.CreateMemo(tc.Ctx, b.Build())\n\trequire.NoError(tc.T, err)\n\treturn memo\n}\n\n// PinMemo pins a memo by ID.\nfunc (tc *MemoFilterTestContext) PinMemo(memoID int32) {\n\terr := tc.Store.UpdateMemo(tc.Ctx, &store.UpdateMemo{\n\t\tID:     memoID,\n\t\tPinned: boolPtr(true),\n\t})\n\trequire.NoError(tc.T, err)\n}\n\n// ListWithFilter lists memos with the given filter and returns the count.\nfunc (tc *MemoFilterTestContext) ListWithFilter(filter string) []*store.Memo {\n\tmemos, err := tc.Store.ListMemos(tc.Ctx, &store.FindMemo{\n\t\tFilters: []string{filter},\n\t})\n\trequire.NoError(tc.T, err)\n\treturn memos\n}\n\n// ListWithFilters lists memos with multiple filters and returns the count.\nfunc (tc *MemoFilterTestContext) ListWithFilters(filters ...string) []*store.Memo {\n\tmemos, err := tc.Store.ListMemos(tc.Ctx, &store.FindMemo{\n\t\tFilters: filters,\n\t})\n\trequire.NoError(tc.T, err)\n\treturn memos\n}\n\n// Close closes the test store.\nfunc (tc *MemoFilterTestContext) Close() {\n\ttc.Store.Close()\n}\n\n// AttachmentFilterTestContext holds common test dependencies for attachment filter tests.\ntype AttachmentFilterTestContext struct {\n\tCtx       context.Context\n\tT         *testing.T\n\tStore     *store.Store\n\tUser      *store.User\n\tCreatorID int32\n}\n\n// NewAttachmentFilterTestContext creates a new test context for attachments.\nfunc NewAttachmentFilterTestContext(t *testing.T) *AttachmentFilterTestContext {\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\n\treturn &AttachmentFilterTestContext{\n\t\tCtx:       ctx,\n\t\tT:         t,\n\t\tStore:     ts,\n\t\tCreatorID: 101,\n\t}\n}\n\n// NewAttachmentFilterTestContextWithUser creates a new test context with a user.\nfunc NewAttachmentFilterTestContextWithUser(t *testing.T) *AttachmentFilterTestContext {\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\tuser, err := createTestingHostUser(ctx, ts)\n\trequire.NoError(t, err)\n\n\treturn &AttachmentFilterTestContext{\n\t\tCtx:       ctx,\n\t\tT:         t,\n\t\tStore:     ts,\n\t\tUser:      user,\n\t\tCreatorID: user.ID,\n\t}\n}\n\n// CreateAttachment creates an attachment using the builder pattern.\nfunc (tc *AttachmentFilterTestContext) CreateAttachment(b *AttachmentBuilder) *store.Attachment {\n\tattachment, err := tc.Store.CreateAttachment(tc.Ctx, b.Build())\n\trequire.NoError(tc.T, err)\n\treturn attachment\n}\n\n// CreateMemo creates a memo (for attachment tests that need memos).\nfunc (tc *AttachmentFilterTestContext) CreateMemo(uid, content string) *store.Memo {\n\tmemo, err := tc.Store.CreateMemo(tc.Ctx, &store.Memo{\n\t\tUID:        uid,\n\t\tCreatorID:  tc.CreatorID,\n\t\tContent:    content,\n\t\tVisibility: store.Public,\n\t})\n\trequire.NoError(tc.T, err)\n\treturn memo\n}\n\n// ListWithFilter lists attachments with the given filter.\nfunc (tc *AttachmentFilterTestContext) ListWithFilter(filter string) []*store.Attachment {\n\tattachments, err := tc.Store.ListAttachments(tc.Ctx, &store.FindAttachment{\n\t\tCreatorID: &tc.CreatorID,\n\t\tFilters:   []string{filter},\n\t})\n\trequire.NoError(tc.T, err)\n\treturn attachments\n}\n\n// ListWithFilters lists attachments with multiple filters.\nfunc (tc *AttachmentFilterTestContext) ListWithFilters(filters ...string) []*store.Attachment {\n\tattachments, err := tc.Store.ListAttachments(tc.Ctx, &store.FindAttachment{\n\t\tCreatorID: &tc.CreatorID,\n\t\tFilters:   filters,\n\t})\n\trequire.NoError(tc.T, err)\n\treturn attachments\n}\n\n// Close closes the test store.\nfunc (tc *AttachmentFilterTestContext) Close() {\n\ttc.Store.Close()\n}\n\n// =============================================================================\n// Filter Test Case Definition\n// =============================================================================\n\n// FilterTestCase defines a single filter test case for table-driven tests.\ntype FilterTestCase struct {\n\tName          string\n\tFilter        string\n\tExpectedCount int\n}\n"
  },
  {
    "path": "store/test/idp_test.go",
    "content": "package test\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n\n\tstorepb \"github.com/usememos/memos/proto/gen/store\"\n\t\"github.com/usememos/memos/store\"\n)\n\nfunc TestIdentityProviderStore(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\tcreatedIDP, err := ts.CreateIdentityProvider(ctx, &storepb.IdentityProvider{\n\t\tUid:              \"test-github-oauth\",\n\t\tName:             \"GitHub OAuth\",\n\t\tType:             storepb.IdentityProvider_OAUTH2,\n\t\tIdentifierFilter: \"\",\n\t\tConfig: &storepb.IdentityProviderConfig{\n\t\t\tConfig: &storepb.IdentityProviderConfig_Oauth2Config{\n\t\t\t\tOauth2Config: &storepb.OAuth2Config{\n\t\t\t\t\tClientId:     \"client_id\",\n\t\t\t\t\tClientSecret: \"client_secret\",\n\t\t\t\t\tAuthUrl:      \"https://github.com/auth\",\n\t\t\t\t\tTokenUrl:     \"https://github.com/token\",\n\t\t\t\t\tUserInfoUrl:  \"https://github.com/user\",\n\t\t\t\t\tScopes:       []string{\"login\"},\n\t\t\t\t\tFieldMapping: &storepb.FieldMapping{\n\t\t\t\t\t\tIdentifier:  \"login\",\n\t\t\t\t\t\tDisplayName: \"name\",\n\t\t\t\t\t\tEmail:       \"email\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t})\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"test-github-oauth\", createdIDP.Uid)\n\tidp, err := ts.GetIdentityProvider(ctx, &store.FindIdentityProvider{\n\t\tID: &createdIDP.Id,\n\t})\n\trequire.NoError(t, err)\n\trequire.NotNil(t, idp)\n\trequire.Equal(t, createdIDP, idp)\n\tnewName := \"My GitHub OAuth\"\n\tupdatedIdp, err := ts.UpdateIdentityProvider(ctx, &store.UpdateIdentityProviderV1{\n\t\tID:   idp.Id,\n\t\tName: &newName,\n\t})\n\trequire.NoError(t, err)\n\trequire.Equal(t, newName, updatedIdp.Name)\n\terr = ts.DeleteIdentityProvider(ctx, &store.DeleteIdentityProvider{\n\t\tID: idp.Id,\n\t})\n\trequire.NoError(t, err)\n\tidpList, err := ts.ListIdentityProviders(ctx, &store.FindIdentityProvider{})\n\trequire.NoError(t, err)\n\trequire.Equal(t, 0, len(idpList))\n\tts.Close()\n}\n\nfunc TestIdentityProviderGetByID(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\n\t// Create IDP\n\tidp, err := ts.CreateIdentityProvider(ctx, createTestOAuth2IDP(\"Test IDP\", \"test-idp\"))\n\trequire.NoError(t, err)\n\n\t// Get by ID\n\tfound, err := ts.GetIdentityProvider(ctx, &store.FindIdentityProvider{ID: &idp.Id})\n\trequire.NoError(t, err)\n\trequire.NotNil(t, found)\n\trequire.Equal(t, idp.Id, found.Id)\n\trequire.Equal(t, idp.Name, found.Name)\n\n\t// Get by UID\n\tfoundByUID, err := ts.GetIdentityProvider(ctx, &store.FindIdentityProvider{UID: &idp.Uid})\n\trequire.NoError(t, err)\n\trequire.NotNil(t, foundByUID)\n\trequire.Equal(t, idp.Id, foundByUID.Id)\n\trequire.Equal(t, idp.Uid, foundByUID.Uid)\n\n\t// Get by non-existent ID\n\tnonExistentID := int32(99999)\n\tnotFound, err := ts.GetIdentityProvider(ctx, &store.FindIdentityProvider{ID: &nonExistentID})\n\trequire.NoError(t, err)\n\trequire.Nil(t, notFound)\n\n\tts.Close()\n}\n\nfunc TestIdentityProviderListMultiple(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\n\t// Create multiple IDPs\n\t_, err := ts.CreateIdentityProvider(ctx, createTestOAuth2IDP(\"GitHub OAuth\", \"github-oauth\"))\n\trequire.NoError(t, err)\n\t_, err = ts.CreateIdentityProvider(ctx, createTestOAuth2IDP(\"Google OAuth\", \"google-oauth\"))\n\trequire.NoError(t, err)\n\t_, err = ts.CreateIdentityProvider(ctx, createTestOAuth2IDP(\"GitLab OAuth\", \"gitlab-oauth\"))\n\trequire.NoError(t, err)\n\n\t// List all\n\tidpList, err := ts.ListIdentityProviders(ctx, &store.FindIdentityProvider{})\n\trequire.NoError(t, err)\n\trequire.Len(t, idpList, 3)\n\n\tts.Close()\n}\n\nfunc TestIdentityProviderListByID(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\n\t// Create multiple IDPs\n\tidp1, err := ts.CreateIdentityProvider(ctx, createTestOAuth2IDP(\"GitHub OAuth\", \"github-oauth\"))\n\trequire.NoError(t, err)\n\t_, err = ts.CreateIdentityProvider(ctx, createTestOAuth2IDP(\"Google OAuth\", \"google-oauth\"))\n\trequire.NoError(t, err)\n\n\t// List by specific ID\n\tidpList, err := ts.ListIdentityProviders(ctx, &store.FindIdentityProvider{ID: &idp1.Id})\n\trequire.NoError(t, err)\n\trequire.Len(t, idpList, 1)\n\trequire.Equal(t, \"GitHub OAuth\", idpList[0].Name)\n\n\tts.Close()\n}\n\nfunc TestIdentityProviderUpdateName(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\n\tidp, err := ts.CreateIdentityProvider(ctx, createTestOAuth2IDP(\"Original Name\", \"original-name\"))\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"Original Name\", idp.Name)\n\n\t// Update name\n\tnewName := \"Updated Name\"\n\tupdated, err := ts.UpdateIdentityProvider(ctx, &store.UpdateIdentityProviderV1{\n\t\tID:   idp.Id,\n\t\tType: storepb.IdentityProvider_OAUTH2,\n\t\tName: &newName,\n\t})\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"Updated Name\", updated.Name)\n\n\t// Verify update persisted\n\tfound, err := ts.GetIdentityProvider(ctx, &store.FindIdentityProvider{ID: &idp.Id})\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"Updated Name\", found.Name)\n\n\tts.Close()\n}\n\nfunc TestIdentityProviderUpdateIdentifierFilter(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\n\tidp, err := ts.CreateIdentityProvider(ctx, createTestOAuth2IDP(\"Test IDP\", \"test-idp\"))\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"\", idp.IdentifierFilter)\n\n\t// Update identifier filter\n\tnewFilter := \"@example.com$\"\n\tupdated, err := ts.UpdateIdentityProvider(ctx, &store.UpdateIdentityProviderV1{\n\t\tID:               idp.Id,\n\t\tType:             storepb.IdentityProvider_OAUTH2,\n\t\tIdentifierFilter: &newFilter,\n\t})\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"@example.com$\", updated.IdentifierFilter)\n\n\t// Verify update persisted\n\tfound, err := ts.GetIdentityProvider(ctx, &store.FindIdentityProvider{ID: &idp.Id})\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"@example.com$\", found.IdentifierFilter)\n\n\tts.Close()\n}\n\nfunc TestIdentityProviderUpdateConfig(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\n\tidp, err := ts.CreateIdentityProvider(ctx, createTestOAuth2IDP(\"Test IDP\", \"test-idp\"))\n\trequire.NoError(t, err)\n\n\t// Update config\n\tnewConfig := &storepb.IdentityProviderConfig{\n\t\tConfig: &storepb.IdentityProviderConfig_Oauth2Config{\n\t\t\tOauth2Config: &storepb.OAuth2Config{\n\t\t\t\tClientId:     \"new_client_id\",\n\t\t\t\tClientSecret: \"new_client_secret\",\n\t\t\t\tAuthUrl:      \"https://newprovider.com/auth\",\n\t\t\t\tTokenUrl:     \"https://newprovider.com/token\",\n\t\t\t\tUserInfoUrl:  \"https://newprovider.com/user\",\n\t\t\t\tScopes:       []string{\"openid\", \"profile\", \"email\"},\n\t\t\t\tFieldMapping: &storepb.FieldMapping{\n\t\t\t\t\tIdentifier:  \"sub\",\n\t\t\t\t\tDisplayName: \"name\",\n\t\t\t\t\tEmail:       \"email\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\tupdated, err := ts.UpdateIdentityProvider(ctx, &store.UpdateIdentityProviderV1{\n\t\tID:     idp.Id,\n\t\tType:   storepb.IdentityProvider_OAUTH2,\n\t\tConfig: newConfig,\n\t})\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"new_client_id\", updated.Config.GetOauth2Config().ClientId)\n\trequire.Equal(t, \"new_client_secret\", updated.Config.GetOauth2Config().ClientSecret)\n\trequire.Contains(t, updated.Config.GetOauth2Config().Scopes, \"openid\")\n\n\t// Verify update persisted\n\tfound, err := ts.GetIdentityProvider(ctx, &store.FindIdentityProvider{ID: &idp.Id})\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"new_client_id\", found.Config.GetOauth2Config().ClientId)\n\n\tts.Close()\n}\n\nfunc TestIdentityProviderUpdateMultipleFields(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\n\tidp, err := ts.CreateIdentityProvider(ctx, createTestOAuth2IDP(\"Original\", \"original\"))\n\trequire.NoError(t, err)\n\n\t// Update multiple fields at once\n\tnewName := \"Updated IDP\"\n\tnewFilter := \"^admin@\"\n\tupdated, err := ts.UpdateIdentityProvider(ctx, &store.UpdateIdentityProviderV1{\n\t\tID:               idp.Id,\n\t\tType:             storepb.IdentityProvider_OAUTH2,\n\t\tName:             &newName,\n\t\tIdentifierFilter: &newFilter,\n\t})\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"Updated IDP\", updated.Name)\n\trequire.Equal(t, \"^admin@\", updated.IdentifierFilter)\n\n\tts.Close()\n}\n\nfunc TestIdentityProviderDelete(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\n\tidp, err := ts.CreateIdentityProvider(ctx, createTestOAuth2IDP(\"Test IDP\", \"test-idp\"))\n\trequire.NoError(t, err)\n\n\t// Delete\n\terr = ts.DeleteIdentityProvider(ctx, &store.DeleteIdentityProvider{ID: idp.Id})\n\trequire.NoError(t, err)\n\n\t// Verify deletion\n\tfound, err := ts.GetIdentityProvider(ctx, &store.FindIdentityProvider{ID: &idp.Id})\n\trequire.NoError(t, err)\n\trequire.Nil(t, found)\n\n\tts.Close()\n}\n\nfunc TestIdentityProviderDeleteNotAffectOthers(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\n\t// Create multiple IDPs\n\tidp1, err := ts.CreateIdentityProvider(ctx, createTestOAuth2IDP(\"IDP 1\", \"idp-1\"))\n\trequire.NoError(t, err)\n\tidp2, err := ts.CreateIdentityProvider(ctx, createTestOAuth2IDP(\"IDP 2\", \"idp-2\"))\n\trequire.NoError(t, err)\n\n\t// Delete first one\n\terr = ts.DeleteIdentityProvider(ctx, &store.DeleteIdentityProvider{ID: idp1.Id})\n\trequire.NoError(t, err)\n\n\t// Verify second still exists\n\tfound, err := ts.GetIdentityProvider(ctx, &store.FindIdentityProvider{ID: &idp2.Id})\n\trequire.NoError(t, err)\n\trequire.NotNil(t, found)\n\trequire.Equal(t, \"IDP 2\", found.Name)\n\n\t// Verify list only contains second\n\tidpList, err := ts.ListIdentityProviders(ctx, &store.FindIdentityProvider{})\n\trequire.NoError(t, err)\n\trequire.Len(t, idpList, 1)\n\n\tts.Close()\n}\n\nfunc TestIdentityProviderOAuth2ConfigScopes(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\n\t// Create IDP with multiple scopes\n\tidp, err := ts.CreateIdentityProvider(ctx, &storepb.IdentityProvider{\n\t\tUid:  \"multi-scope-oauth\",\n\t\tName: \"Multi-Scope OAuth\",\n\t\tType: storepb.IdentityProvider_OAUTH2,\n\t\tConfig: &storepb.IdentityProviderConfig{\n\t\t\tConfig: &storepb.IdentityProviderConfig_Oauth2Config{\n\t\t\t\tOauth2Config: &storepb.OAuth2Config{\n\t\t\t\t\tClientId:     \"client_id\",\n\t\t\t\t\tClientSecret: \"client_secret\",\n\t\t\t\t\tAuthUrl:      \"https://provider.com/auth\",\n\t\t\t\t\tTokenUrl:     \"https://provider.com/token\",\n\t\t\t\t\tUserInfoUrl:  \"https://provider.com/userinfo\",\n\t\t\t\t\tScopes:       []string{\"openid\", \"profile\", \"email\", \"groups\"},\n\t\t\t\t\tFieldMapping: &storepb.FieldMapping{\n\t\t\t\t\t\tIdentifier:  \"sub\",\n\t\t\t\t\t\tDisplayName: \"name\",\n\t\t\t\t\t\tEmail:       \"email\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t})\n\trequire.NoError(t, err)\n\n\t// Verify scopes are preserved\n\tfound, err := ts.GetIdentityProvider(ctx, &store.FindIdentityProvider{ID: &idp.Id})\n\trequire.NoError(t, err)\n\trequire.Len(t, found.Config.GetOauth2Config().Scopes, 4)\n\trequire.Contains(t, found.Config.GetOauth2Config().Scopes, \"openid\")\n\trequire.Contains(t, found.Config.GetOauth2Config().Scopes, \"groups\")\n\n\tts.Close()\n}\n\nfunc TestIdentityProviderFieldMapping(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\n\t// Create IDP with custom field mapping\n\tidp, err := ts.CreateIdentityProvider(ctx, &storepb.IdentityProvider{\n\t\tUid:  \"custom-field-mapping\",\n\t\tName: \"Custom Field Mapping\",\n\t\tType: storepb.IdentityProvider_OAUTH2,\n\t\tConfig: &storepb.IdentityProviderConfig{\n\t\t\tConfig: &storepb.IdentityProviderConfig_Oauth2Config{\n\t\t\t\tOauth2Config: &storepb.OAuth2Config{\n\t\t\t\t\tClientId:     \"client_id\",\n\t\t\t\t\tClientSecret: \"client_secret\",\n\t\t\t\t\tAuthUrl:      \"https://provider.com/auth\",\n\t\t\t\t\tTokenUrl:     \"https://provider.com/token\",\n\t\t\t\t\tUserInfoUrl:  \"https://provider.com/userinfo\",\n\t\t\t\t\tScopes:       []string{\"login\"},\n\t\t\t\t\tFieldMapping: &storepb.FieldMapping{\n\t\t\t\t\t\tIdentifier:  \"preferred_username\",\n\t\t\t\t\t\tDisplayName: \"full_name\",\n\t\t\t\t\t\tEmail:       \"email_address\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t})\n\trequire.NoError(t, err)\n\n\t// Verify field mapping is preserved\n\tfound, err := ts.GetIdentityProvider(ctx, &store.FindIdentityProvider{ID: &idp.Id})\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"preferred_username\", found.Config.GetOauth2Config().FieldMapping.Identifier)\n\trequire.Equal(t, \"full_name\", found.Config.GetOauth2Config().FieldMapping.DisplayName)\n\trequire.Equal(t, \"email_address\", found.Config.GetOauth2Config().FieldMapping.Email)\n\n\tts.Close()\n}\n\nfunc TestIdentityProviderIdentifierFilterPatterns(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\n\ttestCases := []struct {\n\t\tname   string\n\t\tuid    string\n\t\tfilter string\n\t}{\n\t\t{\"Domain filter\", \"domain-filter\", \"@company\\\\.com$\"},\n\t\t{\"Prefix filter\", \"prefix-filter\", \"^admin_\"},\n\t\t{\"Complex regex\", \"complex-regex\", \"^[a-z]+@(dept1|dept2)\\\\.example\\\\.com$\"},\n\t\t{\"Empty filter\", \"empty-filter\", \"\"},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tidp, err := ts.CreateIdentityProvider(ctx, &storepb.IdentityProvider{\n\t\t\t\tUid:              tc.uid,\n\t\t\t\tName:             tc.name,\n\t\t\t\tType:             storepb.IdentityProvider_OAUTH2,\n\t\t\t\tIdentifierFilter: tc.filter,\n\t\t\t\tConfig: &storepb.IdentityProviderConfig{\n\t\t\t\t\tConfig: &storepb.IdentityProviderConfig_Oauth2Config{\n\t\t\t\t\t\tOauth2Config: &storepb.OAuth2Config{\n\t\t\t\t\t\t\tClientId:     \"client_id\",\n\t\t\t\t\t\t\tClientSecret: \"client_secret\",\n\t\t\t\t\t\t\tAuthUrl:      \"https://provider.com/auth\",\n\t\t\t\t\t\t\tTokenUrl:     \"https://provider.com/token\",\n\t\t\t\t\t\t\tUserInfoUrl:  \"https://provider.com/userinfo\",\n\t\t\t\t\t\t\tScopes:       []string{\"login\"},\n\t\t\t\t\t\t\tFieldMapping: &storepb.FieldMapping{\n\t\t\t\t\t\t\t\tIdentifier: \"sub\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t})\n\t\t\trequire.NoError(t, err)\n\n\t\t\tfound, err := ts.GetIdentityProvider(ctx, &store.FindIdentityProvider{ID: &idp.Id})\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, tc.filter, found.IdentifierFilter)\n\n\t\t\t// Cleanup\n\t\t\terr = ts.DeleteIdentityProvider(ctx, &store.DeleteIdentityProvider{ID: idp.Id})\n\t\t\trequire.NoError(t, err)\n\t\t})\n\t}\n\n\tts.Close()\n}\n\n// Helper function to create a test OAuth2 IDP.\nfunc createTestOAuth2IDP(name, uid string) *storepb.IdentityProvider {\n\treturn &storepb.IdentityProvider{\n\t\tUid:              uid,\n\t\tName:             name,\n\t\tType:             storepb.IdentityProvider_OAUTH2,\n\t\tIdentifierFilter: \"\",\n\t\tConfig: &storepb.IdentityProviderConfig{\n\t\t\tConfig: &storepb.IdentityProviderConfig_Oauth2Config{\n\t\t\t\tOauth2Config: &storepb.OAuth2Config{\n\t\t\t\t\tClientId:     \"client_id\",\n\t\t\t\t\tClientSecret: \"client_secret\",\n\t\t\t\t\tAuthUrl:      \"https://provider.com/auth\",\n\t\t\t\t\tTokenUrl:     \"https://provider.com/token\",\n\t\t\t\t\tUserInfoUrl:  \"https://provider.com/userinfo\",\n\t\t\t\t\tScopes:       []string{\"login\"},\n\t\t\t\t\tFieldMapping: &storepb.FieldMapping{\n\t\t\t\t\t\tIdentifier:  \"login\",\n\t\t\t\t\t\tDisplayName: \"name\",\n\t\t\t\t\t\tEmail:       \"email\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n}\n"
  },
  {
    "path": "store/test/inbox_test.go",
    "content": "package test\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n\n\tstorepb \"github.com/usememos/memos/proto/gen/store\"\n\t\"github.com/usememos/memos/store\"\n)\n\nfunc TestInboxStore(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\tuser, err := createTestingHostUser(ctx, ts)\n\trequire.NoError(t, err)\n\tconst systemBotID int32 = 0\n\tcreate := &store.Inbox{\n\t\tSenderID:   systemBotID,\n\t\tReceiverID: user.ID,\n\t\tStatus:     store.UNREAD,\n\t\tMessage: &storepb.InboxMessage{\n\t\t\tType: storepb.InboxMessage_MEMO_COMMENT,\n\t\t},\n\t}\n\tinbox, err := ts.CreateInbox(ctx, create)\n\trequire.NoError(t, err)\n\trequire.NotNil(t, inbox)\n\trequire.Equal(t, create.Message, inbox.Message)\n\tinboxes, err := ts.ListInboxes(ctx, &store.FindInbox{\n\t\tReceiverID: &user.ID,\n\t})\n\trequire.NoError(t, err)\n\trequire.Equal(t, 1, len(inboxes))\n\trequire.Equal(t, inbox, inboxes[0])\n\tupdatedInbox, err := ts.UpdateInbox(ctx, &store.UpdateInbox{\n\t\tID:     inbox.ID,\n\t\tStatus: store.ARCHIVED,\n\t})\n\trequire.NoError(t, err)\n\trequire.NotNil(t, updatedInbox)\n\trequire.Equal(t, store.ARCHIVED, updatedInbox.Status)\n\terr = ts.DeleteInbox(ctx, &store.DeleteInbox{\n\t\tID: inbox.ID,\n\t})\n\trequire.NoError(t, err)\n\tinboxes, err = ts.ListInboxes(ctx, &store.FindInbox{\n\t\tReceiverID: &user.ID,\n\t})\n\trequire.NoError(t, err)\n\trequire.Equal(t, 0, len(inboxes))\n\tts.Close()\n}\n\nfunc TestInboxListByID(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\tuser, err := createTestingHostUser(ctx, ts)\n\trequire.NoError(t, err)\n\n\tinbox, err := ts.CreateInbox(ctx, &store.Inbox{\n\t\tSenderID:   0,\n\t\tReceiverID: user.ID,\n\t\tStatus:     store.UNREAD,\n\t\tMessage:    &storepb.InboxMessage{Type: storepb.InboxMessage_MEMO_COMMENT},\n\t})\n\trequire.NoError(t, err)\n\n\t// List by ID\n\tinboxes, err := ts.ListInboxes(ctx, &store.FindInbox{ID: &inbox.ID})\n\trequire.NoError(t, err)\n\trequire.Len(t, inboxes, 1)\n\trequire.Equal(t, inbox.ID, inboxes[0].ID)\n\n\t// List by non-existent ID\n\tnonExistentID := int32(99999)\n\tinboxes, err = ts.ListInboxes(ctx, &store.FindInbox{ID: &nonExistentID})\n\trequire.NoError(t, err)\n\trequire.Len(t, inboxes, 0)\n\n\tts.Close()\n}\n\nfunc TestInboxListBySenderID(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\tuser1, err := createTestingHostUser(ctx, ts)\n\trequire.NoError(t, err)\n\tuser2, err := createTestingUserWithRole(ctx, ts, \"user2\", store.RoleUser)\n\trequire.NoError(t, err)\n\n\t// Create inbox from system bot (senderID = 0)\n\t_, err = ts.CreateInbox(ctx, &store.Inbox{\n\t\tSenderID:   0,\n\t\tReceiverID: user1.ID,\n\t\tStatus:     store.UNREAD,\n\t\tMessage:    &storepb.InboxMessage{Type: storepb.InboxMessage_MEMO_COMMENT},\n\t})\n\trequire.NoError(t, err)\n\n\t// Create inbox from user2\n\t_, err = ts.CreateInbox(ctx, &store.Inbox{\n\t\tSenderID:   user2.ID,\n\t\tReceiverID: user1.ID,\n\t\tStatus:     store.UNREAD,\n\t\tMessage:    &storepb.InboxMessage{Type: storepb.InboxMessage_MEMO_COMMENT},\n\t})\n\trequire.NoError(t, err)\n\n\t// List by sender ID = user2\n\tinboxes, err := ts.ListInboxes(ctx, &store.FindInbox{SenderID: &user2.ID})\n\trequire.NoError(t, err)\n\trequire.Len(t, inboxes, 1)\n\trequire.Equal(t, user2.ID, inboxes[0].SenderID)\n\n\t// List by sender ID = 0 (system bot)\n\tsystemBotID := int32(0)\n\tinboxes, err = ts.ListInboxes(ctx, &store.FindInbox{SenderID: &systemBotID})\n\trequire.NoError(t, err)\n\trequire.Len(t, inboxes, 1)\n\trequire.Equal(t, int32(0), inboxes[0].SenderID)\n\n\tts.Close()\n}\n\nfunc TestInboxListByStatus(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\tuser, err := createTestingHostUser(ctx, ts)\n\trequire.NoError(t, err)\n\n\t// Create UNREAD inbox\n\t_, err = ts.CreateInbox(ctx, &store.Inbox{\n\t\tSenderID:   0,\n\t\tReceiverID: user.ID,\n\t\tStatus:     store.UNREAD,\n\t\tMessage:    &storepb.InboxMessage{Type: storepb.InboxMessage_MEMO_COMMENT},\n\t})\n\trequire.NoError(t, err)\n\n\t// Create another inbox and archive it\n\tinbox2, err := ts.CreateInbox(ctx, &store.Inbox{\n\t\tSenderID:   0,\n\t\tReceiverID: user.ID,\n\t\tStatus:     store.UNREAD,\n\t\tMessage:    &storepb.InboxMessage{Type: storepb.InboxMessage_MEMO_COMMENT},\n\t})\n\trequire.NoError(t, err)\n\t_, err = ts.UpdateInbox(ctx, &store.UpdateInbox{ID: inbox2.ID, Status: store.ARCHIVED})\n\trequire.NoError(t, err)\n\n\t// List by UNREAD status\n\tunreadStatus := store.UNREAD\n\tinboxes, err := ts.ListInboxes(ctx, &store.FindInbox{Status: &unreadStatus})\n\trequire.NoError(t, err)\n\trequire.Len(t, inboxes, 1)\n\trequire.Equal(t, store.UNREAD, inboxes[0].Status)\n\n\t// List by ARCHIVED status\n\tarchivedStatus := store.ARCHIVED\n\tinboxes, err = ts.ListInboxes(ctx, &store.FindInbox{Status: &archivedStatus})\n\trequire.NoError(t, err)\n\trequire.Len(t, inboxes, 1)\n\trequire.Equal(t, store.ARCHIVED, inboxes[0].Status)\n\n\tts.Close()\n}\n\nfunc TestInboxListByMessageType(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\tuser, err := createTestingHostUser(ctx, ts)\n\trequire.NoError(t, err)\n\n\t// Create MEMO_COMMENT inboxes\n\t_, err = ts.CreateInbox(ctx, &store.Inbox{\n\t\tSenderID:   0,\n\t\tReceiverID: user.ID,\n\t\tStatus:     store.UNREAD,\n\t\tMessage:    &storepb.InboxMessage{Type: storepb.InboxMessage_MEMO_COMMENT},\n\t})\n\trequire.NoError(t, err)\n\n\t_, err = ts.CreateInbox(ctx, &store.Inbox{\n\t\tSenderID:   0,\n\t\tReceiverID: user.ID,\n\t\tStatus:     store.UNREAD,\n\t\tMessage:    &storepb.InboxMessage{Type: storepb.InboxMessage_MEMO_COMMENT},\n\t})\n\trequire.NoError(t, err)\n\n\t// List by MEMO_COMMENT type\n\tmemoCommentType := storepb.InboxMessage_MEMO_COMMENT\n\tinboxes, err := ts.ListInboxes(ctx, &store.FindInbox{MessageType: &memoCommentType})\n\trequire.NoError(t, err)\n\trequire.Len(t, inboxes, 2)\n\tfor _, inbox := range inboxes {\n\t\trequire.Equal(t, storepb.InboxMessage_MEMO_COMMENT, inbox.Message.Type)\n\t}\n\n\tts.Close()\n}\n\nfunc TestInboxListPagination(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\tuser, err := createTestingHostUser(ctx, ts)\n\trequire.NoError(t, err)\n\n\t// Create 5 inboxes\n\tfor i := 0; i < 5; i++ {\n\t\t_, err = ts.CreateInbox(ctx, &store.Inbox{\n\t\t\tSenderID:   0,\n\t\t\tReceiverID: user.ID,\n\t\t\tStatus:     store.UNREAD,\n\t\t\tMessage:    &storepb.InboxMessage{Type: storepb.InboxMessage_MEMO_COMMENT},\n\t\t})\n\t\trequire.NoError(t, err)\n\t}\n\n\t// Test Limit only\n\tlimit := 3\n\tinboxes, err := ts.ListInboxes(ctx, &store.FindInbox{\n\t\tReceiverID: &user.ID,\n\t\tLimit:      &limit,\n\t})\n\trequire.NoError(t, err)\n\trequire.Len(t, inboxes, 3)\n\n\t// Test Limit + Offset (offset requires limit in the implementation)\n\tlimit = 2\n\toffset := 2\n\tinboxes, err = ts.ListInboxes(ctx, &store.FindInbox{\n\t\tReceiverID: &user.ID,\n\t\tLimit:      &limit,\n\t\tOffset:     &offset,\n\t})\n\trequire.NoError(t, err)\n\trequire.Len(t, inboxes, 2)\n\n\t// Test Limit + Offset skipping to end\n\tlimit = 10\n\toffset = 3\n\tinboxes, err = ts.ListInboxes(ctx, &store.FindInbox{\n\t\tReceiverID: &user.ID,\n\t\tLimit:      &limit,\n\t\tOffset:     &offset,\n\t})\n\trequire.NoError(t, err)\n\trequire.Len(t, inboxes, 2) // Only 2 remaining after offset of 3\n\n\tts.Close()\n}\n\nfunc TestInboxListCombinedFilters(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\tuser1, err := createTestingHostUser(ctx, ts)\n\trequire.NoError(t, err)\n\tuser2, err := createTestingUserWithRole(ctx, ts, \"user2\", store.RoleUser)\n\trequire.NoError(t, err)\n\n\t// Create various inboxes\n\t// user2 -> user1, MEMO_COMMENT, UNREAD\n\t_, err = ts.CreateInbox(ctx, &store.Inbox{\n\t\tSenderID:   user2.ID,\n\t\tReceiverID: user1.ID,\n\t\tStatus:     store.UNREAD,\n\t\tMessage:    &storepb.InboxMessage{Type: storepb.InboxMessage_MEMO_COMMENT},\n\t})\n\trequire.NoError(t, err)\n\n\t// user2 -> user1, TYPE_UNSPECIFIED, UNREAD\n\t_, err = ts.CreateInbox(ctx, &store.Inbox{\n\t\tSenderID:   user2.ID,\n\t\tReceiverID: user1.ID,\n\t\tStatus:     store.UNREAD,\n\t\tMessage:    &storepb.InboxMessage{Type: storepb.InboxMessage_TYPE_UNSPECIFIED},\n\t})\n\trequire.NoError(t, err)\n\n\t// system -> user1, MEMO_COMMENT, ARCHIVED\n\tinbox3, err := ts.CreateInbox(ctx, &store.Inbox{\n\t\tSenderID:   0,\n\t\tReceiverID: user1.ID,\n\t\tStatus:     store.UNREAD,\n\t\tMessage:    &storepb.InboxMessage{Type: storepb.InboxMessage_MEMO_COMMENT},\n\t})\n\trequire.NoError(t, err)\n\t_, err = ts.UpdateInbox(ctx, &store.UpdateInbox{ID: inbox3.ID, Status: store.ARCHIVED})\n\trequire.NoError(t, err)\n\n\t// Combined filter: ReceiverID + SenderID + Status\n\tunreadStatus := store.UNREAD\n\tinboxes, err := ts.ListInboxes(ctx, &store.FindInbox{\n\t\tReceiverID: &user1.ID,\n\t\tSenderID:   &user2.ID,\n\t\tStatus:     &unreadStatus,\n\t})\n\trequire.NoError(t, err)\n\trequire.Len(t, inboxes, 2)\n\n\t// Combined filter: ReceiverID + MessageType + Status\n\tmemoCommentType := storepb.InboxMessage_MEMO_COMMENT\n\tinboxes, err = ts.ListInboxes(ctx, &store.FindInbox{\n\t\tReceiverID:  &user1.ID,\n\t\tMessageType: &memoCommentType,\n\t\tStatus:      &unreadStatus,\n\t})\n\trequire.NoError(t, err)\n\trequire.Len(t, inboxes, 1)\n\trequire.Equal(t, user2.ID, inboxes[0].SenderID)\n\n\tts.Close()\n}\n\nfunc TestInboxMessagePayload(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\tuser, err := createTestingHostUser(ctx, ts)\n\trequire.NoError(t, err)\n\n\t// Create inbox with message payload containing memo references.\n\tmemoID := int32(123)\n\trelatedMemoID := int32(456)\n\tinbox, err := ts.CreateInbox(ctx, &store.Inbox{\n\t\tSenderID:   0,\n\t\tReceiverID: user.ID,\n\t\tStatus:     store.UNREAD,\n\t\tMessage: &storepb.InboxMessage{\n\t\t\tType: storepb.InboxMessage_MEMO_COMMENT,\n\t\t\tPayload: &storepb.InboxMessage_MemoComment{\n\t\t\t\tMemoComment: &storepb.InboxMessage_MemoCommentPayload{\n\t\t\t\t\tMemoId:        memoID,\n\t\t\t\t\tRelatedMemoId: relatedMemoID,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t})\n\trequire.NoError(t, err)\n\trequire.NotNil(t, inbox.Message)\n\trequire.Equal(t, storepb.InboxMessage_MEMO_COMMENT, inbox.Message.Type)\n\trequire.NotNil(t, inbox.Message.GetMemoComment())\n\trequire.Equal(t, memoID, inbox.Message.GetMemoComment().MemoId)\n\trequire.Equal(t, relatedMemoID, inbox.Message.GetMemoComment().RelatedMemoId)\n\n\t// List and verify payload is preserved\n\tinboxes, err := ts.ListInboxes(ctx, &store.FindInbox{ReceiverID: &user.ID})\n\trequire.NoError(t, err)\n\trequire.Len(t, inboxes, 1)\n\trequire.NotNil(t, inboxes[0].Message.GetMemoComment())\n\trequire.Equal(t, memoID, inboxes[0].Message.GetMemoComment().MemoId)\n\trequire.Equal(t, relatedMemoID, inboxes[0].Message.GetMemoComment().RelatedMemoId)\n\n\tts.Close()\n}\n\nfunc TestInboxUpdateStatus(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\tuser, err := createTestingHostUser(ctx, ts)\n\trequire.NoError(t, err)\n\n\tinbox, err := ts.CreateInbox(ctx, &store.Inbox{\n\t\tSenderID:   0,\n\t\tReceiverID: user.ID,\n\t\tStatus:     store.UNREAD,\n\t\tMessage:    &storepb.InboxMessage{Type: storepb.InboxMessage_MEMO_COMMENT},\n\t})\n\trequire.NoError(t, err)\n\trequire.Equal(t, store.UNREAD, inbox.Status)\n\n\t// Update to ARCHIVED\n\tupdated, err := ts.UpdateInbox(ctx, &store.UpdateInbox{\n\t\tID:     inbox.ID,\n\t\tStatus: store.ARCHIVED,\n\t})\n\trequire.NoError(t, err)\n\trequire.Equal(t, store.ARCHIVED, updated.Status)\n\trequire.Equal(t, inbox.ID, updated.ID)\n\n\t// Verify the update persisted\n\tinboxes, err := ts.ListInboxes(ctx, &store.FindInbox{ID: &inbox.ID})\n\trequire.NoError(t, err)\n\trequire.Len(t, inboxes, 1)\n\trequire.Equal(t, store.ARCHIVED, inboxes[0].Status)\n\n\tts.Close()\n}\n\nfunc TestInboxListByMessageTypeMultipleTypes(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\tuser, err := createTestingHostUser(ctx, ts)\n\trequire.NoError(t, err)\n\n\t// Create inboxes with different message types\n\t_, err = ts.CreateInbox(ctx, &store.Inbox{\n\t\tSenderID:   0,\n\t\tReceiverID: user.ID,\n\t\tStatus:     store.UNREAD,\n\t\tMessage:    &storepb.InboxMessage{Type: storepb.InboxMessage_MEMO_COMMENT},\n\t})\n\trequire.NoError(t, err)\n\n\t_, err = ts.CreateInbox(ctx, &store.Inbox{\n\t\tSenderID:   0,\n\t\tReceiverID: user.ID,\n\t\tStatus:     store.UNREAD,\n\t\tMessage:    &storepb.InboxMessage{Type: storepb.InboxMessage_TYPE_UNSPECIFIED},\n\t})\n\trequire.NoError(t, err)\n\n\t_, err = ts.CreateInbox(ctx, &store.Inbox{\n\t\tSenderID:   0,\n\t\tReceiverID: user.ID,\n\t\tStatus:     store.UNREAD,\n\t\tMessage:    &storepb.InboxMessage{Type: storepb.InboxMessage_MEMO_COMMENT},\n\t})\n\trequire.NoError(t, err)\n\n\t// Filter by MEMO_COMMENT - should get 2\n\tmemoCommentType := storepb.InboxMessage_MEMO_COMMENT\n\tinboxes, err := ts.ListInboxes(ctx, &store.FindInbox{\n\t\tReceiverID:  &user.ID,\n\t\tMessageType: &memoCommentType,\n\t})\n\trequire.NoError(t, err)\n\trequire.Len(t, inboxes, 2)\n\tfor _, inbox := range inboxes {\n\t\trequire.Equal(t, storepb.InboxMessage_MEMO_COMMENT, inbox.Message.Type)\n\t}\n\n\t// Filter by TYPE_UNSPECIFIED - should get 1\n\tunspecifiedType := storepb.InboxMessage_TYPE_UNSPECIFIED\n\tinboxes, err = ts.ListInboxes(ctx, &store.FindInbox{\n\t\tReceiverID:  &user.ID,\n\t\tMessageType: &unspecifiedType,\n\t})\n\trequire.NoError(t, err)\n\trequire.Len(t, inboxes, 1)\n\trequire.Equal(t, storepb.InboxMessage_TYPE_UNSPECIFIED, inboxes[0].Message.Type)\n\n\tts.Close()\n}\n\nfunc TestInboxMessageTypeFilterWithPayload(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\tuser, err := createTestingHostUser(ctx, ts)\n\trequire.NoError(t, err)\n\n\t// Create inbox with full payload.\n\tmemoID := int32(456)\n\t_, err = ts.CreateInbox(ctx, &store.Inbox{\n\t\tSenderID:   0,\n\t\tReceiverID: user.ID,\n\t\tStatus:     store.UNREAD,\n\t\tMessage: &storepb.InboxMessage{\n\t\t\tType: storepb.InboxMessage_MEMO_COMMENT,\n\t\t\tPayload: &storepb.InboxMessage_MemoComment{\n\t\t\t\tMemoComment: &storepb.InboxMessage_MemoCommentPayload{\n\t\t\t\t\tMemoId:        memoID,\n\t\t\t\t\tRelatedMemoId: 654,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t})\n\trequire.NoError(t, err)\n\n\t// Create inbox with different type but also has payload.\n\totherMemoID := int32(789)\n\t_, err = ts.CreateInbox(ctx, &store.Inbox{\n\t\tSenderID:   0,\n\t\tReceiverID: user.ID,\n\t\tStatus:     store.UNREAD,\n\t\tMessage: &storepb.InboxMessage{\n\t\t\tType: storepb.InboxMessage_TYPE_UNSPECIFIED,\n\t\t\tPayload: &storepb.InboxMessage_MemoComment{\n\t\t\t\tMemoComment: &storepb.InboxMessage_MemoCommentPayload{\n\t\t\t\t\tMemoId:        otherMemoID,\n\t\t\t\t\tRelatedMemoId: 987,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t})\n\trequire.NoError(t, err)\n\n\t// Filter by type should work correctly even with complex JSON payload.\n\tmemoCommentType := storepb.InboxMessage_MEMO_COMMENT\n\tinboxes, err := ts.ListInboxes(ctx, &store.FindInbox{\n\t\tReceiverID:  &user.ID,\n\t\tMessageType: &memoCommentType,\n\t})\n\trequire.NoError(t, err)\n\trequire.Len(t, inboxes, 1)\n\trequire.NotNil(t, inboxes[0].Message.GetMemoComment())\n\trequire.Equal(t, memoID, inboxes[0].Message.GetMemoComment().MemoId)\n\n\tts.Close()\n}\n\nfunc TestInboxMessageTypeFilterWithStatusAndPagination(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\tuser, err := createTestingHostUser(ctx, ts)\n\trequire.NoError(t, err)\n\n\t// Create multiple inboxes with various combinations\n\tfor i := 0; i < 5; i++ {\n\t\t_, err = ts.CreateInbox(ctx, &store.Inbox{\n\t\t\tSenderID:   0,\n\t\t\tReceiverID: user.ID,\n\t\t\tStatus:     store.UNREAD,\n\t\t\tMessage:    &storepb.InboxMessage{Type: storepb.InboxMessage_MEMO_COMMENT},\n\t\t})\n\t\trequire.NoError(t, err)\n\t}\n\n\t// Archive 2 of them\n\tallInboxes, err := ts.ListInboxes(ctx, &store.FindInbox{ReceiverID: &user.ID})\n\trequire.NoError(t, err)\n\tfor i := 0; i < 2; i++ {\n\t\t_, err = ts.UpdateInbox(ctx, &store.UpdateInbox{ID: allInboxes[i].ID, Status: store.ARCHIVED})\n\t\trequire.NoError(t, err)\n\t}\n\n\t// Filter by type + status + pagination\n\tmemoCommentType := storepb.InboxMessage_MEMO_COMMENT\n\tunreadStatus := store.UNREAD\n\tlimit := 2\n\tinboxes, err := ts.ListInboxes(ctx, &store.FindInbox{\n\t\tReceiverID:  &user.ID,\n\t\tMessageType: &memoCommentType,\n\t\tStatus:      &unreadStatus,\n\t\tLimit:       &limit,\n\t})\n\trequire.NoError(t, err)\n\trequire.Len(t, inboxes, 2)\n\tfor _, inbox := range inboxes {\n\t\trequire.Equal(t, storepb.InboxMessage_MEMO_COMMENT, inbox.Message.Type)\n\t\trequire.Equal(t, store.UNREAD, inbox.Status)\n\t}\n\n\t// Get next page\n\toffset := 2\n\tinboxes, err = ts.ListInboxes(ctx, &store.FindInbox{\n\t\tReceiverID:  &user.ID,\n\t\tMessageType: &memoCommentType,\n\t\tStatus:      &unreadStatus,\n\t\tLimit:       &limit,\n\t\tOffset:      &offset,\n\t})\n\trequire.NoError(t, err)\n\trequire.Len(t, inboxes, 1) // Only 1 remaining (3 unread total, got 2, now 1 left)\n\n\tts.Close()\n}\n\nfunc TestInboxMultipleReceivers(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\tuser1, err := createTestingHostUser(ctx, ts)\n\trequire.NoError(t, err)\n\tuser2, err := createTestingUserWithRole(ctx, ts, \"user2\", store.RoleUser)\n\trequire.NoError(t, err)\n\n\t// Create inbox for user1\n\t_, err = ts.CreateInbox(ctx, &store.Inbox{\n\t\tSenderID:   0,\n\t\tReceiverID: user1.ID,\n\t\tStatus:     store.UNREAD,\n\t\tMessage:    &storepb.InboxMessage{Type: storepb.InboxMessage_MEMO_COMMENT},\n\t})\n\trequire.NoError(t, err)\n\n\t// Create inbox for user2\n\t_, err = ts.CreateInbox(ctx, &store.Inbox{\n\t\tSenderID:   0,\n\t\tReceiverID: user2.ID,\n\t\tStatus:     store.UNREAD,\n\t\tMessage:    &storepb.InboxMessage{Type: storepb.InboxMessage_MEMO_COMMENT},\n\t})\n\trequire.NoError(t, err)\n\n\t// User1 should only see their inbox\n\tinboxes, err := ts.ListInboxes(ctx, &store.FindInbox{ReceiverID: &user1.ID})\n\trequire.NoError(t, err)\n\trequire.Len(t, inboxes, 1)\n\trequire.Equal(t, user1.ID, inboxes[0].ReceiverID)\n\n\t// User2 should only see their inbox\n\tinboxes, err = ts.ListInboxes(ctx, &store.FindInbox{ReceiverID: &user2.ID})\n\trequire.NoError(t, err)\n\trequire.Len(t, inboxes, 1)\n\trequire.Equal(t, user2.ID, inboxes[0].ReceiverID)\n\n\tts.Close()\n}\n"
  },
  {
    "path": "store/test/instance_setting_test.go",
    "content": "package test\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n\tcolorpb \"google.golang.org/genproto/googleapis/type/color\"\n\n\tstorepb \"github.com/usememos/memos/proto/gen/store\"\n\t\"github.com/usememos/memos/store\"\n)\n\nfunc TestInstanceSettingV1Store(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\tinstanceSetting, err := ts.UpsertInstanceSetting(ctx, &storepb.InstanceSetting{\n\t\tKey: storepb.InstanceSettingKey_GENERAL,\n\t\tValue: &storepb.InstanceSetting_GeneralSetting{\n\t\t\tGeneralSetting: &storepb.InstanceGeneralSetting{\n\t\t\t\tAdditionalScript: \"\",\n\t\t\t},\n\t\t},\n\t})\n\trequire.NoError(t, err)\n\tsetting, err := ts.GetInstanceSetting(ctx, &store.FindInstanceSetting{\n\t\tName: storepb.InstanceSettingKey_GENERAL.String(),\n\t})\n\trequire.NoError(t, err)\n\trequire.Equal(t, instanceSetting, setting)\n\tts.Close()\n}\n\nfunc TestInstanceSettingGetNonExistent(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\n\t// Get non-existent setting\n\tsetting, err := ts.GetInstanceSetting(ctx, &store.FindInstanceSetting{\n\t\tName: storepb.InstanceSettingKey_STORAGE.String(),\n\t})\n\trequire.NoError(t, err)\n\trequire.Nil(t, setting)\n\n\tts.Close()\n}\n\nfunc TestInstanceSettingUpsertUpdate(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\n\t// Create setting\n\t_, err := ts.UpsertInstanceSetting(ctx, &storepb.InstanceSetting{\n\t\tKey: storepb.InstanceSettingKey_GENERAL,\n\t\tValue: &storepb.InstanceSetting_GeneralSetting{\n\t\t\tGeneralSetting: &storepb.InstanceGeneralSetting{\n\t\t\t\tAdditionalScript: \"console.log('v1')\",\n\t\t\t},\n\t\t},\n\t})\n\trequire.NoError(t, err)\n\n\t// Update setting\n\t_, err = ts.UpsertInstanceSetting(ctx, &storepb.InstanceSetting{\n\t\tKey: storepb.InstanceSettingKey_GENERAL,\n\t\tValue: &storepb.InstanceSetting_GeneralSetting{\n\t\t\tGeneralSetting: &storepb.InstanceGeneralSetting{\n\t\t\t\tAdditionalScript: \"console.log('v2')\",\n\t\t\t},\n\t\t},\n\t})\n\trequire.NoError(t, err)\n\n\t// Verify update\n\tsetting, err := ts.GetInstanceSetting(ctx, &store.FindInstanceSetting{\n\t\tName: storepb.InstanceSettingKey_GENERAL.String(),\n\t})\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"console.log('v2')\", setting.GetGeneralSetting().AdditionalScript)\n\n\t// Verify only one setting exists\n\tlist, err := ts.ListInstanceSettings(ctx, &store.FindInstanceSetting{\n\t\tName: storepb.InstanceSettingKey_GENERAL.String(),\n\t})\n\trequire.NoError(t, err)\n\trequire.Equal(t, 1, len(list))\n\n\tts.Close()\n}\n\nfunc TestInstanceSettingBasicSetting(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\n\t// Get default basic setting (should return empty defaults)\n\tbasicSetting, err := ts.GetInstanceBasicSetting(ctx)\n\trequire.NoError(t, err)\n\trequire.NotNil(t, basicSetting)\n\n\t// Set basic setting\n\t_, err = ts.UpsertInstanceSetting(ctx, &storepb.InstanceSetting{\n\t\tKey: storepb.InstanceSettingKey_BASIC,\n\t\tValue: &storepb.InstanceSetting_BasicSetting{\n\t\t\tBasicSetting: &storepb.InstanceBasicSetting{\n\t\t\t\tSecretKey: \"my-secret-key\",\n\t\t\t},\n\t\t},\n\t})\n\trequire.NoError(t, err)\n\n\t// Verify\n\tbasicSetting, err = ts.GetInstanceBasicSetting(ctx)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"my-secret-key\", basicSetting.SecretKey)\n\n\tts.Close()\n}\n\nfunc TestInstanceSettingGeneralSetting(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\n\t// Get default general setting\n\tgeneralSetting, err := ts.GetInstanceGeneralSetting(ctx)\n\trequire.NoError(t, err)\n\trequire.NotNil(t, generalSetting)\n\n\t// Set general setting\n\t_, err = ts.UpsertInstanceSetting(ctx, &storepb.InstanceSetting{\n\t\tKey: storepb.InstanceSettingKey_GENERAL,\n\t\tValue: &storepb.InstanceSetting_GeneralSetting{\n\t\t\tGeneralSetting: &storepb.InstanceGeneralSetting{\n\t\t\t\tAdditionalScript: \"console.log('test')\",\n\t\t\t\tAdditionalStyle:  \"body { color: red; }\",\n\t\t\t},\n\t\t},\n\t})\n\trequire.NoError(t, err)\n\n\t// Verify\n\tgeneralSetting, err = ts.GetInstanceGeneralSetting(ctx)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"console.log('test')\", generalSetting.AdditionalScript)\n\trequire.Equal(t, \"body { color: red; }\", generalSetting.AdditionalStyle)\n\n\tts.Close()\n}\n\nfunc TestInstanceSettingMemoRelatedSetting(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\n\t// Get default memo related setting (should have defaults)\n\tmemoSetting, err := ts.GetInstanceMemoRelatedSetting(ctx)\n\trequire.NoError(t, err)\n\trequire.NotNil(t, memoSetting)\n\trequire.GreaterOrEqual(t, memoSetting.ContentLengthLimit, int32(store.DefaultContentLengthLimit))\n\trequire.NotEmpty(t, memoSetting.Reactions)\n\n\t// Set custom memo related setting\n\tcustomReactions := []string{\"👍\", \"👎\", \"🚀\"}\n\t_, err = ts.UpsertInstanceSetting(ctx, &storepb.InstanceSetting{\n\t\tKey: storepb.InstanceSettingKey_MEMO_RELATED,\n\t\tValue: &storepb.InstanceSetting_MemoRelatedSetting{\n\t\t\tMemoRelatedSetting: &storepb.InstanceMemoRelatedSetting{\n\t\t\t\tContentLengthLimit: 16384,\n\t\t\t\tReactions:          customReactions,\n\t\t\t},\n\t\t},\n\t})\n\trequire.NoError(t, err)\n\n\t// Verify\n\tmemoSetting, err = ts.GetInstanceMemoRelatedSetting(ctx)\n\trequire.NoError(t, err)\n\trequire.Equal(t, int32(16384), memoSetting.ContentLengthLimit)\n\trequire.Equal(t, customReactions, memoSetting.Reactions)\n\n\tts.Close()\n}\n\nfunc TestInstanceSettingStorageSetting(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\n\t// Get default storage setting (should have defaults)\n\tstorageSetting, err := ts.GetInstanceStorageSetting(ctx)\n\trequire.NoError(t, err)\n\trequire.NotNil(t, storageSetting)\n\trequire.Equal(t, storepb.InstanceStorageSetting_LOCAL, storageSetting.StorageType)\n\trequire.Equal(t, int64(30), storageSetting.UploadSizeLimitMb)\n\trequire.Equal(t, \"assets/{timestamp}_{filename}\", storageSetting.FilepathTemplate)\n\n\t// Set custom storage setting\n\t_, err = ts.UpsertInstanceSetting(ctx, &storepb.InstanceSetting{\n\t\tKey: storepb.InstanceSettingKey_STORAGE,\n\t\tValue: &storepb.InstanceSetting_StorageSetting{\n\t\t\tStorageSetting: &storepb.InstanceStorageSetting{\n\t\t\t\tStorageType:       storepb.InstanceStorageSetting_LOCAL,\n\t\t\t\tUploadSizeLimitMb: 100,\n\t\t\t\tFilepathTemplate:  \"uploads/{date}/{filename}\",\n\t\t\t},\n\t\t},\n\t})\n\trequire.NoError(t, err)\n\n\t// Verify\n\tstorageSetting, err = ts.GetInstanceStorageSetting(ctx)\n\trequire.NoError(t, err)\n\trequire.Equal(t, storepb.InstanceStorageSetting_LOCAL, storageSetting.StorageType)\n\trequire.Equal(t, int64(100), storageSetting.UploadSizeLimitMb)\n\trequire.Equal(t, \"uploads/{date}/{filename}\", storageSetting.FilepathTemplate)\n\n\tts.Close()\n}\n\nfunc TestInstanceSettingTagsSetting(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\n\ttagsSetting, err := ts.GetInstanceTagsSetting(ctx)\n\trequire.NoError(t, err)\n\trequire.NotNil(t, tagsSetting)\n\trequire.Empty(t, tagsSetting.Tags)\n\n\t_, err = ts.UpsertInstanceSetting(ctx, &storepb.InstanceSetting{\n\t\tKey: storepb.InstanceSettingKey_TAGS,\n\t\tValue: &storepb.InstanceSetting_TagsSetting{\n\t\t\tTagsSetting: &storepb.InstanceTagsSetting{\n\t\t\t\tTags: map[string]*storepb.InstanceTagMetadata{\n\t\t\t\t\t\"bug\": {\n\t\t\t\t\t\tBackgroundColor: &colorpb.Color{\n\t\t\t\t\t\t\tRed:   0.9,\n\t\t\t\t\t\t\tGreen: 0.1,\n\t\t\t\t\t\t\tBlue:  0.1,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t})\n\trequire.NoError(t, err)\n\n\ttagsSetting, err = ts.GetInstanceTagsSetting(ctx)\n\trequire.NoError(t, err)\n\trequire.Contains(t, tagsSetting.Tags, \"bug\")\n\trequire.InDelta(t, 0.9, tagsSetting.Tags[\"bug\"].GetBackgroundColor().GetRed(), 0.0001)\n\n\tts.Close()\n}\n\nfunc TestInstanceSettingNotificationSetting(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\n\tnotificationSetting, err := ts.GetInstanceNotificationSetting(ctx)\n\trequire.NoError(t, err)\n\trequire.NotNil(t, notificationSetting)\n\trequire.NotNil(t, notificationSetting.Email)\n\trequire.False(t, notificationSetting.Email.Enabled)\n\n\t_, err = ts.UpsertInstanceSetting(ctx, &storepb.InstanceSetting{\n\t\tKey: storepb.InstanceSettingKey_NOTIFICATION,\n\t\tValue: &storepb.InstanceSetting_NotificationSetting{\n\t\t\tNotificationSetting: &storepb.InstanceNotificationSetting{\n\t\t\t\tEmail: &storepb.InstanceNotificationSetting_EmailSetting{\n\t\t\t\t\tEnabled:      true,\n\t\t\t\t\tSmtpHost:     \"smtp.example.com\",\n\t\t\t\t\tSmtpPort:     587,\n\t\t\t\t\tSmtpUsername: \"bot@example.com\",\n\t\t\t\t\tSmtpPassword: \"secret\",\n\t\t\t\t\tFromEmail:    \"bot@example.com\",\n\t\t\t\t\tFromName:     \"Memos Bot\",\n\t\t\t\t\tReplyTo:      \"support@example.com\",\n\t\t\t\t\tUseTls:       true,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t})\n\trequire.NoError(t, err)\n\n\tnotificationSetting, err = ts.GetInstanceNotificationSetting(ctx)\n\trequire.NoError(t, err)\n\trequire.True(t, notificationSetting.Email.Enabled)\n\trequire.Equal(t, \"smtp.example.com\", notificationSetting.Email.SmtpHost)\n\trequire.Equal(t, int32(587), notificationSetting.Email.SmtpPort)\n\trequire.Equal(t, \"bot@example.com\", notificationSetting.Email.FromEmail)\n\n\tts.Close()\n}\n\nfunc TestInstanceSettingListAll(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\n\t// Count initial settings\n\tinitialList, err := ts.ListInstanceSettings(ctx, &store.FindInstanceSetting{})\n\trequire.NoError(t, err)\n\tinitialCount := len(initialList)\n\n\t// Create multiple settings\n\t_, err = ts.UpsertInstanceSetting(ctx, &storepb.InstanceSetting{\n\t\tKey: storepb.InstanceSettingKey_GENERAL,\n\t\tValue: &storepb.InstanceSetting_GeneralSetting{\n\t\t\tGeneralSetting: &storepb.InstanceGeneralSetting{},\n\t\t},\n\t})\n\trequire.NoError(t, err)\n\n\t_, err = ts.UpsertInstanceSetting(ctx, &storepb.InstanceSetting{\n\t\tKey: storepb.InstanceSettingKey_STORAGE,\n\t\tValue: &storepb.InstanceSetting_StorageSetting{\n\t\t\tStorageSetting: &storepb.InstanceStorageSetting{},\n\t\t},\n\t})\n\trequire.NoError(t, err)\n\n\t_, err = ts.UpsertInstanceSetting(ctx, &storepb.InstanceSetting{\n\t\tKey: storepb.InstanceSettingKey_NOTIFICATION,\n\t\tValue: &storepb.InstanceSetting_NotificationSetting{\n\t\t\tNotificationSetting: &storepb.InstanceNotificationSetting{},\n\t\t},\n\t})\n\trequire.NoError(t, err)\n\n\t// List all - should have 3 more than initial\n\tlist, err := ts.ListInstanceSettings(ctx, &store.FindInstanceSetting{})\n\trequire.NoError(t, err)\n\trequire.Equal(t, initialCount+3, len(list))\n\n\tts.Close()\n}\n\nfunc TestInstanceSettingEdgeCases(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\n\t// Case 1: General Setting with special characters and Unicode\n\tspecialScript := `<script>alert(\"你好\"); var x = 'test\\'s';</script>`\n\tspecialStyle := `body { font-family: \"Noto Sans SC\", sans-serif; content: \"\\u2764\"; }`\n\t_, err := ts.UpsertInstanceSetting(ctx, &storepb.InstanceSetting{\n\t\tKey: storepb.InstanceSettingKey_GENERAL,\n\t\tValue: &storepb.InstanceSetting_GeneralSetting{\n\t\t\tGeneralSetting: &storepb.InstanceGeneralSetting{\n\t\t\t\tAdditionalScript: specialScript,\n\t\t\t\tAdditionalStyle:  specialStyle,\n\t\t\t},\n\t\t},\n\t})\n\trequire.NoError(t, err)\n\n\tgeneralSetting, err := ts.GetInstanceGeneralSetting(ctx)\n\trequire.NoError(t, err)\n\trequire.Equal(t, specialScript, generalSetting.AdditionalScript)\n\trequire.Equal(t, specialStyle, generalSetting.AdditionalStyle)\n\n\t// Case 2: Memo Related Setting with Unicode reactions\n\tunicodeReactions := []string{\"🐱\", \"🐶\", \"🦊\", \"🦄\"}\n\t_, err = ts.UpsertInstanceSetting(ctx, &storepb.InstanceSetting{\n\t\tKey: storepb.InstanceSettingKey_MEMO_RELATED,\n\t\tValue: &storepb.InstanceSetting_MemoRelatedSetting{\n\t\t\tMemoRelatedSetting: &storepb.InstanceMemoRelatedSetting{\n\t\t\t\tContentLengthLimit: 1000,\n\t\t\t\tReactions:          unicodeReactions,\n\t\t\t},\n\t\t},\n\t})\n\trequire.NoError(t, err)\n\n\tmemoSetting, err := ts.GetInstanceMemoRelatedSetting(ctx)\n\trequire.NoError(t, err)\n\trequire.Equal(t, unicodeReactions, memoSetting.Reactions)\n\n\tts.Close()\n}\n"
  },
  {
    "path": "store/test/main_test.go",
    "content": "package test\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"testing\"\n)\n\nfunc TestMain(m *testing.M) {\n\t// If DRIVER is set, run tests for that driver only\n\tif os.Getenv(\"DRIVER\") != \"\" {\n\t\tdefer TerminateContainers()\n\t\tm.Run() //nolint:revive // Exit code is handled by test runner\n\t\treturn\n\t}\n\n\t// No DRIVER set - run tests for all drivers sequentially\n\trunAllDrivers()\n}\n\nfunc runAllDrivers() {\n\tdrivers := []string{\"sqlite\", \"mysql\", \"postgres\"}\n\t_, currentFile, _, _ := runtime.Caller(0)\n\tprojectRoot := filepath.Dir(filepath.Dir(filepath.Dir(currentFile)))\n\n\tvar failed []string\n\tfor _, driver := range drivers {\n\t\tfmt.Printf(\"\\n==================== %s ====================\\n\\n\", driver)\n\n\t\tcmd := exec.Command(\"go\", \"test\", \"-v\", \"-count=1\", \"./store/test/...\")\n\t\tcmd.Dir = projectRoot\n\t\tcmd.Env = append(os.Environ(), \"DRIVER=\"+driver)\n\t\tcmd.Stdout = os.Stdout\n\t\tcmd.Stderr = os.Stderr\n\n\t\tif err := cmd.Run(); err != nil {\n\t\t\tfailed = append(failed, driver)\n\t\t}\n\t}\n\n\tfmt.Println()\n\tif len(failed) > 0 {\n\t\tfmt.Printf(\"FAIL: %v\\n\", failed)\n\t\tpanic(\"some drivers failed\")\n\t}\n\tfmt.Println(\"PASS: all drivers\")\n}\n"
  },
  {
    "path": "store/test/memo_filter_test.go",
    "content": "package test\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/require\"\n\n\tstorepb \"github.com/usememos/memos/proto/gen/store\"\n\t\"github.com/usememos/memos/store\"\n)\n\n// =============================================================================\n// Content Field Tests\n// Schema: content (string, supports contains)\n// =============================================================================\n\nfunc TestMemoFilterContentContains(t *testing.T) {\n\tt.Parallel()\n\ttc := NewMemoFilterTestContext(t)\n\tdefer tc.Close()\n\n\t// Create memos with different content\n\ttc.CreateMemo(NewMemoBuilder(\"memo-hello\", tc.User.ID).Content(\"Hello world\"))\n\ttc.CreateMemo(NewMemoBuilder(\"memo-goodbye\", tc.User.ID).Content(\"Goodbye world\"))\n\ttc.CreateMemo(NewMemoBuilder(\"memo-test\", tc.User.ID).Content(\"Testing content\"))\n\n\t// Test: content.contains(\"Hello\") - single match\n\tmemos := tc.ListWithFilter(`content.contains(\"Hello\")`)\n\trequire.Len(t, memos, 1)\n\trequire.Contains(t, memos[0].Content, \"Hello\")\n\n\t// Test: content.contains(\"world\") - multiple matches\n\tmemos = tc.ListWithFilter(`content.contains(\"world\")`)\n\trequire.Len(t, memos, 2)\n\n\t// Test: content.contains(\"nonexistent\") - no matches\n\tmemos = tc.ListWithFilter(`content.contains(\"nonexistent\")`)\n\trequire.Len(t, memos, 0)\n}\n\nfunc TestMemoFilterContentSpecialCharacters(t *testing.T) {\n\tt.Parallel()\n\ttc := NewMemoFilterTestContext(t)\n\tdefer tc.Close()\n\n\ttc.CreateMemo(NewMemoBuilder(\"memo-special\", tc.User.ID).Content(\"Special chars: @#$%^&*()\"))\n\n\tmemos := tc.ListWithFilter(`content.contains(\"@#$%\")`)\n\trequire.Len(t, memos, 1)\n}\n\nfunc TestMemoFilterContentUnicode(t *testing.T) {\n\tt.Parallel()\n\ttc := NewMemoFilterTestContext(t)\n\tdefer tc.Close()\n\n\ttc.CreateMemo(NewMemoBuilder(\"memo-unicode\", tc.User.ID).Content(\"Unicode test: 你好世界 🌍\"))\n\n\tmemos := tc.ListWithFilter(`content.contains(\"你好\")`)\n\trequire.Len(t, memos, 1)\n}\n\nfunc TestMemoFilterContentUnicodeCaseFold(t *testing.T) {\n\tt.Parallel()\n\ttc := NewMemoFilterTestContext(t)\n\tdefer tc.Close()\n\n\ttc.CreateMemo(NewMemoBuilder(\"memo-unicode-case\", tc.User.ID).Content(\"Привет Мир\"))\n\n\tmemos := tc.ListWithFilter(`content.contains(\"привет\")`)\n\trequire.Len(t, memos, 1)\n}\n\nfunc TestMemoFilterContentCaseSensitivity(t *testing.T) {\n\tt.Parallel()\n\ttc := NewMemoFilterTestContext(t)\n\tdefer tc.Close()\n\n\ttc.CreateMemo(NewMemoBuilder(\"memo-case\", tc.User.ID).Content(\"MixedCase Content\"))\n\n\t// Exact match\n\tmemos := tc.ListWithFilter(`content.contains(\"MixedCase\")`)\n\trequire.Len(t, memos, 1)\n\n\t// Lowercase match (depends on DB collation, usually case-insensitive in default installs but good to verify behavior)\n\t// SQLite default LIKE is case-insensitive for ASCII.\n\tmemosLower := tc.ListWithFilter(`content.contains(\"mixedcase\")`)\n\t// We just verify it doesn't crash; strict case sensitivity expectation depends on DB config.\n\t// For standard Memos setup (SQLite), it's often case-insensitive.\n\t// Let's check if we get a result or not to characterize current behavior.\n\tif len(memosLower) > 0 {\n\t\trequire.Equal(t, \"MixedCase Content\", memosLower[0].Content)\n\t}\n}\n\n// =============================================================================\n// Visibility Field Tests\n// Schema: visibility (string, ==, !=)\n// =============================================================================\n\nfunc TestMemoFilterVisibilityEquals(t *testing.T) {\n\tt.Parallel()\n\ttc := NewMemoFilterTestContext(t)\n\tdefer tc.Close()\n\n\ttc.CreateMemo(NewMemoBuilder(\"memo-public\", tc.User.ID).Content(\"Public memo\").Visibility(store.Public))\n\ttc.CreateMemo(NewMemoBuilder(\"memo-private\", tc.User.ID).Content(\"Private memo\").Visibility(store.Private))\n\ttc.CreateMemo(NewMemoBuilder(\"memo-protected\", tc.User.ID).Content(\"Protected memo\").Visibility(store.Protected))\n\n\t// Test: visibility == \"PUBLIC\"\n\tmemos := tc.ListWithFilter(`visibility == \"PUBLIC\"`)\n\trequire.Len(t, memos, 1)\n\trequire.Equal(t, store.Public, memos[0].Visibility)\n\n\t// Test: visibility == \"PRIVATE\"\n\tmemos = tc.ListWithFilter(`visibility == \"PRIVATE\"`)\n\trequire.Len(t, memos, 1)\n\trequire.Equal(t, store.Private, memos[0].Visibility)\n}\n\nfunc TestMemoFilterVisibilityNotEquals(t *testing.T) {\n\tt.Parallel()\n\ttc := NewMemoFilterTestContext(t)\n\tdefer tc.Close()\n\n\ttc.CreateMemo(NewMemoBuilder(\"memo-public\", tc.User.ID).Content(\"Public memo\").Visibility(store.Public))\n\ttc.CreateMemo(NewMemoBuilder(\"memo-private\", tc.User.ID).Content(\"Private memo\").Visibility(store.Private))\n\n\tmemos := tc.ListWithFilter(`visibility != \"PUBLIC\"`)\n\trequire.Len(t, memos, 1)\n\trequire.Equal(t, store.Private, memos[0].Visibility)\n}\n\nfunc TestMemoFilterVisibilityInList(t *testing.T) {\n\tt.Parallel()\n\ttc := NewMemoFilterTestContext(t)\n\tdefer tc.Close()\n\n\ttc.CreateMemo(NewMemoBuilder(\"memo-pub\", tc.User.ID).Visibility(store.Public))\n\ttc.CreateMemo(NewMemoBuilder(\"memo-priv\", tc.User.ID).Visibility(store.Private))\n\ttc.CreateMemo(NewMemoBuilder(\"memo-prot\", tc.User.ID).Visibility(store.Protected))\n\n\tmemos := tc.ListWithFilter(`visibility in [\"PUBLIC\", \"PRIVATE\"]`)\n\trequire.Len(t, memos, 2)\n}\n\n// =============================================================================\n// Pinned Field Tests\n// Schema: pinned (bool column, ==, !=, predicate)\n// =============================================================================\n\nfunc TestMemoFilterPinnedEquals(t *testing.T) {\n\tt.Parallel()\n\ttc := NewMemoFilterTestContext(t)\n\tdefer tc.Close()\n\n\tpinnedMemo := tc.CreateMemo(NewMemoBuilder(\"memo-pinned\", tc.User.ID).Content(\"Pinned memo\"))\n\ttc.PinMemo(pinnedMemo.ID)\n\ttc.CreateMemo(NewMemoBuilder(\"memo-unpinned\", tc.User.ID).Content(\"Unpinned memo\"))\n\n\t// Test: pinned == true\n\tmemos := tc.ListWithFilter(`pinned == true`)\n\trequire.Len(t, memos, 1)\n\trequire.True(t, memos[0].Pinned)\n\n\t// Test: pinned == false\n\tmemos = tc.ListWithFilter(`pinned == false`)\n\trequire.Len(t, memos, 1)\n\trequire.False(t, memos[0].Pinned)\n}\n\nfunc TestMemoFilterPinnedPredicate(t *testing.T) {\n\tt.Parallel()\n\ttc := NewMemoFilterTestContext(t)\n\tdefer tc.Close()\n\n\tpinnedMemo := tc.CreateMemo(NewMemoBuilder(\"memo-pinned\", tc.User.ID).Content(\"Pinned memo\"))\n\ttc.PinMemo(pinnedMemo.ID)\n\ttc.CreateMemo(NewMemoBuilder(\"memo-unpinned\", tc.User.ID).Content(\"Unpinned memo\"))\n\n\tmemos := tc.ListWithFilter(`pinned`)\n\trequire.Len(t, memos, 1)\n\trequire.True(t, memos[0].Pinned)\n}\n\n// =============================================================================\n// Creator ID Field Tests\n// Schema: creator_id (int, ==, !=)\n// =============================================================================\n\nfunc TestMemoFilterCreatorIdEquals(t *testing.T) {\n\tt.Parallel()\n\ttc := NewMemoFilterTestContext(t)\n\tdefer tc.Close()\n\n\tuser2, err := tc.Store.CreateUser(tc.Ctx, &store.User{\n\t\tUsername: \"user2\",\n\t\tRole:     store.RoleUser,\n\t\tEmail:    \"user2@example.com\",\n\t\tNickname: \"User 2\",\n\t})\n\trequire.NoError(t, err)\n\n\ttc.CreateMemo(NewMemoBuilder(\"memo-user1\", tc.User.ID).Content(\"User 1 memo\"))\n\ttc.CreateMemo(NewMemoBuilder(\"memo-user2\", user2.ID).Content(\"User 2 memo\"))\n\n\tmemos := tc.ListWithFilter(`creator_id == ` + formatInt(int(tc.User.ID)))\n\trequire.Len(t, memos, 1)\n\trequire.Equal(t, tc.User.ID, memos[0].CreatorID)\n}\n\nfunc TestMemoFilterCreatorIdNotEquals(t *testing.T) {\n\tt.Parallel()\n\ttc := NewMemoFilterTestContext(t)\n\tdefer tc.Close()\n\n\tuser2, err := tc.Store.CreateUser(tc.Ctx, &store.User{\n\t\tUsername: \"user2\",\n\t\tRole:     store.RoleUser,\n\t\tEmail:    \"user2@example.com\",\n\t\tNickname: \"User 2\",\n\t})\n\trequire.NoError(t, err)\n\n\ttc.CreateMemo(NewMemoBuilder(\"memo-user1\", tc.User.ID).Content(\"User 1 memo\"))\n\ttc.CreateMemo(NewMemoBuilder(\"memo-user2\", user2.ID).Content(\"User 2 memo\"))\n\n\tmemos := tc.ListWithFilter(`creator_id != ` + formatInt(int(tc.User.ID)))\n\trequire.Len(t, memos, 1)\n\trequire.Equal(t, user2.ID, memos[0].CreatorID)\n}\n\n// =============================================================================\n// Tags Field Tests\n// Schema: tags (JSON list), tag (virtual alias)\n// Operators: tag in [...], \"value\" in tags\n// =============================================================================\n\nfunc TestMemoFilterTagInList(t *testing.T) {\n\tt.Parallel()\n\ttc := NewMemoFilterTestContext(t)\n\tdefer tc.Close()\n\n\ttc.CreateMemo(NewMemoBuilder(\"memo-work\", tc.User.ID).Content(\"Work memo\").Tags(\"work\", \"important\"))\n\ttc.CreateMemo(NewMemoBuilder(\"memo-personal\", tc.User.ID).Content(\"Personal memo\").Tags(\"personal\", \"fun\"))\n\ttc.CreateMemo(NewMemoBuilder(\"memo-no-tags\", tc.User.ID).Content(\"No tags\"))\n\n\t// Test: tag in [\"work\"]\n\tmemos := tc.ListWithFilter(`tag in [\"work\"]`)\n\trequire.Len(t, memos, 1)\n\trequire.Contains(t, memos[0].Payload.Tags, \"work\")\n\n\t// Test: tag in [\"work\", \"personal\"]\n\tmemos = tc.ListWithFilter(`tag in [\"work\", \"personal\"]`)\n\trequire.Len(t, memos, 2)\n}\n\nfunc TestMemoFilterElementInTags(t *testing.T) {\n\tt.Parallel()\n\ttc := NewMemoFilterTestContext(t)\n\tdefer tc.Close()\n\n\ttc.CreateMemo(NewMemoBuilder(\"memo-tagged\", tc.User.ID).Content(\"Tagged memo\").Tags(\"project\", \"todo\"))\n\ttc.CreateMemo(NewMemoBuilder(\"memo-untagged\", tc.User.ID).Content(\"Untagged memo\"))\n\n\t// Test: \"project\" in tags\n\tmemos := tc.ListWithFilter(`\"project\" in tags`)\n\trequire.Len(t, memos, 1)\n\n\t// Test: \"nonexistent\" in tags\n\tmemos = tc.ListWithFilter(`\"nonexistent\" in tags`)\n\trequire.Len(t, memos, 0)\n}\n\nfunc TestMemoFilterHierarchicalTags(t *testing.T) {\n\tt.Parallel()\n\ttc := NewMemoFilterTestContext(t)\n\tdefer tc.Close()\n\n\ttc.CreateMemo(NewMemoBuilder(\"memo-book\", tc.User.ID).Content(\"Book memo\").Tags(\"book\"))\n\ttc.CreateMemo(NewMemoBuilder(\"memo-book-fiction\", tc.User.ID).Content(\"Fiction book memo\").Tags(\"book/fiction\"))\n\n\t// Test: tag in [\"book\"] should match both (hierarchical matching)\n\tmemos := tc.ListWithFilter(`tag in [\"book\"]`)\n\trequire.Len(t, memos, 2)\n}\n\nfunc TestMemoFilterEmptyTags(t *testing.T) {\n\tt.Parallel()\n\ttc := NewMemoFilterTestContext(t)\n\tdefer tc.Close()\n\n\ttc.CreateMemo(NewMemoBuilder(\"memo-empty-tags\", tc.User.ID).Content(\"Empty tags\").Tags())\n\n\tmemos := tc.ListWithFilter(`tag in [\"anything\"]`)\n\trequire.Len(t, memos, 0)\n}\n\n// =============================================================================\n// JSON Bool Field Tests\n// Schema: has_task_list, has_link, has_code, has_incomplete_tasks\n// Operators: ==, !=, predicate\n// =============================================================================\n\nfunc TestMemoFilterHasTaskList(t *testing.T) {\n\tt.Parallel()\n\ttc := NewMemoFilterTestContext(t)\n\tdefer tc.Close()\n\n\ttc.CreateMemo(NewMemoBuilder(\"memo-with-tasks\", tc.User.ID).\n\t\tContent(\"- [ ] Task 1\\n- [x] Task 2\").\n\t\tProperty(func(p *storepb.MemoPayload_Property) { p.HasTaskList = true }))\n\ttc.CreateMemo(NewMemoBuilder(\"memo-no-tasks\", tc.User.ID).Content(\"No tasks here\"))\n\n\t// Test: has_task_list (predicate)\n\tmemos := tc.ListWithFilter(`has_task_list`)\n\trequire.Len(t, memos, 1)\n\trequire.True(t, memos[0].Payload.Property.HasTaskList)\n\n\t// Test: has_task_list == true\n\tmemos = tc.ListWithFilter(`has_task_list == true`)\n\trequire.Len(t, memos, 1)\n\n\t// Note: has_task_list == false is not tested because JSON boolean fields\n\t// with false value may not be queryable when the field is not present in JSON\n}\n\nfunc TestMemoFilterHasLink(t *testing.T) {\n\tt.Parallel()\n\ttc := NewMemoFilterTestContext(t)\n\tdefer tc.Close()\n\n\ttc.CreateMemo(NewMemoBuilder(\"memo-with-link\", tc.User.ID).\n\t\tContent(\"Check out https://example.com\").\n\t\tProperty(func(p *storepb.MemoPayload_Property) { p.HasLink = true }))\n\ttc.CreateMemo(NewMemoBuilder(\"memo-no-link\", tc.User.ID).Content(\"No links\"))\n\n\tmemos := tc.ListWithFilter(`has_link`)\n\trequire.Len(t, memos, 1)\n\trequire.True(t, memos[0].Payload.Property.HasLink)\n}\n\nfunc TestMemoFilterHasCode(t *testing.T) {\n\tt.Parallel()\n\ttc := NewMemoFilterTestContext(t)\n\tdefer tc.Close()\n\n\ttc.CreateMemo(NewMemoBuilder(\"memo-with-code\", tc.User.ID).\n\t\tContent(\"```go\\nfmt.Println(\\\"Hello\\\")\\n```\").\n\t\tProperty(func(p *storepb.MemoPayload_Property) { p.HasCode = true }))\n\ttc.CreateMemo(NewMemoBuilder(\"memo-no-code\", tc.User.ID).Content(\"No code\"))\n\n\tmemos := tc.ListWithFilter(`has_code`)\n\trequire.Len(t, memos, 1)\n\trequire.True(t, memos[0].Payload.Property.HasCode)\n}\n\nfunc TestMemoFilterHasIncompleteTasks(t *testing.T) {\n\tt.Parallel()\n\ttc := NewMemoFilterTestContext(t)\n\tdefer tc.Close()\n\n\ttc.CreateMemo(NewMemoBuilder(\"memo-incomplete\", tc.User.ID).\n\t\tContent(\"- [ ] Incomplete task\").\n\t\tProperty(func(p *storepb.MemoPayload_Property) {\n\t\t\tp.HasTaskList = true\n\t\t\tp.HasIncompleteTasks = true\n\t\t}))\n\ttc.CreateMemo(NewMemoBuilder(\"memo-complete\", tc.User.ID).\n\t\tContent(\"- [x] Complete task\").\n\t\tProperty(func(p *storepb.MemoPayload_Property) {\n\t\t\tp.HasTaskList = true\n\t\t\tp.HasIncompleteTasks = false\n\t\t}))\n\n\tmemos := tc.ListWithFilter(`has_incomplete_tasks`)\n\trequire.Len(t, memos, 1)\n\trequire.True(t, memos[0].Payload.Property.HasIncompleteTasks)\n}\n\nfunc TestMemoFilterCombinedJSONBool(t *testing.T) {\n\tt.Parallel()\n\ttc := NewMemoFilterTestContext(t)\n\tdefer tc.Close()\n\n\t// Memo with all properties\n\ttc.CreateMemo(NewMemoBuilder(\"memo-all-props\", tc.User.ID).\n\t\tContent(\"All properties\").\n\t\tProperty(func(p *storepb.MemoPayload_Property) {\n\t\t\tp.HasLink = true\n\t\t\tp.HasTaskList = true\n\t\t\tp.HasCode = true\n\t\t\tp.HasIncompleteTasks = true\n\t\t}))\n\n\t// Memo with only link\n\ttc.CreateMemo(NewMemoBuilder(\"memo-only-link\", tc.User.ID).\n\t\tContent(\"Only link\").\n\t\tProperty(func(p *storepb.MemoPayload_Property) { p.HasLink = true }))\n\n\t// Test: has_link && has_code\n\tmemos := tc.ListWithFilter(`has_link && has_code`)\n\trequire.Len(t, memos, 1)\n\n\t// Test: has_task_list && has_incomplete_tasks\n\tmemos = tc.ListWithFilter(`has_task_list && has_incomplete_tasks`)\n\trequire.Len(t, memos, 1)\n\n\t// Test: has_link || has_code\n\tmemos = tc.ListWithFilter(`has_link || has_code`)\n\trequire.Len(t, memos, 2)\n}\n\n// =============================================================================\n// Timestamp Field Tests\n// Schema: created_ts, updated_ts (timestamp, all comparison operators)\n// Functions: now(), arithmetic (+, -, *)\n// =============================================================================\n\nfunc TestMemoFilterCreatedTsComparison(t *testing.T) {\n\tt.Parallel()\n\ttc := NewMemoFilterTestContext(t)\n\tdefer tc.Close()\n\n\tnow := time.Now().Unix()\n\ttc.CreateMemo(NewMemoBuilder(\"memo-ts\", tc.User.ID).Content(\"Timestamp test\"))\n\n\t// Test: created_ts < future (should match)\n\tmemos := tc.ListWithFilter(`created_ts < ` + formatInt64(now+3600))\n\trequire.Len(t, memos, 1)\n\n\t// Test: created_ts > past (should match)\n\tmemos = tc.ListWithFilter(`created_ts > ` + formatInt64(now-3600))\n\trequire.Len(t, memos, 1)\n\n\t// Test: created_ts > future (should not match)\n\tmemos = tc.ListWithFilter(`created_ts > ` + formatInt64(now+3600))\n\trequire.Len(t, memos, 0)\n}\n\nfunc TestMemoFilterCreatedTsWithNow(t *testing.T) {\n\tt.Parallel()\n\ttc := NewMemoFilterTestContext(t)\n\tdefer tc.Close()\n\n\ttc.CreateMemo(NewMemoBuilder(\"memo-ts-test\", tc.User.ID).Content(\"Timestamp test\"))\n\n\t// Test: created_ts < now() + 5 (buffer for container clock drift)\n\tmemos := tc.ListWithFilter(`created_ts < now() + 5`)\n\trequire.Len(t, memos, 1)\n\n\t// Test: created_ts > now() + 5 (should not match)\n\tmemos = tc.ListWithFilter(`created_ts > now() + 5`)\n\trequire.Len(t, memos, 0)\n}\n\nfunc TestMemoFilterCreatedTsArithmetic(t *testing.T) {\n\tt.Parallel()\n\ttc := NewMemoFilterTestContext(t)\n\tdefer tc.Close()\n\n\ttc.CreateMemo(NewMemoBuilder(\"memo-ts-arith\", tc.User.ID).Content(\"Timestamp arithmetic test\"))\n\n\t// Test: created_ts >= now() - 3600 (memos created in last hour)\n\tmemos := tc.ListWithFilter(`created_ts >= now() - 3600`)\n\trequire.Len(t, memos, 1)\n\n\t// Test: created_ts < now() - 86400 (memos older than 1 day - should be empty)\n\tmemos = tc.ListWithFilter(`created_ts < now() - 86400`)\n\trequire.Len(t, memos, 0)\n\n\t// Test: Multiplication - created_ts >= now() - 60 * 60\n\tmemos = tc.ListWithFilter(`created_ts >= now() - 60 * 60`)\n\trequire.Len(t, memos, 1)\n}\n\nfunc TestMemoFilterUpdatedTs(t *testing.T) {\n\tt.Parallel()\n\ttc := NewMemoFilterTestContext(t)\n\tdefer tc.Close()\n\n\tmemo := tc.CreateMemo(NewMemoBuilder(\"memo-updated\", tc.User.ID).Content(\"Will be updated\"))\n\n\t// Update the memo\n\tnewContent := \"Updated content\"\n\terr := tc.Store.UpdateMemo(tc.Ctx, &store.UpdateMemo{\n\t\tID:      memo.ID,\n\t\tContent: &newContent,\n\t})\n\trequire.NoError(t, err)\n\n\t// Test: updated_ts >= now() - 60 (updated in last minute)\n\tmemos := tc.ListWithFilter(`updated_ts >= now() - 60`)\n\trequire.Len(t, memos, 1)\n\n\t// Test: updated_ts > now() + 3600 (should be empty)\n\tmemos = tc.ListWithFilter(`updated_ts > now() + 3600`)\n\trequire.Len(t, memos, 0)\n}\n\nfunc TestMemoFilterAllComparisonOperators(t *testing.T) {\n\tt.Parallel()\n\ttc := NewMemoFilterTestContext(t)\n\tdefer tc.Close()\n\n\ttc.CreateMemo(NewMemoBuilder(\"memo-ops\", tc.User.ID).Content(\"Comparison operators test\"))\n\n\t// Test: < (less than)\n\tmemos := tc.ListWithFilter(`created_ts < now() + 3600`)\n\trequire.Len(t, memos, 1)\n\n\t// Test: <= (less than or equal) with buffer for clock drift\n\tmemos = tc.ListWithFilter(`created_ts < now() + 5`)\n\trequire.Len(t, memos, 1)\n\n\t// Test: > (greater than)\n\tmemos = tc.ListWithFilter(`created_ts > now() - 3600`)\n\trequire.Len(t, memos, 1)\n\n\t// Test: >= (greater than or equal)\n\tmemos = tc.ListWithFilter(`created_ts >= now() - 60`)\n\trequire.Len(t, memos, 1)\n}\n\n// =============================================================================\n// Logical Operator Tests\n// Operators: && (AND), || (OR), ! (NOT)\n// =============================================================================\n\nfunc TestMemoFilterLogicalAnd(t *testing.T) {\n\tt.Parallel()\n\ttc := NewMemoFilterTestContext(t)\n\tdefer tc.Close()\n\n\tpinnedMemo := tc.CreateMemo(NewMemoBuilder(\"memo-pinned-public\", tc.User.ID).Content(\"Pinned public\"))\n\ttc.PinMemo(pinnedMemo.ID)\n\ttc.CreateMemo(NewMemoBuilder(\"memo-unpinned-public\", tc.User.ID).Content(\"Unpinned public\"))\n\n\tmemos := tc.ListWithFilter(`pinned && visibility == \"PUBLIC\"`)\n\trequire.Len(t, memos, 1)\n\trequire.True(t, memos[0].Pinned)\n}\n\nfunc TestMemoFilterLogicalOr(t *testing.T) {\n\tt.Parallel()\n\ttc := NewMemoFilterTestContext(t)\n\tdefer tc.Close()\n\n\ttc.CreateMemo(NewMemoBuilder(\"memo-public\", tc.User.ID).Visibility(store.Public))\n\ttc.CreateMemo(NewMemoBuilder(\"memo-private\", tc.User.ID).Visibility(store.Private))\n\ttc.CreateMemo(NewMemoBuilder(\"memo-protected\", tc.User.ID).Visibility(store.Protected))\n\n\tmemos := tc.ListWithFilter(`visibility == \"PUBLIC\" || visibility == \"PRIVATE\"`)\n\trequire.Len(t, memos, 2)\n}\n\nfunc TestMemoFilterLogicalNot(t *testing.T) {\n\tt.Parallel()\n\ttc := NewMemoFilterTestContext(t)\n\tdefer tc.Close()\n\n\tpinnedMemo := tc.CreateMemo(NewMemoBuilder(\"memo-pinned\", tc.User.ID).Content(\"Pinned\"))\n\ttc.PinMemo(pinnedMemo.ID)\n\ttc.CreateMemo(NewMemoBuilder(\"memo-unpinned\", tc.User.ID).Content(\"Unpinned\"))\n\n\tmemos := tc.ListWithFilter(`!pinned`)\n\trequire.Len(t, memos, 1)\n\trequire.False(t, memos[0].Pinned)\n}\n\nfunc TestMemoFilterNegatedComparison(t *testing.T) {\n\tt.Parallel()\n\ttc := NewMemoFilterTestContext(t)\n\tdefer tc.Close()\n\n\ttc.CreateMemo(NewMemoBuilder(\"memo-public\", tc.User.ID).Visibility(store.Public))\n\ttc.CreateMemo(NewMemoBuilder(\"memo-private\", tc.User.ID).Visibility(store.Private))\n\n\tmemos := tc.ListWithFilter(`!(visibility == \"PUBLIC\")`)\n\trequire.Len(t, memos, 1)\n\trequire.Equal(t, store.Private, memos[0].Visibility)\n}\n\nfunc TestMemoFilterComplexLogical(t *testing.T) {\n\tt.Parallel()\n\ttc := NewMemoFilterTestContext(t)\n\tdefer tc.Close()\n\n\t// Create pinned public memo with tags\n\tpinnedMemo := tc.CreateMemo(NewMemoBuilder(\"memo-pinned-tagged\", tc.User.ID).\n\t\tContent(\"Pinned and tagged\").Tags(\"important\"))\n\ttc.PinMemo(pinnedMemo.ID)\n\n\t// Create unpinned memo with same tag\n\ttc.CreateMemo(NewMemoBuilder(\"memo-unpinned-tagged\", tc.User.ID).\n\t\tContent(\"Unpinned but tagged\").Tags(\"important\"))\n\n\t// Create pinned memo without tag\n\tpinned2 := tc.CreateMemo(NewMemoBuilder(\"memo-pinned-untagged\", tc.User.ID).Content(\"Pinned but untagged\"))\n\ttc.PinMemo(pinned2.ID)\n\n\t// Test: pinned && tag in [\"important\"]\n\tmemos := tc.ListWithFilter(`pinned && tag in [\"important\"]`)\n\trequire.Len(t, memos, 1)\n\n\t// Test: (pinned || tag in [\"important\"]) && visibility == \"PUBLIC\"\n\tmemos = tc.ListWithFilter(`(pinned || tag in [\"important\"]) && visibility == \"PUBLIC\"`)\n\trequire.Len(t, memos, 3)\n\n\t// Test: De Morgan's Law ! (A || B) == !A && !B\n\t// ! (pinned || has_task_list)\n\ttc.CreateMemo(NewMemoBuilder(\"memo-no-props\", tc.User.ID).Content(\"Nothing special\"))\n\tmemos = tc.ListWithFilter(`!(pinned || has_task_list)`)\n\trequire.Len(t, memos, 2) // Unpinned-tagged + Nothing special (pinned-untagged is pinned)\n}\n\n// =============================================================================\n// Tag Comprehension Tests (exists macro)\n// Schema: tags (list of strings, supports exists/all macros with predicates)\n// =============================================================================\n\nfunc TestMemoFilterTagsExistsStartsWith(t *testing.T) {\n\tt.Parallel()\n\ttc := NewMemoFilterTestContext(t)\n\tdefer tc.Close()\n\n\t// Create memos with different tags\n\ttc.CreateMemo(NewMemoBuilder(\"memo-archive1\", tc.User.ID).\n\t\tContent(\"Archived project memo\").\n\t\tTags(\"archive/project\", \"done\"))\n\n\ttc.CreateMemo(NewMemoBuilder(\"memo-archive2\", tc.User.ID).\n\t\tContent(\"Archived work memo\").\n\t\tTags(\"archive/work\", \"old\"))\n\n\ttc.CreateMemo(NewMemoBuilder(\"memo-active\", tc.User.ID).\n\t\tContent(\"Active project memo\").\n\t\tTags(\"project/active\", \"todo\"))\n\n\ttc.CreateMemo(NewMemoBuilder(\"memo-homelab\", tc.User.ID).\n\t\tContent(\"Homelab memo\").\n\t\tTags(\"homelab/memos\", \"tech\"))\n\n\t// Test: tags.exists(t, t.startsWith(\"archive\")) - should match archived memos\n\tmemos := tc.ListWithFilter(`tags.exists(t, t.startsWith(\"archive\"))`)\n\trequire.Len(t, memos, 2, \"Should find 2 archived memos\")\n\tfor _, memo := range memos {\n\t\thasArchiveTag := false\n\t\tfor _, tag := range memo.Payload.Tags {\n\t\t\tif len(tag) >= 7 && tag[:7] == \"archive\" {\n\t\t\t\thasArchiveTag = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\trequire.True(t, hasArchiveTag, \"Memo should have tag starting with 'archive'\")\n\t}\n\n\t// Test: !tags.exists(t, t.startsWith(\"archive\")) - should match non-archived memos\n\tmemos = tc.ListWithFilter(`!tags.exists(t, t.startsWith(\"archive\"))`)\n\trequire.Len(t, memos, 2, \"Should find 2 non-archived memos\")\n\n\t// Test: tags.exists(t, t.startsWith(\"project\")) - should match project memos\n\tmemos = tc.ListWithFilter(`tags.exists(t, t.startsWith(\"project\"))`)\n\trequire.Len(t, memos, 1, \"Should find 1 project memo\")\n\n\t// Test: tags.exists(t, t.startsWith(\"homelab\")) - should match homelab memos\n\tmemos = tc.ListWithFilter(`tags.exists(t, t.startsWith(\"homelab\"))`)\n\trequire.Len(t, memos, 1, \"Should find 1 homelab memo\")\n\n\t// Test: tags.exists(t, t.startsWith(\"nonexistent\")) - should match nothing\n\tmemos = tc.ListWithFilter(`tags.exists(t, t.startsWith(\"nonexistent\"))`)\n\trequire.Len(t, memos, 0, \"Should find no memos\")\n}\n\nfunc TestMemoFilterTagsExistsContains(t *testing.T) {\n\tt.Parallel()\n\ttc := NewMemoFilterTestContext(t)\n\tdefer tc.Close()\n\n\t// Create memos with different tags\n\ttc.CreateMemo(NewMemoBuilder(\"memo-todo1\", tc.User.ID).\n\t\tContent(\"Todo task 1\").\n\t\tTags(\"project/todo\", \"urgent\"))\n\n\ttc.CreateMemo(NewMemoBuilder(\"memo-todo2\", tc.User.ID).\n\t\tContent(\"Todo task 2\").\n\t\tTags(\"work/todo-list\", \"pending\"))\n\n\ttc.CreateMemo(NewMemoBuilder(\"memo-done\", tc.User.ID).\n\t\tContent(\"Done task\").\n\t\tTags(\"project/completed\", \"done\"))\n\n\t// Test: tags.exists(t, t.contains(\"todo\")) - should match todos\n\tmemos := tc.ListWithFilter(`tags.exists(t, t.contains(\"todo\"))`)\n\trequire.Len(t, memos, 2, \"Should find 2 todo memos\")\n\n\t// Test: tags.exists(t, t.contains(\"done\")) - should match done\n\tmemos = tc.ListWithFilter(`tags.exists(t, t.contains(\"done\"))`)\n\trequire.Len(t, memos, 1, \"Should find 1 done memo\")\n\n\t// Test: !tags.exists(t, t.contains(\"todo\")) - should exclude todos\n\tmemos = tc.ListWithFilter(`!tags.exists(t, t.contains(\"todo\"))`)\n\trequire.Len(t, memos, 1, \"Should find 1 non-todo memo\")\n}\n\nfunc TestMemoFilterTagsExistsEndsWith(t *testing.T) {\n\tt.Parallel()\n\ttc := NewMemoFilterTestContext(t)\n\tdefer tc.Close()\n\n\t// Create memos with different tag endings\n\ttc.CreateMemo(NewMemoBuilder(\"memo-bug\", tc.User.ID).\n\t\tContent(\"Bug report\").\n\t\tTags(\"project/bug\", \"critical\"))\n\n\ttc.CreateMemo(NewMemoBuilder(\"memo-debug\", tc.User.ID).\n\t\tContent(\"Debug session\").\n\t\tTags(\"work/debug\", \"dev\"))\n\n\ttc.CreateMemo(NewMemoBuilder(\"memo-feature\", tc.User.ID).\n\t\tContent(\"New feature\").\n\t\tTags(\"project/feature\", \"new\"))\n\n\t// Test: tags.exists(t, t.endsWith(\"bug\")) - should match bug-related tags\n\tmemos := tc.ListWithFilter(`tags.exists(t, t.endsWith(\"bug\"))`)\n\trequire.Len(t, memos, 2, \"Should find 2 bug-related memos\")\n\n\t// Test: tags.exists(t, t.endsWith(\"feature\")) - should match feature\n\tmemos = tc.ListWithFilter(`tags.exists(t, t.endsWith(\"feature\"))`)\n\trequire.Len(t, memos, 1, \"Should find 1 feature memo\")\n\n\t// Test: !tags.exists(t, t.endsWith(\"bug\")) - should exclude bug-related\n\tmemos = tc.ListWithFilter(`!tags.exists(t, t.endsWith(\"bug\"))`)\n\trequire.Len(t, memos, 1, \"Should find 1 non-bug memo\")\n}\n\nfunc TestMemoFilterTagsExistsCombinedWithOtherFilters(t *testing.T) {\n\tt.Parallel()\n\ttc := NewMemoFilterTestContext(t)\n\tdefer tc.Close()\n\n\t// Create memos with tags and other properties\n\ttc.CreateMemo(NewMemoBuilder(\"memo-archived-old\", tc.User.ID).\n\t\tContent(\"Old archived memo\").\n\t\tTags(\"archive/old\", \"done\"))\n\n\ttc.CreateMemo(NewMemoBuilder(\"memo-archived-recent\", tc.User.ID).\n\t\tContent(\"Recent archived memo with TODO\").\n\t\tTags(\"archive/recent\", \"done\"))\n\n\ttc.CreateMemo(NewMemoBuilder(\"memo-active-todo\", tc.User.ID).\n\t\tContent(\"Active TODO\").\n\t\tTags(\"project/active\", \"todo\"))\n\n\t// Test: Combine tag filter with content filter\n\tmemos := tc.ListWithFilter(`tags.exists(t, t.startsWith(\"archive\")) && content.contains(\"TODO\")`)\n\trequire.Len(t, memos, 1, \"Should find 1 archived memo with TODO in content\")\n\n\t// Test: OR condition with tag filters\n\tmemos = tc.ListWithFilter(`tags.exists(t, t.startsWith(\"archive\")) || tags.exists(t, t.contains(\"todo\"))`)\n\trequire.Len(t, memos, 3, \"Should find all memos (archived or with todo tag)\")\n\n\t// Test: Complex filter - archived but not containing \"Recent\"\n\tmemos = tc.ListWithFilter(`tags.exists(t, t.startsWith(\"archive\")) && !content.contains(\"Recent\")`)\n\trequire.Len(t, memos, 1, \"Should find 1 old archived memo\")\n}\n\nfunc TestMemoFilterTagsExistsEmptyAndNullCases(t *testing.T) {\n\tt.Parallel()\n\ttc := NewMemoFilterTestContext(t)\n\tdefer tc.Close()\n\n\t// Create memo with no tags\n\ttc.CreateMemo(NewMemoBuilder(\"memo-no-tags\", tc.User.ID).\n\t\tContent(\"Memo without tags\"))\n\n\t// Create memo with tags\n\ttc.CreateMemo(NewMemoBuilder(\"memo-with-tags\", tc.User.ID).\n\t\tContent(\"Memo with tags\").\n\t\tTags(\"tag1\", \"tag2\"))\n\n\t// Test: tags.exists should not match memos without tags\n\tmemos := tc.ListWithFilter(`tags.exists(t, t.startsWith(\"tag\"))`)\n\trequire.Len(t, memos, 1, \"Should only find memo with tags\")\n\n\t// Test: Negation should match memos without matching tags\n\tmemos = tc.ListWithFilter(`!tags.exists(t, t.startsWith(\"tag\"))`)\n\trequire.Len(t, memos, 1, \"Should find memo without matching tags\")\n}\n\nfunc TestMemoFilterIssue5480_ArchiveWorkflow(t *testing.T) {\n\tt.Parallel()\n\ttc := NewMemoFilterTestContext(t)\n\tdefer tc.Close()\n\n\t// Create a realistic scenario as described in issue #5480\n\t// User has hierarchical tags and archives memos by prefixing with \"archive\"\n\n\t// Active memos\n\ttc.CreateMemo(NewMemoBuilder(\"memo-homelab\", tc.User.ID).\n\t\tContent(\"Setting up Memos\").\n\t\tTags(\"homelab/memos\", \"tech\"))\n\n\ttc.CreateMemo(NewMemoBuilder(\"memo-project-alpha\", tc.User.ID).\n\t\tContent(\"Project Alpha notes\").\n\t\tTags(\"work/project-alpha\", \"active\"))\n\n\t// Archived memos (user prefixed tags with \"archive\")\n\ttc.CreateMemo(NewMemoBuilder(\"memo-old-homelab\", tc.User.ID).\n\t\tContent(\"Old homelab setup\").\n\t\tTags(\"archive/homelab/old-server\", \"done\"))\n\n\ttc.CreateMemo(NewMemoBuilder(\"memo-old-project\", tc.User.ID).\n\t\tContent(\"Old project beta\").\n\t\tTags(\"archive/work/project-beta\", \"completed\"))\n\n\ttc.CreateMemo(NewMemoBuilder(\"memo-archived-personal\", tc.User.ID).\n\t\tContent(\"Archived personal note\").\n\t\tTags(\"archive/personal/2024\", \"old\"))\n\n\t// Test: Filter out ALL archived memos using startsWith\n\tmemos := tc.ListWithFilter(`!tags.exists(t, t.startsWith(\"archive\"))`)\n\trequire.Len(t, memos, 2, \"Should only show active memos (not archived)\")\n\tfor _, memo := range memos {\n\t\tfor _, tag := range memo.Payload.Tags {\n\t\t\trequire.NotContains(t, tag, \"archive\", \"Active memos should not have archive prefix\")\n\t\t}\n\t}\n\n\t// Test: Show ONLY archived memos\n\tmemos = tc.ListWithFilter(`tags.exists(t, t.startsWith(\"archive\"))`)\n\trequire.Len(t, memos, 3, \"Should find all archived memos\")\n\tfor _, memo := range memos {\n\t\thasArchiveTag := false\n\t\tfor _, tag := range memo.Payload.Tags {\n\t\t\tif len(tag) >= 7 && tag[:7] == \"archive\" {\n\t\t\t\thasArchiveTag = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\trequire.True(t, hasArchiveTag, \"All returned memos should have archive prefix\")\n\t}\n\n\t// Test: Filter archived homelab memos specifically\n\tmemos = tc.ListWithFilter(`tags.exists(t, t.startsWith(\"archive/homelab\"))`)\n\trequire.Len(t, memos, 1, \"Should find only archived homelab memos\")\n}\n\n// =============================================================================\n// Multiple Filters Tests\n// =============================================================================\n\nfunc TestMemoFilterMultipleFilters(t *testing.T) {\n\tt.Parallel()\n\ttc := NewMemoFilterTestContext(t)\n\tdefer tc.Close()\n\n\ttc.CreateMemo(NewMemoBuilder(\"memo-public-hello\", tc.User.ID).Content(\"Hello world\").Visibility(store.Public))\n\ttc.CreateMemo(NewMemoBuilder(\"memo-private-hello\", tc.User.ID).Content(\"Hello private\").Visibility(store.Private))\n\n\t// Test: Multiple filters (applied as AND)\n\tmemos := tc.ListWithFilters(`content.contains(\"Hello\")`, `visibility == \"PUBLIC\"`)\n\trequire.Len(t, memos, 1)\n\trequire.Contains(t, memos[0].Content, \"Hello\")\n\trequire.Equal(t, store.Public, memos[0].Visibility)\n}\n\n// =============================================================================\n// Edge Cases\n// =============================================================================\n\nfunc TestMemoFilterNullPayload(t *testing.T) {\n\tt.Parallel()\n\ttc := NewMemoFilterTestContext(t)\n\tdefer tc.Close()\n\n\ttc.CreateMemo(NewMemoBuilder(\"memo-null-payload\", tc.User.ID).Content(\"Null payload\"))\n\n\t// Test: has_link should not crash and return no results\n\tmemos := tc.ListWithFilter(`has_link`)\n\trequire.Len(t, memos, 0)\n}\n\nfunc TestMemoFilterNoMatches(t *testing.T) {\n\tt.Parallel()\n\ttc := NewMemoFilterTestContext(t)\n\tdefer tc.Close()\n\n\ttc.CreateMemo(NewMemoBuilder(\"memo-test\", tc.User.ID).Content(\"Test content\"))\n\n\tmemos := tc.ListWithFilter(`content.contains(\"nonexistent12345\")`)\n\trequire.Len(t, memos, 0)\n}\n\nfunc TestMemoFilterJSONBooleanLogic(t *testing.T) {\n\tt.Parallel()\n\ttc := NewMemoFilterTestContext(t)\n\tdefer tc.Close()\n\n\t// 1. Memo with task list (true) and NO link (null)\n\ttc.CreateMemo(NewMemoBuilder(\"memo-task-only\", tc.User.ID).\n\t\tContent(\"Task only\").\n\t\tProperty(func(p *storepb.MemoPayload_Property) { p.HasTaskList = true }))\n\n\t// 2. Memo with link (true) and NO task list (null)\n\ttc.CreateMemo(NewMemoBuilder(\"memo-link-only\", tc.User.ID).\n\t\tContent(\"Link only\").\n\t\tProperty(func(p *storepb.MemoPayload_Property) { p.HasLink = true }))\n\n\t// 3. Memo with both (true)\n\ttc.CreateMemo(NewMemoBuilder(\"memo-both\", tc.User.ID).\n\t\tContent(\"Both\").\n\t\tProperty(func(p *storepb.MemoPayload_Property) {\n\t\t\tp.HasTaskList = true\n\t\t\tp.HasLink = true\n\t\t}))\n\n\t// 4. Memo with neither (null)\n\ttc.CreateMemo(NewMemoBuilder(\"memo-neither\", tc.User.ID).Content(\"Neither\"))\n\n\t// Test A: has_task_list || has_link\n\t// Expected: 3 memos (task-only, link-only, both). Neither should be excluded.\n\t// This specifically tests the NULL handling in OR logic (NULL || TRUE should be TRUE)\n\tmemos := tc.ListWithFilter(`has_task_list || has_link`)\n\trequire.Len(t, memos, 3, \"Should find 3 memos with OR logic\")\n\n\t// Test B: !has_task_list\n\t// Expected: 2 memos (link-only, neither). Memos where has_task_list is NULL or FALSE.\n\t// Note: If NULL is not handled, !NULL is still NULL (false-y in WHERE), so \"neither\" might be missed depending on logic.\n\t// In our implementation, we want missing fields to behave as false.\n\tmemos = tc.ListWithFilter(`!has_task_list`)\n\trequire.Len(t, memos, 2, \"Should find 2 memos where task list is false or missing\")\n\n\t// Test C: has_task_list && !has_link\n\t// Expected: 1 memo (task-only).\n\tmemos = tc.ListWithFilter(`has_task_list && !has_link`)\n\trequire.Len(t, memos, 1, \"Should find 1 memo (task only)\")\n}\n"
  },
  {
    "path": "store/test/memo_relation_test.go",
    "content": "package test\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/usememos/memos/store\"\n)\n\nfunc TestMemoRelationStore(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\tuser, err := createTestingHostUser(ctx, ts)\n\trequire.NoError(t, err)\n\tmemoCreate := &store.Memo{\n\t\tUID:        \"main-memo\",\n\t\tCreatorID:  user.ID,\n\t\tContent:    \"main memo content\",\n\t\tVisibility: store.Public,\n\t}\n\tmemo, err := ts.CreateMemo(ctx, memoCreate)\n\trequire.NoError(t, err)\n\trequire.Equal(t, memoCreate.Content, memo.Content)\n\trelatedMemoCreate := &store.Memo{\n\t\tUID:        \"related-memo\",\n\t\tCreatorID:  user.ID,\n\t\tContent:    \"related memo content\",\n\t\tVisibility: store.Public,\n\t}\n\trelatedMemo, err := ts.CreateMemo(ctx, relatedMemoCreate)\n\trequire.NoError(t, err)\n\trequire.Equal(t, relatedMemoCreate.Content, relatedMemo.Content)\n\tcommentMemoCreate := &store.Memo{\n\t\tUID:        \"comment-memo\",\n\t\tCreatorID:  user.ID,\n\t\tContent:    \"comment memo content\",\n\t\tVisibility: store.Public,\n\t}\n\tcommentMemo, err := ts.CreateMemo(ctx, commentMemoCreate)\n\trequire.NoError(t, err)\n\trequire.Equal(t, commentMemoCreate.Content, commentMemo.Content)\n\n\t// Reference relation.\n\treferenceRelation := &store.MemoRelation{\n\t\tMemoID:        memo.ID,\n\t\tRelatedMemoID: relatedMemo.ID,\n\t\tType:          store.MemoRelationReference,\n\t}\n\t_, err = ts.UpsertMemoRelation(ctx, referenceRelation)\n\trequire.NoError(t, err)\n\t// Comment relation.\n\tcommentRelation := &store.MemoRelation{\n\t\tMemoID:        memo.ID,\n\t\tRelatedMemoID: commentMemo.ID,\n\t\tType:          store.MemoRelationComment,\n\t}\n\t_, err = ts.UpsertMemoRelation(ctx, commentRelation)\n\trequire.NoError(t, err)\n\tts.Close()\n}\n\nfunc TestMemoRelationListByMemoID(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\tuser, err := createTestingHostUser(ctx, ts)\n\trequire.NoError(t, err)\n\n\t// Create main memo\n\tmainMemo, err := ts.CreateMemo(ctx, &store.Memo{\n\t\tUID:        \"main-memo\",\n\t\tCreatorID:  user.ID,\n\t\tContent:    \"main memo content\",\n\t\tVisibility: store.Public,\n\t})\n\trequire.NoError(t, err)\n\n\t// Create related memos\n\trelatedMemo1, err := ts.CreateMemo(ctx, &store.Memo{\n\t\tUID:        \"related-memo-1\",\n\t\tCreatorID:  user.ID,\n\t\tContent:    \"related memo 1 content\",\n\t\tVisibility: store.Public,\n\t})\n\trequire.NoError(t, err)\n\n\trelatedMemo2, err := ts.CreateMemo(ctx, &store.Memo{\n\t\tUID:        \"related-memo-2\",\n\t\tCreatorID:  user.ID,\n\t\tContent:    \"related memo 2 content\",\n\t\tVisibility: store.Public,\n\t})\n\trequire.NoError(t, err)\n\n\t// Create relations\n\t_, err = ts.UpsertMemoRelation(ctx, &store.MemoRelation{\n\t\tMemoID:        mainMemo.ID,\n\t\tRelatedMemoID: relatedMemo1.ID,\n\t\tType:          store.MemoRelationReference,\n\t})\n\trequire.NoError(t, err)\n\n\t_, err = ts.UpsertMemoRelation(ctx, &store.MemoRelation{\n\t\tMemoID:        mainMemo.ID,\n\t\tRelatedMemoID: relatedMemo2.ID,\n\t\tType:          store.MemoRelationComment,\n\t})\n\trequire.NoError(t, err)\n\n\t// List by memo ID\n\trelations, err := ts.ListMemoRelations(ctx, &store.FindMemoRelation{\n\t\tMemoID: &mainMemo.ID,\n\t})\n\trequire.NoError(t, err)\n\trequire.Equal(t, 2, len(relations))\n\n\t// List by type\n\trefType := store.MemoRelationReference\n\trefRelations, err := ts.ListMemoRelations(ctx, &store.FindMemoRelation{\n\t\tMemoID: &mainMemo.ID,\n\t\tType:   &refType,\n\t})\n\trequire.NoError(t, err)\n\trequire.Equal(t, 1, len(refRelations))\n\trequire.Equal(t, store.MemoRelationReference, refRelations[0].Type)\n\n\t// List by related memo ID\n\trelations, err = ts.ListMemoRelations(ctx, &store.FindMemoRelation{\n\t\tRelatedMemoID: &relatedMemo1.ID,\n\t})\n\trequire.NoError(t, err)\n\trequire.Equal(t, 1, len(relations))\n\n\tts.Close()\n}\n\nfunc TestMemoRelationDelete(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\tuser, err := createTestingHostUser(ctx, ts)\n\trequire.NoError(t, err)\n\n\t// Create memos\n\tmainMemo, err := ts.CreateMemo(ctx, &store.Memo{\n\t\tUID:        \"main-memo\",\n\t\tCreatorID:  user.ID,\n\t\tContent:    \"main memo content\",\n\t\tVisibility: store.Public,\n\t})\n\trequire.NoError(t, err)\n\n\trelatedMemo, err := ts.CreateMemo(ctx, &store.Memo{\n\t\tUID:        \"related-memo\",\n\t\tCreatorID:  user.ID,\n\t\tContent:    \"related memo content\",\n\t\tVisibility: store.Public,\n\t})\n\trequire.NoError(t, err)\n\n\t// Create relation\n\t_, err = ts.UpsertMemoRelation(ctx, &store.MemoRelation{\n\t\tMemoID:        mainMemo.ID,\n\t\tRelatedMemoID: relatedMemo.ID,\n\t\tType:          store.MemoRelationReference,\n\t})\n\trequire.NoError(t, err)\n\n\t// Verify relation exists\n\trelations, err := ts.ListMemoRelations(ctx, &store.FindMemoRelation{\n\t\tMemoID: &mainMemo.ID,\n\t})\n\trequire.NoError(t, err)\n\trequire.Equal(t, 1, len(relations))\n\n\t// Delete relation by memo ID\n\trelType := store.MemoRelationReference\n\terr = ts.DeleteMemoRelation(ctx, &store.DeleteMemoRelation{\n\t\tMemoID:        &mainMemo.ID,\n\t\tRelatedMemoID: &relatedMemo.ID,\n\t\tType:          &relType,\n\t})\n\trequire.NoError(t, err)\n\n\t// Verify relation is deleted\n\trelations, err = ts.ListMemoRelations(ctx, &store.FindMemoRelation{\n\t\tMemoID: &mainMemo.ID,\n\t})\n\trequire.NoError(t, err)\n\trequire.Equal(t, 0, len(relations))\n\n\tts.Close()\n}\n\nfunc TestMemoRelationDifferentTypes(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\tuser, err := createTestingHostUser(ctx, ts)\n\trequire.NoError(t, err)\n\n\tmainMemo, err := ts.CreateMemo(ctx, &store.Memo{\n\t\tUID:        \"main-memo\",\n\t\tCreatorID:  user.ID,\n\t\tContent:    \"main memo content\",\n\t\tVisibility: store.Public,\n\t})\n\trequire.NoError(t, err)\n\n\trelatedMemo, err := ts.CreateMemo(ctx, &store.Memo{\n\t\tUID:        \"related-memo\",\n\t\tCreatorID:  user.ID,\n\t\tContent:    \"related memo content\",\n\t\tVisibility: store.Public,\n\t})\n\trequire.NoError(t, err)\n\n\t// Create reference relation\n\t_, err = ts.UpsertMemoRelation(ctx, &store.MemoRelation{\n\t\tMemoID:        mainMemo.ID,\n\t\tRelatedMemoID: relatedMemo.ID,\n\t\tType:          store.MemoRelationReference,\n\t})\n\trequire.NoError(t, err)\n\n\t// Create comment relation (same memos, different type - should be allowed)\n\t_, err = ts.UpsertMemoRelation(ctx, &store.MemoRelation{\n\t\tMemoID:        mainMemo.ID,\n\t\tRelatedMemoID: relatedMemo.ID,\n\t\tType:          store.MemoRelationComment,\n\t})\n\trequire.NoError(t, err)\n\n\t// Verify both relations exist\n\trelations, err := ts.ListMemoRelations(ctx, &store.FindMemoRelation{\n\t\tMemoID: &mainMemo.ID,\n\t})\n\trequire.NoError(t, err)\n\trequire.Equal(t, 2, len(relations))\n\n\tts.Close()\n}\n\nfunc TestMemoRelationUpsertSameRelation(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\tuser, err := createTestingHostUser(ctx, ts)\n\trequire.NoError(t, err)\n\n\tmainMemo, err := ts.CreateMemo(ctx, &store.Memo{\n\t\tUID:        \"main-memo\",\n\t\tCreatorID:  user.ID,\n\t\tContent:    \"main memo content\",\n\t\tVisibility: store.Public,\n\t})\n\trequire.NoError(t, err)\n\n\trelatedMemo, err := ts.CreateMemo(ctx, &store.Memo{\n\t\tUID:        \"related-memo\",\n\t\tCreatorID:  user.ID,\n\t\tContent:    \"related memo content\",\n\t\tVisibility: store.Public,\n\t})\n\trequire.NoError(t, err)\n\n\t// Create relation\n\t_, err = ts.UpsertMemoRelation(ctx, &store.MemoRelation{\n\t\tMemoID:        mainMemo.ID,\n\t\tRelatedMemoID: relatedMemo.ID,\n\t\tType:          store.MemoRelationReference,\n\t})\n\trequire.NoError(t, err)\n\n\t// Upsert the same relation again (should not create duplicate)\n\t_, err = ts.UpsertMemoRelation(ctx, &store.MemoRelation{\n\t\tMemoID:        mainMemo.ID,\n\t\tRelatedMemoID: relatedMemo.ID,\n\t\tType:          store.MemoRelationReference,\n\t})\n\trequire.NoError(t, err)\n\n\t// Verify only one relation exists\n\trelations, err := ts.ListMemoRelations(ctx, &store.FindMemoRelation{\n\t\tMemoID: &mainMemo.ID,\n\t})\n\trequire.NoError(t, err)\n\trequire.Len(t, relations, 1)\n\n\tts.Close()\n}\n\nfunc TestMemoRelationDeleteByType(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\tuser, err := createTestingHostUser(ctx, ts)\n\trequire.NoError(t, err)\n\n\tmainMemo, err := ts.CreateMemo(ctx, &store.Memo{\n\t\tUID:        \"main-memo\",\n\t\tCreatorID:  user.ID,\n\t\tContent:    \"main memo content\",\n\t\tVisibility: store.Public,\n\t})\n\trequire.NoError(t, err)\n\n\trelatedMemo1, err := ts.CreateMemo(ctx, &store.Memo{\n\t\tUID:        \"related-memo-1\",\n\t\tCreatorID:  user.ID,\n\t\tContent:    \"related memo 1 content\",\n\t\tVisibility: store.Public,\n\t})\n\trequire.NoError(t, err)\n\n\trelatedMemo2, err := ts.CreateMemo(ctx, &store.Memo{\n\t\tUID:        \"related-memo-2\",\n\t\tCreatorID:  user.ID,\n\t\tContent:    \"related memo 2 content\",\n\t\tVisibility: store.Public,\n\t})\n\trequire.NoError(t, err)\n\n\t// Create reference relations\n\t_, err = ts.UpsertMemoRelation(ctx, &store.MemoRelation{\n\t\tMemoID:        mainMemo.ID,\n\t\tRelatedMemoID: relatedMemo1.ID,\n\t\tType:          store.MemoRelationReference,\n\t})\n\trequire.NoError(t, err)\n\n\t// Create comment relation\n\t_, err = ts.UpsertMemoRelation(ctx, &store.MemoRelation{\n\t\tMemoID:        mainMemo.ID,\n\t\tRelatedMemoID: relatedMemo2.ID,\n\t\tType:          store.MemoRelationComment,\n\t})\n\trequire.NoError(t, err)\n\n\t// Delete only reference type relations\n\trefType := store.MemoRelationReference\n\terr = ts.DeleteMemoRelation(ctx, &store.DeleteMemoRelation{\n\t\tMemoID: &mainMemo.ID,\n\t\tType:   &refType,\n\t})\n\trequire.NoError(t, err)\n\n\t// Verify only comment relation remains\n\trelations, err := ts.ListMemoRelations(ctx, &store.FindMemoRelation{\n\t\tMemoID: &mainMemo.ID,\n\t})\n\trequire.NoError(t, err)\n\trequire.Len(t, relations, 1)\n\trequire.Equal(t, store.MemoRelationComment, relations[0].Type)\n\n\tts.Close()\n}\n\nfunc TestMemoRelationDeleteByMemoID(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\tuser, err := createTestingHostUser(ctx, ts)\n\trequire.NoError(t, err)\n\n\tmemo1, err := ts.CreateMemo(ctx, &store.Memo{\n\t\tUID:        \"memo-1\",\n\t\tCreatorID:  user.ID,\n\t\tContent:    \"memo 1 content\",\n\t\tVisibility: store.Public,\n\t})\n\trequire.NoError(t, err)\n\n\tmemo2, err := ts.CreateMemo(ctx, &store.Memo{\n\t\tUID:        \"memo-2\",\n\t\tCreatorID:  user.ID,\n\t\tContent:    \"memo 2 content\",\n\t\tVisibility: store.Public,\n\t})\n\trequire.NoError(t, err)\n\n\trelatedMemo, err := ts.CreateMemo(ctx, &store.Memo{\n\t\tUID:        \"related-memo\",\n\t\tCreatorID:  user.ID,\n\t\tContent:    \"related memo content\",\n\t\tVisibility: store.Public,\n\t})\n\trequire.NoError(t, err)\n\n\t// Create relations for both memos\n\t_, err = ts.UpsertMemoRelation(ctx, &store.MemoRelation{\n\t\tMemoID:        memo1.ID,\n\t\tRelatedMemoID: relatedMemo.ID,\n\t\tType:          store.MemoRelationReference,\n\t})\n\trequire.NoError(t, err)\n\n\t_, err = ts.UpsertMemoRelation(ctx, &store.MemoRelation{\n\t\tMemoID:        memo2.ID,\n\t\tRelatedMemoID: relatedMemo.ID,\n\t\tType:          store.MemoRelationReference,\n\t})\n\trequire.NoError(t, err)\n\n\t// Delete all relations for memo1\n\terr = ts.DeleteMemoRelation(ctx, &store.DeleteMemoRelation{\n\t\tMemoID: &memo1.ID,\n\t})\n\trequire.NoError(t, err)\n\n\t// Verify memo1's relations are gone\n\trelations, err := ts.ListMemoRelations(ctx, &store.FindMemoRelation{\n\t\tMemoID: &memo1.ID,\n\t})\n\trequire.NoError(t, err)\n\trequire.Len(t, relations, 0)\n\n\t// Verify memo2's relations still exist\n\trelations, err = ts.ListMemoRelations(ctx, &store.FindMemoRelation{\n\t\tMemoID: &memo2.ID,\n\t})\n\trequire.NoError(t, err)\n\trequire.Len(t, relations, 1)\n\n\tts.Close()\n}\n\nfunc TestMemoRelationListByRelatedMemoID(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\tuser, err := createTestingHostUser(ctx, ts)\n\trequire.NoError(t, err)\n\n\t// Create a memo that will be referenced by others\n\ttargetMemo, err := ts.CreateMemo(ctx, &store.Memo{\n\t\tUID:        \"target-memo\",\n\t\tCreatorID:  user.ID,\n\t\tContent:    \"target memo content\",\n\t\tVisibility: store.Public,\n\t})\n\trequire.NoError(t, err)\n\n\t// Create memos that reference the target\n\treferrer1, err := ts.CreateMemo(ctx, &store.Memo{\n\t\tUID:        \"referrer-1\",\n\t\tCreatorID:  user.ID,\n\t\tContent:    \"referrer 1 content\",\n\t\tVisibility: store.Public,\n\t})\n\trequire.NoError(t, err)\n\n\treferrer2, err := ts.CreateMemo(ctx, &store.Memo{\n\t\tUID:        \"referrer-2\",\n\t\tCreatorID:  user.ID,\n\t\tContent:    \"referrer 2 content\",\n\t\tVisibility: store.Public,\n\t})\n\trequire.NoError(t, err)\n\n\t// Create relations pointing to target\n\t_, err = ts.UpsertMemoRelation(ctx, &store.MemoRelation{\n\t\tMemoID:        referrer1.ID,\n\t\tRelatedMemoID: targetMemo.ID,\n\t\tType:          store.MemoRelationReference,\n\t})\n\trequire.NoError(t, err)\n\n\t_, err = ts.UpsertMemoRelation(ctx, &store.MemoRelation{\n\t\tMemoID:        referrer2.ID,\n\t\tRelatedMemoID: targetMemo.ID,\n\t\tType:          store.MemoRelationComment,\n\t})\n\trequire.NoError(t, err)\n\n\t// List by related memo ID (find all memos that reference the target)\n\trelations, err := ts.ListMemoRelations(ctx, &store.FindMemoRelation{\n\t\tRelatedMemoID: &targetMemo.ID,\n\t})\n\trequire.NoError(t, err)\n\trequire.Len(t, relations, 2)\n\n\tts.Close()\n}\n\nfunc TestMemoRelationListCombinedFilters(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\tuser, err := createTestingHostUser(ctx, ts)\n\trequire.NoError(t, err)\n\n\tmainMemo, err := ts.CreateMemo(ctx, &store.Memo{\n\t\tUID:        \"main-memo\",\n\t\tCreatorID:  user.ID,\n\t\tContent:    \"main memo content\",\n\t\tVisibility: store.Public,\n\t})\n\trequire.NoError(t, err)\n\n\trelatedMemo1, err := ts.CreateMemo(ctx, &store.Memo{\n\t\tUID:        \"related-memo-1\",\n\t\tCreatorID:  user.ID,\n\t\tContent:    \"related memo 1 content\",\n\t\tVisibility: store.Public,\n\t})\n\trequire.NoError(t, err)\n\n\trelatedMemo2, err := ts.CreateMemo(ctx, &store.Memo{\n\t\tUID:        \"related-memo-2\",\n\t\tCreatorID:  user.ID,\n\t\tContent:    \"related memo 2 content\",\n\t\tVisibility: store.Public,\n\t})\n\trequire.NoError(t, err)\n\n\t// Create multiple relations\n\t_, err = ts.UpsertMemoRelation(ctx, &store.MemoRelation{\n\t\tMemoID:        mainMemo.ID,\n\t\tRelatedMemoID: relatedMemo1.ID,\n\t\tType:          store.MemoRelationReference,\n\t})\n\trequire.NoError(t, err)\n\n\t_, err = ts.UpsertMemoRelation(ctx, &store.MemoRelation{\n\t\tMemoID:        mainMemo.ID,\n\t\tRelatedMemoID: relatedMemo2.ID,\n\t\tType:          store.MemoRelationComment,\n\t})\n\trequire.NoError(t, err)\n\n\t// List with MemoID and Type filter\n\trefType := store.MemoRelationReference\n\trelations, err := ts.ListMemoRelations(ctx, &store.FindMemoRelation{\n\t\tMemoID: &mainMemo.ID,\n\t\tType:   &refType,\n\t})\n\trequire.NoError(t, err)\n\trequire.Len(t, relations, 1)\n\trequire.Equal(t, relatedMemo1.ID, relations[0].RelatedMemoID)\n\n\t// List with MemoID, RelatedMemoID, and Type filter\n\tcommentType := store.MemoRelationComment\n\trelations, err = ts.ListMemoRelations(ctx, &store.FindMemoRelation{\n\t\tMemoID:        &mainMemo.ID,\n\t\tRelatedMemoID: &relatedMemo2.ID,\n\t\tType:          &commentType,\n\t})\n\trequire.NoError(t, err)\n\trequire.Len(t, relations, 1)\n\n\tts.Close()\n}\n\nfunc TestMemoRelationListEmpty(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\tuser, err := createTestingHostUser(ctx, ts)\n\trequire.NoError(t, err)\n\n\tmemo, err := ts.CreateMemo(ctx, &store.Memo{\n\t\tUID:        \"memo-no-relations\",\n\t\tCreatorID:  user.ID,\n\t\tContent:    \"memo with no relations\",\n\t\tVisibility: store.Public,\n\t})\n\trequire.NoError(t, err)\n\n\t// List relations for memo with none\n\trelations, err := ts.ListMemoRelations(ctx, &store.FindMemoRelation{\n\t\tMemoID: &memo.ID,\n\t})\n\trequire.NoError(t, err)\n\trequire.Len(t, relations, 0)\n\n\tts.Close()\n}\n\nfunc TestMemoRelationBidirectional(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\tuser, err := createTestingHostUser(ctx, ts)\n\trequire.NoError(t, err)\n\n\tmemoA, err := ts.CreateMemo(ctx, &store.Memo{\n\t\tUID:        \"memo-a\",\n\t\tCreatorID:  user.ID,\n\t\tContent:    \"memo A content\",\n\t\tVisibility: store.Public,\n\t})\n\trequire.NoError(t, err)\n\n\tmemoB, err := ts.CreateMemo(ctx, &store.Memo{\n\t\tUID:        \"memo-b\",\n\t\tCreatorID:  user.ID,\n\t\tContent:    \"memo B content\",\n\t\tVisibility: store.Public,\n\t})\n\trequire.NoError(t, err)\n\n\t// Create relation A -> B\n\t_, err = ts.UpsertMemoRelation(ctx, &store.MemoRelation{\n\t\tMemoID:        memoA.ID,\n\t\tRelatedMemoID: memoB.ID,\n\t\tType:          store.MemoRelationReference,\n\t})\n\trequire.NoError(t, err)\n\n\t// Create relation B -> A (reverse direction)\n\t_, err = ts.UpsertMemoRelation(ctx, &store.MemoRelation{\n\t\tMemoID:        memoB.ID,\n\t\tRelatedMemoID: memoA.ID,\n\t\tType:          store.MemoRelationReference,\n\t})\n\trequire.NoError(t, err)\n\n\t// Verify A -> B exists\n\trelationsFromA, err := ts.ListMemoRelations(ctx, &store.FindMemoRelation{\n\t\tMemoID: &memoA.ID,\n\t})\n\trequire.NoError(t, err)\n\trequire.Len(t, relationsFromA, 1)\n\trequire.Equal(t, memoB.ID, relationsFromA[0].RelatedMemoID)\n\n\t// Verify B -> A exists\n\trelationsFromB, err := ts.ListMemoRelations(ctx, &store.FindMemoRelation{\n\t\tMemoID: &memoB.ID,\n\t})\n\trequire.NoError(t, err)\n\trequire.Len(t, relationsFromB, 1)\n\trequire.Equal(t, memoA.ID, relationsFromB[0].RelatedMemoID)\n\n\tts.Close()\n}\n\nfunc TestMemoRelationListByMemoIDList(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\tuser, err := createTestingHostUser(ctx, ts)\n\trequire.NoError(t, err)\n\n\t// Create 3 memos.\n\tmemoA, err := ts.CreateMemo(ctx, &store.Memo{\n\t\tUID:        \"memo-a\",\n\t\tCreatorID:  user.ID,\n\t\tContent:    \"memo A content\",\n\t\tVisibility: store.Public,\n\t})\n\trequire.NoError(t, err)\n\n\tmemoB, err := ts.CreateMemo(ctx, &store.Memo{\n\t\tUID:        \"memo-b\",\n\t\tCreatorID:  user.ID,\n\t\tContent:    \"memo B content\",\n\t\tVisibility: store.Public,\n\t})\n\trequire.NoError(t, err)\n\n\tmemoC, err := ts.CreateMemo(ctx, &store.Memo{\n\t\tUID:        \"memo-c\",\n\t\tCreatorID:  user.ID,\n\t\tContent:    \"memo C content\",\n\t\tVisibility: store.Public,\n\t})\n\trequire.NoError(t, err)\n\n\tmemoD, err := ts.CreateMemo(ctx, &store.Memo{\n\t\tUID:        \"memo-d\",\n\t\tCreatorID:  user.ID,\n\t\tContent:    \"memo D content\",\n\t\tVisibility: store.Public,\n\t})\n\trequire.NoError(t, err)\n\n\t// A -> B (reference)\n\t_, err = ts.UpsertMemoRelation(ctx, &store.MemoRelation{\n\t\tMemoID:        memoA.ID,\n\t\tRelatedMemoID: memoB.ID,\n\t\tType:          store.MemoRelationReference,\n\t})\n\trequire.NoError(t, err)\n\n\t// A -> C (comment)\n\t_, err = ts.UpsertMemoRelation(ctx, &store.MemoRelation{\n\t\tMemoID:        memoA.ID,\n\t\tRelatedMemoID: memoC.ID,\n\t\tType:          store.MemoRelationComment,\n\t})\n\trequire.NoError(t, err)\n\n\t// D -> B (reference) — B appears as related_memo_id\n\t_, err = ts.UpsertMemoRelation(ctx, &store.MemoRelation{\n\t\tMemoID:        memoD.ID,\n\t\tRelatedMemoID: memoB.ID,\n\t\tType:          store.MemoRelationReference,\n\t})\n\trequire.NoError(t, err)\n\n\t// Batch query for memos A and B: should return all 3 relations\n\t// (A->B because A is in list, A->C because A is in list, D->B because B is in list)\n\trelations, err := ts.ListMemoRelations(ctx, &store.FindMemoRelation{\n\t\tMemoIDList: []int32{memoA.ID, memoB.ID},\n\t})\n\trequire.NoError(t, err)\n\trequire.Len(t, relations, 3)\n\n\t// Batch query for memo C only: should return 1 relation (A->C because C is related_memo_id)\n\trelations, err = ts.ListMemoRelations(ctx, &store.FindMemoRelation{\n\t\tMemoIDList: []int32{memoC.ID},\n\t})\n\trequire.NoError(t, err)\n\trequire.Len(t, relations, 1)\n\trequire.Equal(t, memoA.ID, relations[0].MemoID)\n\trequire.Equal(t, memoC.ID, relations[0].RelatedMemoID)\n\n\t// Batch query for memo D only: should return 1 relation (D->B because D is memo_id)\n\trelations, err = ts.ListMemoRelations(ctx, &store.FindMemoRelation{\n\t\tMemoIDList: []int32{memoD.ID},\n\t})\n\trequire.NoError(t, err)\n\trequire.Len(t, relations, 1)\n\trequire.Equal(t, memoD.ID, relations[0].MemoID)\n\trequire.Equal(t, memoB.ID, relations[0].RelatedMemoID)\n\n\tts.Close()\n}\n\nfunc TestMemoRelationListByMemoIDListEmpty(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\tuser, err := createTestingHostUser(ctx, ts)\n\trequire.NoError(t, err)\n\n\tmemo, err := ts.CreateMemo(ctx, &store.Memo{\n\t\tUID:        \"memo-no-relations\",\n\t\tCreatorID:  user.ID,\n\t\tContent:    \"memo with no relations\",\n\t\tVisibility: store.Public,\n\t})\n\trequire.NoError(t, err)\n\n\t// Batch query with a memo that has no relations.\n\trelations, err := ts.ListMemoRelations(ctx, &store.FindMemoRelation{\n\t\tMemoIDList: []int32{memo.ID},\n\t})\n\trequire.NoError(t, err)\n\trequire.Len(t, relations, 0)\n\n\t// Empty MemoIDList should not filter by MemoIDList (returns based on other filters).\n\trelations, err = ts.ListMemoRelations(ctx, &store.FindMemoRelation{\n\t\tMemoIDList: []int32{},\n\t})\n\trequire.NoError(t, err)\n\trequire.Len(t, relations, 0)\n\n\tts.Close()\n}\n\nfunc TestMemoRelationListByMemoIDListWithTypeFilter(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\tuser, err := createTestingHostUser(ctx, ts)\n\trequire.NoError(t, err)\n\n\tmemoA, err := ts.CreateMemo(ctx, &store.Memo{\n\t\tUID:        \"memo-a\",\n\t\tCreatorID:  user.ID,\n\t\tContent:    \"memo A content\",\n\t\tVisibility: store.Public,\n\t})\n\trequire.NoError(t, err)\n\n\tmemoB, err := ts.CreateMemo(ctx, &store.Memo{\n\t\tUID:        \"memo-b\",\n\t\tCreatorID:  user.ID,\n\t\tContent:    \"memo B content\",\n\t\tVisibility: store.Public,\n\t})\n\trequire.NoError(t, err)\n\n\tmemoC, err := ts.CreateMemo(ctx, &store.Memo{\n\t\tUID:        \"memo-c\",\n\t\tCreatorID:  user.ID,\n\t\tContent:    \"memo C content\",\n\t\tVisibility: store.Public,\n\t})\n\trequire.NoError(t, err)\n\n\t// A -> B (reference)\n\t_, err = ts.UpsertMemoRelation(ctx, &store.MemoRelation{\n\t\tMemoID:        memoA.ID,\n\t\tRelatedMemoID: memoB.ID,\n\t\tType:          store.MemoRelationReference,\n\t})\n\trequire.NoError(t, err)\n\n\t// A -> C (comment)\n\t_, err = ts.UpsertMemoRelation(ctx, &store.MemoRelation{\n\t\tMemoID:        memoA.ID,\n\t\tRelatedMemoID: memoC.ID,\n\t\tType:          store.MemoRelationComment,\n\t})\n\trequire.NoError(t, err)\n\n\t// Batch query with type filter: only references\n\trefType := store.MemoRelationReference\n\trelations, err := ts.ListMemoRelations(ctx, &store.FindMemoRelation{\n\t\tMemoIDList: []int32{memoA.ID},\n\t\tType:       &refType,\n\t})\n\trequire.NoError(t, err)\n\trequire.Len(t, relations, 1)\n\trequire.Equal(t, store.MemoRelationReference, relations[0].Type)\n\n\t// Batch query with type filter: only comments\n\tcommentType := store.MemoRelationComment\n\trelations, err = ts.ListMemoRelations(ctx, &store.FindMemoRelation{\n\t\tMemoIDList: []int32{memoA.ID},\n\t\tType:       &commentType,\n\t})\n\trequire.NoError(t, err)\n\trequire.Len(t, relations, 1)\n\trequire.Equal(t, store.MemoRelationComment, relations[0].Type)\n\n\tts.Close()\n}\n\nfunc TestMemoRelationListByMemoIDListBothDirections(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\tuser, err := createTestingHostUser(ctx, ts)\n\trequire.NoError(t, err)\n\n\tmemoA, err := ts.CreateMemo(ctx, &store.Memo{\n\t\tUID:        \"memo-a\",\n\t\tCreatorID:  user.ID,\n\t\tContent:    \"memo A content\",\n\t\tVisibility: store.Public,\n\t})\n\trequire.NoError(t, err)\n\n\tmemoB, err := ts.CreateMemo(ctx, &store.Memo{\n\t\tUID:        \"memo-b\",\n\t\tCreatorID:  user.ID,\n\t\tContent:    \"memo B content\",\n\t\tVisibility: store.Public,\n\t})\n\trequire.NoError(t, err)\n\n\tmemoX, err := ts.CreateMemo(ctx, &store.Memo{\n\t\tUID:        \"memo-x\",\n\t\tCreatorID:  user.ID,\n\t\tContent:    \"memo X content\",\n\t\tVisibility: store.Public,\n\t})\n\trequire.NoError(t, err)\n\n\t// X -> A (A appears as related_memo_id)\n\t_, err = ts.UpsertMemoRelation(ctx, &store.MemoRelation{\n\t\tMemoID:        memoX.ID,\n\t\tRelatedMemoID: memoA.ID,\n\t\tType:          store.MemoRelationReference,\n\t})\n\trequire.NoError(t, err)\n\n\t// A -> B (A appears as memo_id)\n\t_, err = ts.UpsertMemoRelation(ctx, &store.MemoRelation{\n\t\tMemoID:        memoA.ID,\n\t\tRelatedMemoID: memoB.ID,\n\t\tType:          store.MemoRelationReference,\n\t})\n\trequire.NoError(t, err)\n\n\t// Query with MemoIDList=[A]: should find both relations (A as source and A as target).\n\trelations, err := ts.ListMemoRelations(ctx, &store.FindMemoRelation{\n\t\tMemoIDList: []int32{memoA.ID},\n\t})\n\trequire.NoError(t, err)\n\trequire.Len(t, relations, 2)\n\n\t// Verify we got both directions.\n\tmemoIDs := map[int32]bool{}\n\trelatedIDs := map[int32]bool{}\n\tfor _, r := range relations {\n\t\tmemoIDs[r.MemoID] = true\n\t\trelatedIDs[r.RelatedMemoID] = true\n\t}\n\trequire.True(t, memoIDs[memoX.ID], \"should include X->A relation\")\n\trequire.True(t, memoIDs[memoA.ID], \"should include A->B relation\")\n\trequire.True(t, relatedIDs[memoA.ID], \"should include X->A relation\")\n\trequire.True(t, relatedIDs[memoB.ID], \"should include A->B relation\")\n\n\tts.Close()\n}\n\nfunc TestMemoRelationMultipleRelationsToSameMemo(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\tuser, err := createTestingHostUser(ctx, ts)\n\trequire.NoError(t, err)\n\n\tmainMemo, err := ts.CreateMemo(ctx, &store.Memo{\n\t\tUID:        \"main-memo\",\n\t\tCreatorID:  user.ID,\n\t\tContent:    \"main memo content\",\n\t\tVisibility: store.Public,\n\t})\n\trequire.NoError(t, err)\n\n\t// Create multiple memos that all relate to the main memo\n\tfor i := 1; i <= 5; i++ {\n\t\trelatedMemo, err := ts.CreateMemo(ctx, &store.Memo{\n\t\t\tUID:        \"related-memo-\" + string(rune('0'+i)),\n\t\t\tCreatorID:  user.ID,\n\t\t\tContent:    \"related memo content\",\n\t\t\tVisibility: store.Public,\n\t\t})\n\t\trequire.NoError(t, err)\n\n\t\t_, err = ts.UpsertMemoRelation(ctx, &store.MemoRelation{\n\t\t\tMemoID:        mainMemo.ID,\n\t\t\tRelatedMemoID: relatedMemo.ID,\n\t\t\tType:          store.MemoRelationReference,\n\t\t})\n\t\trequire.NoError(t, err)\n\t}\n\n\t// Verify all 5 relations exist\n\trelations, err := ts.ListMemoRelations(ctx, &store.FindMemoRelation{\n\t\tMemoID: &mainMemo.ID,\n\t})\n\trequire.NoError(t, err)\n\trequire.Len(t, relations, 5)\n\n\tts.Close()\n}\n"
  },
  {
    "path": "store/test/memo_test.go",
    "content": "package test\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/usememos/memos/store\"\n\n\tstorepb \"github.com/usememos/memos/proto/gen/store\"\n)\n\nfunc TestMemoStore(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\tuser, err := createTestingHostUser(ctx, ts)\n\trequire.NoError(t, err)\n\tmemoCreate := &store.Memo{\n\t\tUID:        \"test-resource-name\",\n\t\tCreatorID:  user.ID,\n\t\tContent:    \"test_content\",\n\t\tVisibility: store.Public,\n\t}\n\tmemo, err := ts.CreateMemo(ctx, memoCreate)\n\trequire.NoError(t, err)\n\trequire.Equal(t, memoCreate.Content, memo.Content)\n\tmemoPatchContent := \"test_content_2\"\n\tmemoPatch := &store.UpdateMemo{\n\t\tID:      memo.ID,\n\t\tContent: &memoPatchContent,\n\t}\n\terr = ts.UpdateMemo(ctx, memoPatch)\n\trequire.NoError(t, err)\n\tmemo, err = ts.GetMemo(ctx, &store.FindMemo{\n\t\tID: &memo.ID,\n\t})\n\trequire.NoError(t, err)\n\trequire.NotNil(t, memo)\n\tmemoList, err := ts.ListMemos(ctx, &store.FindMemo{\n\t\tCreatorID: &user.ID,\n\t})\n\trequire.NoError(t, err)\n\trequire.Equal(t, 1, len(memoList))\n\trequire.Equal(t, memo, memoList[0])\n\terr = ts.DeleteMemo(ctx, &store.DeleteMemo{\n\t\tID: memo.ID,\n\t})\n\trequire.NoError(t, err)\n\tmemoList, err = ts.ListMemos(ctx, &store.FindMemo{\n\t\tCreatorID: &user.ID,\n\t})\n\trequire.NoError(t, err)\n\trequire.Equal(t, 0, len(memoList))\n\n\tmemoList, err = ts.ListMemos(ctx, &store.FindMemo{\n\t\tCreatorID:      &user.ID,\n\t\tVisibilityList: []store.Visibility{store.Public},\n\t})\n\trequire.NoError(t, err)\n\trequire.Equal(t, 0, len(memoList))\n\tts.Close()\n}\n\nfunc TestMemoListByTags(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\tuser, err := createTestingHostUser(ctx, ts)\n\trequire.NoError(t, err)\n\tmemoCreate := &store.Memo{\n\t\tUID:        \"test-resource-name\",\n\t\tCreatorID:  user.ID,\n\t\tContent:    \"test_content\",\n\t\tVisibility: store.Public,\n\t\tPayload: &storepb.MemoPayload{\n\t\t\tTags: []string{\"test_tag\"},\n\t\t},\n\t}\n\tmemo, err := ts.CreateMemo(ctx, memoCreate)\n\trequire.NoError(t, err)\n\trequire.Equal(t, memoCreate.Content, memo.Content)\n\tmemo, err = ts.GetMemo(ctx, &store.FindMemo{\n\t\tID: &memo.ID,\n\t})\n\trequire.NoError(t, err)\n\trequire.NotNil(t, memo)\n\n\tmemoList, err := ts.ListMemos(ctx, &store.FindMemo{\n\t\tFilters: []string{\"tag in [\\\"test_tag\\\"]\"},\n\t})\n\trequire.NoError(t, err)\n\trequire.Equal(t, 1, len(memoList))\n\trequire.Equal(t, memo, memoList[0])\n\tts.Close()\n}\n\nfunc TestDeleteMemoStore(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\tuser, err := createTestingHostUser(ctx, ts)\n\trequire.NoError(t, err)\n\tmemoCreate := &store.Memo{\n\t\tUID:        \"test-resource-name\",\n\t\tCreatorID:  user.ID,\n\t\tContent:    \"test_content\",\n\t\tVisibility: store.Public,\n\t}\n\tmemo, err := ts.CreateMemo(ctx, memoCreate)\n\trequire.NoError(t, err)\n\trequire.Equal(t, memoCreate.Content, memo.Content)\n\terr = ts.DeleteMemo(ctx, &store.DeleteMemo{\n\t\tID: memo.ID,\n\t})\n\trequire.NoError(t, err)\n\tts.Close()\n}\n\nfunc TestMemoGetByID(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\tuser, err := createTestingHostUser(ctx, ts)\n\trequire.NoError(t, err)\n\n\tmemo, err := ts.CreateMemo(ctx, &store.Memo{\n\t\tUID:        \"test-memo-1\",\n\t\tCreatorID:  user.ID,\n\t\tContent:    \"test content\",\n\t\tVisibility: store.Public,\n\t})\n\trequire.NoError(t, err)\n\n\t// Get by ID\n\tfound, err := ts.GetMemo(ctx, &store.FindMemo{ID: &memo.ID})\n\trequire.NoError(t, err)\n\trequire.NotNil(t, found)\n\trequire.Equal(t, memo.ID, found.ID)\n\trequire.Equal(t, memo.Content, found.Content)\n\n\t// Get non-existent\n\tnonExistentID := int32(99999)\n\tnotFound, err := ts.GetMemo(ctx, &store.FindMemo{ID: &nonExistentID})\n\trequire.NoError(t, err)\n\trequire.Nil(t, notFound)\n\n\tts.Close()\n}\n\nfunc TestMemoGetByUID(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\tuser, err := createTestingHostUser(ctx, ts)\n\trequire.NoError(t, err)\n\n\tuid := \"unique-memo-uid\"\n\tmemo, err := ts.CreateMemo(ctx, &store.Memo{\n\t\tUID:        uid,\n\t\tCreatorID:  user.ID,\n\t\tContent:    \"test content\",\n\t\tVisibility: store.Public,\n\t})\n\trequire.NoError(t, err)\n\n\t// Get by UID\n\tfound, err := ts.GetMemo(ctx, &store.FindMemo{UID: &uid})\n\trequire.NoError(t, err)\n\trequire.NotNil(t, found)\n\trequire.Equal(t, memo.UID, found.UID)\n\n\t// Get non-existent UID\n\tnonExistentUID := \"non-existent-uid\"\n\tnotFound, err := ts.GetMemo(ctx, &store.FindMemo{UID: &nonExistentUID})\n\trequire.NoError(t, err)\n\trequire.Nil(t, notFound)\n\n\tts.Close()\n}\n\nfunc TestMemoListByVisibility(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\tuser, err := createTestingHostUser(ctx, ts)\n\trequire.NoError(t, err)\n\n\t// Create memos with different visibilities\n\t_, err = ts.CreateMemo(ctx, &store.Memo{\n\t\tUID:        \"public-memo\",\n\t\tCreatorID:  user.ID,\n\t\tContent:    \"public content\",\n\t\tVisibility: store.Public,\n\t})\n\trequire.NoError(t, err)\n\n\t_, err = ts.CreateMemo(ctx, &store.Memo{\n\t\tUID:        \"protected-memo\",\n\t\tCreatorID:  user.ID,\n\t\tContent:    \"protected content\",\n\t\tVisibility: store.Protected,\n\t})\n\trequire.NoError(t, err)\n\n\t_, err = ts.CreateMemo(ctx, &store.Memo{\n\t\tUID:        \"private-memo\",\n\t\tCreatorID:  user.ID,\n\t\tContent:    \"private content\",\n\t\tVisibility: store.Private,\n\t})\n\trequire.NoError(t, err)\n\n\t// List public memos only\n\tpublicMemos, err := ts.ListMemos(ctx, &store.FindMemo{\n\t\tVisibilityList: []store.Visibility{store.Public},\n\t})\n\trequire.NoError(t, err)\n\trequire.Equal(t, 1, len(publicMemos))\n\trequire.Equal(t, store.Public, publicMemos[0].Visibility)\n\n\t// List protected memos only\n\tprotectedMemos, err := ts.ListMemos(ctx, &store.FindMemo{\n\t\tVisibilityList: []store.Visibility{store.Protected},\n\t})\n\trequire.NoError(t, err)\n\trequire.Equal(t, 1, len(protectedMemos))\n\trequire.Equal(t, store.Protected, protectedMemos[0].Visibility)\n\n\t// List public and protected (multiple visibility)\n\tpublicAndProtected, err := ts.ListMemos(ctx, &store.FindMemo{\n\t\tVisibilityList: []store.Visibility{store.Public, store.Protected},\n\t})\n\trequire.NoError(t, err)\n\trequire.Equal(t, 2, len(publicAndProtected))\n\n\t// List all\n\tallMemos, err := ts.ListMemos(ctx, &store.FindMemo{})\n\trequire.NoError(t, err)\n\trequire.Equal(t, 3, len(allMemos))\n\n\tts.Close()\n}\n\nfunc TestMemoListWithPagination(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\tuser, err := createTestingHostUser(ctx, ts)\n\trequire.NoError(t, err)\n\n\t// Create 10 memos\n\tfor i := 0; i < 10; i++ {\n\t\t_, err := ts.CreateMemo(ctx, &store.Memo{\n\t\t\tUID:        fmt.Sprintf(\"memo-%d\", i),\n\t\t\tCreatorID:  user.ID,\n\t\t\tContent:    fmt.Sprintf(\"content %d\", i),\n\t\t\tVisibility: store.Public,\n\t\t})\n\t\trequire.NoError(t, err)\n\t}\n\n\t// Test limit\n\tlimit := 5\n\tlimitedMemos, err := ts.ListMemos(ctx, &store.FindMemo{Limit: &limit})\n\trequire.NoError(t, err)\n\trequire.Equal(t, 5, len(limitedMemos))\n\n\t// Test offset\n\toffset := 3\n\toffsetMemos, err := ts.ListMemos(ctx, &store.FindMemo{Limit: &limit, Offset: &offset})\n\trequire.NoError(t, err)\n\trequire.Equal(t, 5, len(offsetMemos))\n\n\t// Verify offset works correctly (different memos)\n\trequire.NotEqual(t, limitedMemos[0].ID, offsetMemos[0].ID)\n\n\tts.Close()\n}\n\nfunc TestMemoUpdatePinned(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\tuser, err := createTestingHostUser(ctx, ts)\n\trequire.NoError(t, err)\n\n\tmemo, err := ts.CreateMemo(ctx, &store.Memo{\n\t\tUID:        \"pinnable-memo\",\n\t\tCreatorID:  user.ID,\n\t\tContent:    \"content\",\n\t\tVisibility: store.Public,\n\t})\n\trequire.NoError(t, err)\n\trequire.False(t, memo.Pinned)\n\n\t// Pin the memo\n\tpinned := true\n\terr = ts.UpdateMemo(ctx, &store.UpdateMemo{\n\t\tID:     memo.ID,\n\t\tPinned: &pinned,\n\t})\n\trequire.NoError(t, err)\n\n\t// Verify pinned\n\tfound, err := ts.GetMemo(ctx, &store.FindMemo{ID: &memo.ID})\n\trequire.NoError(t, err)\n\trequire.True(t, found.Pinned)\n\n\t// Unpin\n\tunpinned := false\n\terr = ts.UpdateMemo(ctx, &store.UpdateMemo{\n\t\tID:     memo.ID,\n\t\tPinned: &unpinned,\n\t})\n\trequire.NoError(t, err)\n\n\tfound, err = ts.GetMemo(ctx, &store.FindMemo{ID: &memo.ID})\n\trequire.NoError(t, err)\n\trequire.False(t, found.Pinned)\n\n\tts.Close()\n}\n\nfunc TestMemoUpdateVisibility(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\tuser, err := createTestingHostUser(ctx, ts)\n\trequire.NoError(t, err)\n\n\tmemo, err := ts.CreateMemo(ctx, &store.Memo{\n\t\tUID:        \"visibility-memo\",\n\t\tCreatorID:  user.ID,\n\t\tContent:    \"content\",\n\t\tVisibility: store.Public,\n\t})\n\trequire.NoError(t, err)\n\trequire.Equal(t, store.Public, memo.Visibility)\n\n\t// Change to private\n\tprivateVisibility := store.Private\n\terr = ts.UpdateMemo(ctx, &store.UpdateMemo{\n\t\tID:         memo.ID,\n\t\tVisibility: &privateVisibility,\n\t})\n\trequire.NoError(t, err)\n\n\tfound, err := ts.GetMemo(ctx, &store.FindMemo{ID: &memo.ID})\n\trequire.NoError(t, err)\n\trequire.Equal(t, store.Private, found.Visibility)\n\n\t// Change to protected\n\tprotectedVisibility := store.Protected\n\terr = ts.UpdateMemo(ctx, &store.UpdateMemo{\n\t\tID:         memo.ID,\n\t\tVisibility: &protectedVisibility,\n\t})\n\trequire.NoError(t, err)\n\n\tfound, err = ts.GetMemo(ctx, &store.FindMemo{ID: &memo.ID})\n\trequire.NoError(t, err)\n\trequire.Equal(t, store.Protected, found.Visibility)\n\n\tts.Close()\n}\n\nfunc TestMemoInvalidUID(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\tuser, err := createTestingHostUser(ctx, ts)\n\trequire.NoError(t, err)\n\n\t// Create memo with invalid UID (contains special characters)\n\t_, err = ts.CreateMemo(ctx, &store.Memo{\n\t\tUID:        \"invalid uid with spaces\",\n\t\tCreatorID:  user.ID,\n\t\tContent:    \"content\",\n\t\tVisibility: store.Public,\n\t})\n\trequire.Error(t, err)\n\trequire.Contains(t, err.Error(), \"invalid uid\")\n\n\tts.Close()\n}\n\nfunc TestMemoCreateWithCustomTimestamps(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\tuser, err := createTestingHostUser(ctx, ts)\n\trequire.NoError(t, err)\n\n\tcustomCreatedTs := int64(1700000000) // 2023-11-14 22:13:20 UTC\n\tcustomUpdatedTs := int64(1700000001)\n\n\tmemo, err := ts.CreateMemo(ctx, &store.Memo{\n\t\tUID:        \"custom-timestamp-memo\",\n\t\tCreatorID:  user.ID,\n\t\tContent:    \"content with custom timestamps\",\n\t\tVisibility: store.Public,\n\t\tCreatedTs:  customCreatedTs,\n\t\tUpdatedTs:  customUpdatedTs,\n\t})\n\trequire.NoError(t, err)\n\trequire.Equal(t, customCreatedTs, memo.CreatedTs)\n\trequire.Equal(t, customUpdatedTs, memo.UpdatedTs)\n\n\t// Fetch and verify timestamps are preserved\n\tfound, err := ts.GetMemo(ctx, &store.FindMemo{ID: &memo.ID})\n\trequire.NoError(t, err)\n\trequire.NotNil(t, found)\n\trequire.Equal(t, customCreatedTs, found.CreatedTs)\n\trequire.Equal(t, customUpdatedTs, found.UpdatedTs)\n\n\tts.Close()\n}\n\nfunc TestMemoCreateWithOnlyCreatedTs(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\tuser, err := createTestingHostUser(ctx, ts)\n\trequire.NoError(t, err)\n\n\tcustomCreatedTs := int64(1609459200) // 2021-01-01 00:00:00 UTC\n\n\tmemo, err := ts.CreateMemo(ctx, &store.Memo{\n\t\tUID:        \"custom-created-ts-only\",\n\t\tCreatorID:  user.ID,\n\t\tContent:    \"content with custom created_ts only\",\n\t\tVisibility: store.Public,\n\t\tCreatedTs:  customCreatedTs,\n\t})\n\trequire.NoError(t, err)\n\trequire.Equal(t, customCreatedTs, memo.CreatedTs)\n\n\tfound, err := ts.GetMemo(ctx, &store.FindMemo{ID: &memo.ID})\n\trequire.NoError(t, err)\n\trequire.NotNil(t, found)\n\trequire.Equal(t, customCreatedTs, found.CreatedTs)\n\n\tts.Close()\n}\n\nfunc TestMemoWithPayload(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\tuser, err := createTestingHostUser(ctx, ts)\n\trequire.NoError(t, err)\n\n\t// Create memo with tags in payload\n\ttags := []string{\"tag1\", \"tag2\", \"tag3\"}\n\tmemo, err := ts.CreateMemo(ctx, &store.Memo{\n\t\tUID:        \"memo-with-payload\",\n\t\tCreatorID:  user.ID,\n\t\tContent:    \"content with tags\",\n\t\tVisibility: store.Public,\n\t\tPayload: &storepb.MemoPayload{\n\t\t\tTags: tags,\n\t\t},\n\t})\n\trequire.NoError(t, err)\n\trequire.NotNil(t, memo.Payload)\n\trequire.Equal(t, tags, memo.Payload.Tags)\n\n\t// Fetch and verify\n\tfound, err := ts.GetMemo(ctx, &store.FindMemo{ID: &memo.ID})\n\trequire.NoError(t, err)\n\trequire.NotNil(t, found.Payload)\n\trequire.Equal(t, tags, found.Payload.Tags)\n\n\tts.Close()\n}\n"
  },
  {
    "path": "store/test/migrator_test.go",
    "content": "package test\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/usememos/memos/store\"\n)\n\n// TestFreshInstall verifies that LATEST.sql applies correctly on a fresh database.\n// This is essentially what NewTestingStore already does, but we make it explicit.\nfunc TestFreshInstall(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\n\t// NewTestingStore creates a fresh database and runs Migrate()\n\t// which applies LATEST.sql for uninitialized databases\n\tts := NewTestingStore(ctx, t)\n\n\t// Verify migration completed successfully\n\tcurrentSchemaVersion, err := ts.GetCurrentSchemaVersion()\n\trequire.NoError(t, err)\n\trequire.NotEmpty(t, currentSchemaVersion, \"schema version should be set after fresh install\")\n\n\t// Verify we can read instance settings (basic sanity check)\n\tinstanceSetting, err := ts.GetInstanceBasicSetting(ctx)\n\trequire.NoError(t, err)\n\trequire.Equal(t, currentSchemaVersion, instanceSetting.SchemaVersion)\n}\n\n// TestMigrationReRun verifies that re-running the migration on an already\n// migrated database does not fail or cause issues. This simulates a\n// scenario where the server is restarted.\nfunc TestMigrationReRun(t *testing.T) {\n\tt.Parallel()\n\n\tctx := context.Background()\n\t// Use the shared testing store which already runs migrations on init\n\tts := NewTestingStore(ctx, t)\n\n\t// Get current version\n\tinitialVersion, err := ts.GetCurrentSchemaVersion()\n\trequire.NoError(t, err)\n\n\t// Manually trigger migration again\n\terr = ts.Migrate(ctx)\n\trequire.NoError(t, err, \"re-running migration should not fail\")\n\n\t// Verify version hasn't changed (or at least is valid)\n\tfinalVersion, err := ts.GetCurrentSchemaVersion()\n\trequire.NoError(t, err)\n\trequire.Equal(t, initialVersion, finalVersion, \"version should match after re-run\")\n}\n\n// TestMigrationWithData verifies that migration preserves data integrity.\n// Creates data, then re-runs migration and verifies data is still accessible.\nfunc TestMigrationWithData(t *testing.T) {\n\tt.Parallel()\n\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\n\t// Create a user and memo before re-running migration\n\tuser, err := createTestingHostUser(ctx, ts)\n\trequire.NoError(t, err, \"should create user\")\n\n\toriginalMemo, err := ts.CreateMemo(ctx, &store.Memo{\n\t\tUID:        \"migration-data-test\",\n\t\tCreatorID:  user.ID,\n\t\tContent:    \"Data before migration re-run\",\n\t\tVisibility: store.Public,\n\t})\n\trequire.NoError(t, err, \"should create memo\")\n\n\t// Re-run migration\n\terr = ts.Migrate(ctx)\n\trequire.NoError(t, err, \"re-running migration should not fail\")\n\n\t// Verify data is still accessible\n\tmemo, err := ts.GetMemo(ctx, &store.FindMemo{UID: &originalMemo.UID})\n\trequire.NoError(t, err, \"should retrieve memo after migration\")\n\trequire.Equal(t, \"Data before migration re-run\", memo.Content, \"memo content should be preserved\")\n}\n\n// TestMigrationMultipleReRuns verifies that migration is idempotent\n// even when run multiple times in succession.\nfunc TestMigrationMultipleReRuns(t *testing.T) {\n\tt.Parallel()\n\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\n\t// Get initial version\n\tinitialVersion, err := ts.GetCurrentSchemaVersion()\n\trequire.NoError(t, err)\n\n\t// Run migration multiple times\n\tfor i := 0; i < 3; i++ {\n\t\terr = ts.Migrate(ctx)\n\t\trequire.NoError(t, err, \"migration run %d should not fail\", i+1)\n\t}\n\n\t// Verify version is still correct\n\tfinalVersion, err := ts.GetCurrentSchemaVersion()\n\trequire.NoError(t, err)\n\trequire.Equal(t, initialVersion, finalVersion, \"version should remain unchanged after multiple re-runs\")\n}\n\n// TestMigrationFromStableVersion verifies that upgrading from a stable Memos version\n// to the current version works correctly. This is the critical upgrade path test.\n//\n// Test flow:\n// 1. Start a stable Memos container to create a database with the old schema\n// 2. Stop the container and wait for cleanup\n// 3. Use the store directly to run migration with current code\n// 4. Verify the migration succeeded and data can be written\n//\n// Note: This test is skipped when running with -race flag because testcontainers\n// has known race conditions in its reaper code that are outside our control.\nfunc TestMigrationFromStableVersion(t *testing.T) {\n\t// Skip for non-SQLite drivers (simplifies the test)\n\tif getDriverFromEnv() != \"sqlite\" {\n\t\tt.Skip(\"skipping upgrade test for non-sqlite driver\")\n\t}\n\n\t// Skip if explicitly disabled (e.g., in environments without Docker)\n\tif os.Getenv(\"SKIP_CONTAINER_TESTS\") == \"1\" {\n\t\tt.Skip(\"skipping container-based test (SKIP_CONTAINER_TESTS=1)\")\n\t}\n\n\tctx := context.Background()\n\tdataDir := t.TempDir()\n\n\t// 1. Start stable Memos container to create database with old schema\n\tcfg := MemosContainerConfig{\n\t\tDriver:  \"sqlite\",\n\t\tDataDir: dataDir,\n\t\tVersion: StableMemosVersion,\n\t}\n\n\tt.Logf(\"Starting Memos %s container to create old-schema database...\", cfg.Version)\n\tcontainer, err := StartMemosContainer(ctx, cfg)\n\trequire.NoError(t, err, \"failed to start stable memos container\")\n\n\t// Wait for the container to fully initialize the database\n\ttime.Sleep(10 * time.Second)\n\n\t// Stop the container gracefully\n\tt.Log(\"Stopping stable Memos container...\")\n\terr = container.Terminate(ctx)\n\trequire.NoError(t, err, \"failed to stop memos container\")\n\n\t// Wait for file handles to be released\n\ttime.Sleep(2 * time.Second)\n\n\t// 2. Connect to the database directly and run migration with current code\n\tdsn := fmt.Sprintf(\"%s/memos_prod.db\", dataDir)\n\tt.Logf(\"Connecting to database at %s...\", dsn)\n\n\tts := NewTestingStoreWithDSN(ctx, t, \"sqlite\", dsn)\n\n\t// Get the schema version before migration\n\toldSetting, err := ts.GetInstanceBasicSetting(ctx)\n\trequire.NoError(t, err)\n\tt.Logf(\"Old schema version: %s\", oldSetting.SchemaVersion)\n\n\t// 3. Run migration with current code\n\tt.Log(\"Running migration with current code...\")\n\terr = ts.Migrate(ctx)\n\trequire.NoError(t, err, \"migration from stable version should succeed\")\n\n\t// 4. Verify migration succeeded\n\tnewVersion, err := ts.GetCurrentSchemaVersion()\n\trequire.NoError(t, err)\n\tt.Logf(\"New schema version: %s\", newVersion)\n\n\tnewSetting, err := ts.GetInstanceBasicSetting(ctx)\n\trequire.NoError(t, err)\n\trequire.Equal(t, newVersion, newSetting.SchemaVersion, \"schema version should be updated\")\n\n\t// Verify we can write data to the migrated database\n\tuser, err := createTestingHostUser(ctx, ts)\n\trequire.NoError(t, err, \"should create user after migration\")\n\n\tmemo, err := ts.CreateMemo(ctx, &store.Memo{\n\t\tUID:        \"post-upgrade-memo\",\n\t\tCreatorID:  user.ID,\n\t\tContent:    \"Content after upgrade from stable\",\n\t\tVisibility: store.Public,\n\t})\n\trequire.NoError(t, err, \"should create memo after migration\")\n\trequire.Equal(t, \"Content after upgrade from stable\", memo.Content)\n\n\tt.Logf(\"Migration successful: %s -> %s\", oldSetting.SchemaVersion, newVersion)\n}\n"
  },
  {
    "path": "store/test/reaction_test.go",
    "content": "package test\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/usememos/memos/store\"\n)\n\nfunc TestReactionStore(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\n\tuser, err := createTestingHostUser(ctx, ts)\n\trequire.NoError(t, err)\n\n\tcontentID := \"test_content_id\"\n\treaction, err := ts.UpsertReaction(ctx, &store.Reaction{\n\t\tCreatorID:    user.ID,\n\t\tContentID:    contentID,\n\t\tReactionType: \"💗\",\n\t})\n\trequire.NoError(t, err)\n\trequire.NotNil(t, reaction)\n\trequire.NotEmpty(t, reaction.ID)\n\n\treactions, err := ts.ListReactions(ctx, &store.FindReaction{\n\t\tContentID: &contentID,\n\t})\n\trequire.NoError(t, err)\n\trequire.Len(t, reactions, 1)\n\trequire.Equal(t, reaction, reactions[0])\n\n\t// Test GetReaction.\n\tgotReaction, err := ts.GetReaction(ctx, &store.FindReaction{\n\t\tID: &reaction.ID,\n\t})\n\trequire.NoError(t, err)\n\trequire.NotNil(t, gotReaction)\n\trequire.Equal(t, reaction.ID, gotReaction.ID)\n\trequire.Equal(t, reaction.CreatorID, gotReaction.CreatorID)\n\trequire.Equal(t, reaction.ContentID, gotReaction.ContentID)\n\trequire.Equal(t, reaction.ReactionType, gotReaction.ReactionType)\n\n\t// Test GetReaction with non-existent ID.\n\tnonExistentID := int32(99999)\n\tnotFoundReaction, err := ts.GetReaction(ctx, &store.FindReaction{\n\t\tID: &nonExistentID,\n\t})\n\trequire.NoError(t, err)\n\trequire.Nil(t, notFoundReaction)\n\n\terr = ts.DeleteReaction(ctx, &store.DeleteReaction{\n\t\tID: reaction.ID,\n\t})\n\trequire.NoError(t, err)\n\n\treactions, err = ts.ListReactions(ctx, &store.FindReaction{\n\t\tContentID: &contentID,\n\t})\n\trequire.NoError(t, err)\n\trequire.Len(t, reactions, 0)\n\n\tts.Close()\n}\n\nfunc TestReactionListByCreatorID(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\n\tuser1, err := createTestingHostUser(ctx, ts)\n\trequire.NoError(t, err)\n\n\tuser2, err := createTestingUserWithRole(ctx, ts, \"user2\", store.RoleUser)\n\trequire.NoError(t, err)\n\n\tcontentID := \"shared_content\"\n\n\t// User 1 creates reaction\n\t_, err = ts.UpsertReaction(ctx, &store.Reaction{\n\t\tCreatorID:    user1.ID,\n\t\tContentID:    contentID,\n\t\tReactionType: \"👍\",\n\t})\n\trequire.NoError(t, err)\n\n\t// User 2 creates reaction\n\t_, err = ts.UpsertReaction(ctx, &store.Reaction{\n\t\tCreatorID:    user2.ID,\n\t\tContentID:    contentID,\n\t\tReactionType: \"❤️\",\n\t})\n\trequire.NoError(t, err)\n\n\t// List all reactions for content\n\treactions, err := ts.ListReactions(ctx, &store.FindReaction{\n\t\tContentID: &contentID,\n\t})\n\trequire.NoError(t, err)\n\trequire.Len(t, reactions, 2)\n\n\t// List by creator ID\n\tuser1Reactions, err := ts.ListReactions(ctx, &store.FindReaction{\n\t\tCreatorID: &user1.ID,\n\t})\n\trequire.NoError(t, err)\n\trequire.Len(t, user1Reactions, 1)\n\trequire.Equal(t, \"👍\", user1Reactions[0].ReactionType)\n\n\tts.Close()\n}\n\nfunc TestReactionMultipleContentIDs(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\n\tuser, err := createTestingHostUser(ctx, ts)\n\trequire.NoError(t, err)\n\n\tcontentID1 := \"content_1\"\n\tcontentID2 := \"content_2\"\n\n\t// Create reactions for different contents\n\t_, err = ts.UpsertReaction(ctx, &store.Reaction{\n\t\tCreatorID:    user.ID,\n\t\tContentID:    contentID1,\n\t\tReactionType: \"👍\",\n\t})\n\trequire.NoError(t, err)\n\n\t_, err = ts.UpsertReaction(ctx, &store.Reaction{\n\t\tCreatorID:    user.ID,\n\t\tContentID:    contentID2,\n\t\tReactionType: \"❤️\",\n\t})\n\trequire.NoError(t, err)\n\n\t// List by content ID list\n\treactions, err := ts.ListReactions(ctx, &store.FindReaction{\n\t\tContentIDList: []string{contentID1, contentID2},\n\t})\n\trequire.NoError(t, err)\n\trequire.Len(t, reactions, 2)\n\n\tts.Close()\n}\n\nfunc TestReactionUpsertDifferentTypes(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\n\tuser, err := createTestingHostUser(ctx, ts)\n\trequire.NoError(t, err)\n\n\tcontentID := \"test_content\"\n\n\t// Create first reaction\n\treaction1, err := ts.UpsertReaction(ctx, &store.Reaction{\n\t\tCreatorID:    user.ID,\n\t\tContentID:    contentID,\n\t\tReactionType: \"👍\",\n\t})\n\trequire.NoError(t, err)\n\n\t// Create second reaction with different type (should create new, not update)\n\treaction2, err := ts.UpsertReaction(ctx, &store.Reaction{\n\t\tCreatorID:    user.ID,\n\t\tContentID:    contentID,\n\t\tReactionType: \"❤️\",\n\t})\n\trequire.NoError(t, err)\n\n\t// Both reactions should exist\n\trequire.NotEqual(t, reaction1.ID, reaction2.ID)\n\n\treactions, err := ts.ListReactions(ctx, &store.FindReaction{\n\t\tContentID: &contentID,\n\t})\n\trequire.NoError(t, err)\n\trequire.Len(t, reactions, 2)\n\n\tts.Close()\n}\n"
  },
  {
    "path": "store/test/store.go",
    "content": "package test\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net\"\n\t\"os\"\n\t\"testing\"\n\n\t// sqlite driver.\n\t_ \"modernc.org/sqlite\"\n\n\t\"github.com/joho/godotenv\"\n\n\t\"github.com/usememos/memos/internal/profile\"\n\t\"github.com/usememos/memos/internal/version\"\n\t\"github.com/usememos/memos/store\"\n\t\"github.com/usememos/memos/store/db\"\n)\n\n// NewTestingStore creates a new testing store with a fresh database.\n// Each test gets its own isolated database:\n//   - SQLite: new temp file per test\n//   - MySQL/PostgreSQL: new database per test in shared container\nfunc NewTestingStore(ctx context.Context, t *testing.T) *store.Store {\n\tdriver := getDriverFromEnv()\n\tprofile := getTestingProfileForDriver(t, driver)\n\tdbDriver, err := db.NewDBDriver(profile)\n\tif err != nil {\n\t\tt.Fatalf(\"failed to create db driver: %v\", err)\n\t}\n\n\tstore := store.New(dbDriver, profile)\n\tif err := store.Migrate(ctx); err != nil {\n\t\tt.Fatalf(\"failed to migrate db: %v\", err)\n\t}\n\treturn store\n}\n\n// NewTestingStoreWithDSN creates a testing store connected to a specific DSN.\n// This is useful for testing migrations on existing data.\nfunc NewTestingStoreWithDSN(_ context.Context, t *testing.T, driver, dsn string) *store.Store {\n\tprofile := &profile.Profile{\n\t\tPort:    getUnusedPort(),\n\t\tData:    t.TempDir(), // Dummy dir, DSN matters\n\t\tDSN:     dsn,\n\t\tDriver:  driver,\n\t\tVersion: version.GetCurrentVersion(),\n\t}\n\tdbDriver, err := db.NewDBDriver(profile)\n\tif err != nil {\n\t\tt.Fatalf(\"failed to create db driver: %v\", err)\n\t}\n\n\tstore := store.New(dbDriver, profile)\n\t// Do not run Migrate() automatically, as we might be testing pre-migration state\n\t// or want to run it manually.\n\treturn store\n}\n\nfunc getUnusedPort() int {\n\t// Get a random unused port\n\tlistener, err := net.Listen(\"tcp\", \"localhost:0\")\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tdefer listener.Close()\n\n\t// Get the port number\n\tport := listener.Addr().(*net.TCPAddr).Port\n\treturn port\n}\n\n// getTestingProfileForDriver creates a testing profile for a specific driver.\nfunc getTestingProfileForDriver(t *testing.T, driver string) *profile.Profile {\n\t// Attempt to load .env file if present (optional, for local development)\n\t_ = godotenv.Load(\".env\")\n\n\t// Get a temporary directory for the test data.\n\tdir := t.TempDir()\n\tmode := \"prod\"\n\tport := getUnusedPort()\n\n\tvar dsn string\n\tswitch driver {\n\tcase \"sqlite\":\n\t\tdsn = fmt.Sprintf(\"%s/memos_%s.db\", dir, mode)\n\tcase \"mysql\":\n\t\tdsn = GetMySQLDSN(t)\n\tcase \"postgres\":\n\t\tdsn = GetPostgresDSN(t)\n\tdefault:\n\t\tt.Fatalf(\"unsupported driver: %s\", driver)\n\t}\n\n\treturn &profile.Profile{\n\t\tPort:    port,\n\t\tData:    dir,\n\t\tDSN:     dsn,\n\t\tDriver:  driver,\n\t\tVersion: version.GetCurrentVersion(),\n\t}\n}\n\nfunc getDriverFromEnv() string {\n\tdriver := os.Getenv(\"DRIVER\")\n\tif driver == \"\" {\n\t\tdriver = \"sqlite\"\n\t}\n\treturn driver\n}\n"
  },
  {
    "path": "store/test/user_setting_test.go",
    "content": "package test\n\nimport (\n\t\"context\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n\t\"google.golang.org/protobuf/types/known/timestamppb\"\n\n\tstorepb \"github.com/usememos/memos/proto/gen/store\"\n\t\"github.com/usememos/memos/store\"\n)\n\nfunc TestUserSettingStore(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\tuser, err := createTestingHostUser(ctx, ts)\n\trequire.NoError(t, err)\n\t_, err = ts.UpsertUserSetting(ctx, &storepb.UserSetting{\n\t\tUserId: user.ID,\n\t\tKey:    storepb.UserSetting_GENERAL,\n\t\tValue:  &storepb.UserSetting_General{General: &storepb.GeneralUserSetting{Locale: \"en\"}},\n\t})\n\trequire.NoError(t, err)\n\tlist, err := ts.ListUserSettings(ctx, &store.FindUserSetting{})\n\trequire.NoError(t, err)\n\trequire.Equal(t, 1, len(list))\n\tts.Close()\n}\n\nfunc TestUserSettingGetByUserID(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\tuser, err := createTestingHostUser(ctx, ts)\n\trequire.NoError(t, err)\n\n\t// Create setting\n\t_, err = ts.UpsertUserSetting(ctx, &storepb.UserSetting{\n\t\tUserId: user.ID,\n\t\tKey:    storepb.UserSetting_GENERAL,\n\t\tValue:  &storepb.UserSetting_General{General: &storepb.GeneralUserSetting{Locale: \"zh\"}},\n\t})\n\trequire.NoError(t, err)\n\n\t// Get by user ID\n\tsetting, err := ts.GetUserSetting(ctx, &store.FindUserSetting{\n\t\tUserID: &user.ID,\n\t\tKey:    storepb.UserSetting_GENERAL,\n\t})\n\trequire.NoError(t, err)\n\trequire.NotNil(t, setting)\n\trequire.Equal(t, \"zh\", setting.GetGeneral().Locale)\n\n\t// Get non-existent key\n\tnonExistentSetting, err := ts.GetUserSetting(ctx, &store.FindUserSetting{\n\t\tUserID: &user.ID,\n\t\tKey:    storepb.UserSetting_SHORTCUTS,\n\t})\n\trequire.NoError(t, err)\n\trequire.Nil(t, nonExistentSetting)\n\n\tts.Close()\n}\n\nfunc TestUserSettingUpsertUpdate(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\tuser, err := createTestingHostUser(ctx, ts)\n\trequire.NoError(t, err)\n\n\t// Create initial setting\n\t_, err = ts.UpsertUserSetting(ctx, &storepb.UserSetting{\n\t\tUserId: user.ID,\n\t\tKey:    storepb.UserSetting_GENERAL,\n\t\tValue:  &storepb.UserSetting_General{General: &storepb.GeneralUserSetting{Locale: \"en\"}},\n\t})\n\trequire.NoError(t, err)\n\n\t// Update setting\n\t_, err = ts.UpsertUserSetting(ctx, &storepb.UserSetting{\n\t\tUserId: user.ID,\n\t\tKey:    storepb.UserSetting_GENERAL,\n\t\tValue:  &storepb.UserSetting_General{General: &storepb.GeneralUserSetting{Locale: \"fr\"}},\n\t})\n\trequire.NoError(t, err)\n\n\t// Verify update\n\tsetting, err := ts.GetUserSetting(ctx, &store.FindUserSetting{\n\t\tUserID: &user.ID,\n\t\tKey:    storepb.UserSetting_GENERAL,\n\t})\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"fr\", setting.GetGeneral().Locale)\n\n\t// Verify only one setting exists\n\tlist, err := ts.ListUserSettings(ctx, &store.FindUserSetting{UserID: &user.ID})\n\trequire.NoError(t, err)\n\trequire.Equal(t, 1, len(list))\n\n\tts.Close()\n}\n\nfunc TestUserSettingRefreshTokens(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\tuser, err := createTestingHostUser(ctx, ts)\n\trequire.NoError(t, err)\n\n\t// Initially no tokens\n\ttokens, err := ts.GetUserRefreshTokens(ctx, user.ID)\n\trequire.NoError(t, err)\n\trequire.Empty(t, tokens)\n\n\t// Add a refresh token\n\ttoken1 := &storepb.RefreshTokensUserSetting_RefreshToken{\n\t\tTokenId:     \"token-1\",\n\t\tDescription: \"Chrome browser session\",\n\t}\n\terr = ts.AddUserRefreshToken(ctx, user.ID, token1)\n\trequire.NoError(t, err)\n\n\t// Verify token was added\n\ttokens, err = ts.GetUserRefreshTokens(ctx, user.ID)\n\trequire.NoError(t, err)\n\trequire.Len(t, tokens, 1)\n\trequire.Equal(t, \"token-1\", tokens[0].TokenId)\n\n\t// Add another token\n\ttoken2 := &storepb.RefreshTokensUserSetting_RefreshToken{\n\t\tTokenId:     \"token-2\",\n\t\tDescription: \"Firefox browser session\",\n\t}\n\terr = ts.AddUserRefreshToken(ctx, user.ID, token2)\n\trequire.NoError(t, err)\n\n\ttokens, err = ts.GetUserRefreshTokens(ctx, user.ID)\n\trequire.NoError(t, err)\n\trequire.Len(t, tokens, 2)\n\n\t// Get specific token by ID\n\tfoundToken, err := ts.GetUserRefreshTokenByID(ctx, user.ID, \"token-1\")\n\trequire.NoError(t, err)\n\trequire.NotNil(t, foundToken)\n\trequire.Equal(t, \"Chrome browser session\", foundToken.Description)\n\n\t// Get non-existent token\n\tnotFound, err := ts.GetUserRefreshTokenByID(ctx, user.ID, \"non-existent\")\n\trequire.NoError(t, err)\n\trequire.Nil(t, notFound)\n\n\t// Remove token\n\terr = ts.RemoveUserRefreshToken(ctx, user.ID, \"token-1\")\n\trequire.NoError(t, err)\n\n\ttokens, err = ts.GetUserRefreshTokens(ctx, user.ID)\n\trequire.NoError(t, err)\n\trequire.Len(t, tokens, 1)\n\trequire.Equal(t, \"token-2\", tokens[0].TokenId)\n\n\tts.Close()\n}\n\nfunc TestUserSettingPersonalAccessTokens(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\tuser, err := createTestingHostUser(ctx, ts)\n\trequire.NoError(t, err)\n\n\t// Initially no PATs\n\tpats, err := ts.GetUserPersonalAccessTokens(ctx, user.ID)\n\trequire.NoError(t, err)\n\trequire.Empty(t, pats)\n\n\t// Add a PAT\n\tpat1 := &storepb.PersonalAccessTokensUserSetting_PersonalAccessToken{\n\t\tTokenId:     \"pat-1\",\n\t\tTokenHash:   \"pat-hash-1\",\n\t\tDescription: \"API Token for external access\",\n\t}\n\terr = ts.AddUserPersonalAccessToken(ctx, user.ID, pat1)\n\trequire.NoError(t, err)\n\n\t// Verify PAT was added\n\tpats, err = ts.GetUserPersonalAccessTokens(ctx, user.ID)\n\trequire.NoError(t, err)\n\trequire.Len(t, pats, 1)\n\trequire.Equal(t, \"API Token for external access\", pats[0].Description)\n\n\t// Add another PAT\n\tpat2 := &storepb.PersonalAccessTokensUserSetting_PersonalAccessToken{\n\t\tTokenId:     \"pat-2\",\n\t\tTokenHash:   \"pat-hash-2\",\n\t\tDescription: \"CI Token\",\n\t}\n\terr = ts.AddUserPersonalAccessToken(ctx, user.ID, pat2)\n\trequire.NoError(t, err)\n\n\tpats, err = ts.GetUserPersonalAccessTokens(ctx, user.ID)\n\trequire.NoError(t, err)\n\trequire.Len(t, pats, 2)\n\n\t// Remove PAT\n\terr = ts.RemoveUserPersonalAccessToken(ctx, user.ID, \"pat-1\")\n\trequire.NoError(t, err)\n\n\tpats, err = ts.GetUserPersonalAccessTokens(ctx, user.ID)\n\trequire.NoError(t, err)\n\trequire.Len(t, pats, 1)\n\trequire.Equal(t, \"pat-2\", pats[0].TokenId)\n\n\tts.Close()\n}\n\nfunc TestUserSettingWebhooks(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\tuser, err := createTestingHostUser(ctx, ts)\n\trequire.NoError(t, err)\n\n\t// Initially no webhooks\n\twebhooks, err := ts.GetUserWebhooks(ctx, user.ID)\n\trequire.NoError(t, err)\n\trequire.Empty(t, webhooks)\n\n\t// Add a webhook\n\twebhook1 := &storepb.WebhooksUserSetting_Webhook{\n\t\tId:    \"webhook-1\",\n\t\tTitle: \"Deploy Hook\",\n\t\tUrl:   \"https://example.com/webhook\",\n\t}\n\terr = ts.AddUserWebhook(ctx, user.ID, webhook1)\n\trequire.NoError(t, err)\n\n\t// Verify webhook was added\n\twebhooks, err = ts.GetUserWebhooks(ctx, user.ID)\n\trequire.NoError(t, err)\n\trequire.Len(t, webhooks, 1)\n\trequire.Equal(t, \"Deploy Hook\", webhooks[0].Title)\n\n\t// Update webhook\n\twebhook1Updated := &storepb.WebhooksUserSetting_Webhook{\n\t\tId:    \"webhook-1\",\n\t\tTitle: \"Updated Deploy Hook\",\n\t\tUrl:   \"https://example.com/webhook/v2\",\n\t}\n\terr = ts.UpdateUserWebhook(ctx, user.ID, webhook1Updated)\n\trequire.NoError(t, err)\n\n\twebhooks, err = ts.GetUserWebhooks(ctx, user.ID)\n\trequire.NoError(t, err)\n\trequire.Len(t, webhooks, 1)\n\trequire.Equal(t, \"Updated Deploy Hook\", webhooks[0].Title)\n\trequire.Equal(t, \"https://example.com/webhook/v2\", webhooks[0].Url)\n\n\t// Add another webhook\n\twebhook2 := &storepb.WebhooksUserSetting_Webhook{\n\t\tId:    \"webhook-2\",\n\t\tTitle: \"Notification Hook\",\n\t\tUrl:   \"https://slack.example.com/webhook\",\n\t}\n\terr = ts.AddUserWebhook(ctx, user.ID, webhook2)\n\trequire.NoError(t, err)\n\n\twebhooks, err = ts.GetUserWebhooks(ctx, user.ID)\n\trequire.NoError(t, err)\n\trequire.Len(t, webhooks, 2)\n\n\t// Remove webhook\n\terr = ts.RemoveUserWebhook(ctx, user.ID, \"webhook-1\")\n\trequire.NoError(t, err)\n\n\twebhooks, err = ts.GetUserWebhooks(ctx, user.ID)\n\trequire.NoError(t, err)\n\trequire.Len(t, webhooks, 1)\n\trequire.Equal(t, \"webhook-2\", webhooks[0].Id)\n\n\tts.Close()\n}\n\nfunc TestUserSettingShortcuts(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\tuser, err := createTestingHostUser(ctx, ts)\n\trequire.NoError(t, err)\n\n\t// Create shortcuts setting\n\tshortcuts := &storepb.ShortcutsUserSetting{\n\t\tShortcuts: []*storepb.ShortcutsUserSetting_Shortcut{\n\t\t\t{Id: \"shortcut-1\", Title: \"Work Notes\", Filter: \"tag:work\"},\n\t\t\t{Id: \"shortcut-2\", Title: \"Personal\", Filter: \"tag:personal\"},\n\t\t},\n\t}\n\t_, err = ts.UpsertUserSetting(ctx, &storepb.UserSetting{\n\t\tUserId: user.ID,\n\t\tKey:    storepb.UserSetting_SHORTCUTS,\n\t\tValue:  &storepb.UserSetting_Shortcuts{Shortcuts: shortcuts},\n\t})\n\trequire.NoError(t, err)\n\n\t// Retrieve and verify\n\tsetting, err := ts.GetUserSetting(ctx, &store.FindUserSetting{\n\t\tUserID: &user.ID,\n\t\tKey:    storepb.UserSetting_SHORTCUTS,\n\t})\n\trequire.NoError(t, err)\n\trequire.NotNil(t, setting)\n\trequire.Len(t, setting.GetShortcuts().Shortcuts, 2)\n\trequire.Equal(t, \"Work Notes\", setting.GetShortcuts().Shortcuts[0].Title)\n\n\tts.Close()\n}\n\nfunc TestUserSettingGetUserByPATHash(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\tuser, err := createTestingHostUser(ctx, ts)\n\trequire.NoError(t, err)\n\n\t// Create a PAT with a known hash\n\tpatHash := \"test-pat-hash-12345\"\n\tpat := &storepb.PersonalAccessTokensUserSetting_PersonalAccessToken{\n\t\tTokenId:     \"pat-test-1\",\n\t\tTokenHash:   patHash,\n\t\tDescription: \"Test PAT for lookup\",\n\t}\n\terr = ts.AddUserPersonalAccessToken(ctx, user.ID, pat)\n\trequire.NoError(t, err)\n\n\t// Lookup user by PAT hash\n\tresult, err := ts.GetUserByPATHash(ctx, patHash)\n\trequire.NoError(t, err)\n\trequire.NotNil(t, result)\n\trequire.Equal(t, user.ID, result.UserID)\n\trequire.NotNil(t, result.User)\n\trequire.Equal(t, user.Username, result.User.Username)\n\trequire.NotNil(t, result.PAT)\n\trequire.Equal(t, \"pat-test-1\", result.PAT.TokenId)\n\trequire.Equal(t, \"Test PAT for lookup\", result.PAT.Description)\n\n\tts.Close()\n}\n\nfunc TestUserSettingGetUserByPATHashNotFound(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\t_, err := createTestingHostUser(ctx, ts)\n\trequire.NoError(t, err)\n\n\t// Lookup non-existent PAT hash\n\tresult, err := ts.GetUserByPATHash(ctx, \"non-existent-hash\")\n\trequire.Error(t, err)\n\trequire.Nil(t, result)\n\n\tts.Close()\n}\n\nfunc TestUserSettingGetUserByPATHashNoTokensKey(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\tuser, err := createTestingHostUser(ctx, ts)\n\trequire.NoError(t, err)\n\n\t// User exists but has no PERSONAL_ACCESS_TOKENS key at all\n\t// This simulates fresh users or users upgraded from v0.25.3\n\tresult, err := ts.GetUserByPATHash(ctx, \"any-hash\")\n\trequire.Error(t, err)\n\trequire.Nil(t, result)\n\t// Error could be \"PAT not found\" (Postgres) or \"sql: no rows in result set\" (SQLite/MySQL)\n\trequire.True(t,\n\t\tstrings.Contains(err.Error(), \"PAT not found\") || strings.Contains(err.Error(), \"no rows\"),\n\t\t\"expected PAT not found or no rows error, got: %v\", err)\n\n\t// Now add a PAT for the user\n\tpat := &storepb.PersonalAccessTokensUserSetting_PersonalAccessToken{\n\t\tTokenId:     \"pat-new\",\n\t\tTokenHash:   \"hash-new\",\n\t\tDescription: \"New PAT\",\n\t}\n\terr = ts.AddUserPersonalAccessToken(ctx, user.ID, pat)\n\trequire.NoError(t, err)\n\n\t// Now the lookup should succeed\n\tresult, err = ts.GetUserByPATHash(ctx, \"hash-new\")\n\trequire.NoError(t, err)\n\trequire.NotNil(t, result)\n\trequire.Equal(t, user.ID, result.UserID)\n\n\tts.Close()\n}\n\nfunc TestUserSettingGetUserByPATHashEmptyTokensArray(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\tuser, err := createTestingHostUser(ctx, ts)\n\trequire.NoError(t, err)\n\n\t// Add a PAT setting with empty tokens array\n\t_, err = ts.UpsertUserSetting(ctx, &storepb.UserSetting{\n\t\tUserId: user.ID,\n\t\tKey:    storepb.UserSetting_PERSONAL_ACCESS_TOKENS,\n\t\tValue: &storepb.UserSetting_PersonalAccessTokens{\n\t\t\tPersonalAccessTokens: &storepb.PersonalAccessTokensUserSetting{\n\t\t\t\tTokens: []*storepb.PersonalAccessTokensUserSetting_PersonalAccessToken{},\n\t\t\t},\n\t\t},\n\t})\n\trequire.NoError(t, err)\n\n\t// Lookup should fail gracefully, not crash\n\tresult, err := ts.GetUserByPATHash(ctx, \"any-hash\")\n\trequire.Error(t, err)\n\trequire.Nil(t, result)\n\t// Error could be \"PAT not found\" (Postgres) or \"sql: no rows in result set\" (SQLite/MySQL)\n\trequire.True(t,\n\t\tstrings.Contains(err.Error(), \"PAT not found\") || strings.Contains(err.Error(), \"no rows\"),\n\t\t\"expected PAT not found or no rows error, got: %v\", err)\n\n\tts.Close()\n}\n\nfunc TestUserSettingGetUserByPATHashWithOtherUsers(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\n\t// Create multiple users - some with PATs, some without\n\tuser1, err := createTestingHostUser(ctx, ts)\n\trequire.NoError(t, err)\n\n\t_, err = createTestingUserWithRole(ctx, ts, \"user2\", store.RoleUser)\n\trequire.NoError(t, err)\n\n\tuser3, err := createTestingUserWithRole(ctx, ts, \"user3\", store.RoleUser)\n\trequire.NoError(t, err)\n\n\t// User1: Has PAT\n\tpat1Hash := \"user1-pat-hash-unique\"\n\terr = ts.AddUserPersonalAccessToken(ctx, user1.ID, &storepb.PersonalAccessTokensUserSetting_PersonalAccessToken{\n\t\tTokenId:     \"pat-user1\",\n\t\tTokenHash:   pat1Hash,\n\t\tDescription: \"User 1 PAT\",\n\t})\n\trequire.NoError(t, err)\n\n\t// User2: Has no PERSONAL_ACCESS_TOKENS key (fresh user)\n\t// User3: Has empty tokens array\n\t_, err = ts.UpsertUserSetting(ctx, &storepb.UserSetting{\n\t\tUserId: user3.ID,\n\t\tKey:    storepb.UserSetting_PERSONAL_ACCESS_TOKENS,\n\t\tValue: &storepb.UserSetting_PersonalAccessTokens{\n\t\t\tPersonalAccessTokens: &storepb.PersonalAccessTokensUserSetting{\n\t\t\t\tTokens: []*storepb.PersonalAccessTokensUserSetting_PersonalAccessToken{},\n\t\t\t},\n\t\t},\n\t})\n\trequire.NoError(t, err)\n\n\t// Should find user1's PAT despite user2 having no key and user3 having empty array\n\tresult, err := ts.GetUserByPATHash(ctx, pat1Hash)\n\trequire.NoError(t, err)\n\trequire.NotNil(t, result)\n\trequire.Equal(t, user1.ID, result.UserID)\n\trequire.Equal(t, \"pat-user1\", result.PAT.TokenId)\n\n\t// Should not find non-existent hash even with mixed user states\n\tresult, err = ts.GetUserByPATHash(ctx, \"non-existent\")\n\trequire.Error(t, err)\n\trequire.Nil(t, result)\n\n\tts.Close()\n}\n\nfunc TestUserSettingGetUserByPATHashMultipleUsers(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\tuser1, err := createTestingHostUser(ctx, ts)\n\trequire.NoError(t, err)\n\tuser2, err := createTestingUserWithRole(ctx, ts, \"user2\", store.RoleUser)\n\trequire.NoError(t, err)\n\n\t// Create PATs for both users\n\tpat1Hash := \"user1-pat-hash\"\n\terr = ts.AddUserPersonalAccessToken(ctx, user1.ID, &storepb.PersonalAccessTokensUserSetting_PersonalAccessToken{\n\t\tTokenId:     \"pat-user1\",\n\t\tTokenHash:   pat1Hash,\n\t\tDescription: \"User 1 PAT\",\n\t})\n\trequire.NoError(t, err)\n\n\tpat2Hash := \"user2-pat-hash\"\n\terr = ts.AddUserPersonalAccessToken(ctx, user2.ID, &storepb.PersonalAccessTokensUserSetting_PersonalAccessToken{\n\t\tTokenId:     \"pat-user2\",\n\t\tTokenHash:   pat2Hash,\n\t\tDescription: \"User 2 PAT\",\n\t})\n\trequire.NoError(t, err)\n\n\t// Lookup user1's PAT\n\tresult1, err := ts.GetUserByPATHash(ctx, pat1Hash)\n\trequire.NoError(t, err)\n\trequire.Equal(t, user1.ID, result1.UserID)\n\trequire.Equal(t, user1.Username, result1.User.Username)\n\n\t// Lookup user2's PAT\n\tresult2, err := ts.GetUserByPATHash(ctx, pat2Hash)\n\trequire.NoError(t, err)\n\trequire.Equal(t, user2.ID, result2.UserID)\n\trequire.Equal(t, user2.Username, result2.User.Username)\n\n\tts.Close()\n}\n\nfunc TestUserSettingGetUserByPATHashMultiplePATsSameUser(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\tuser, err := createTestingHostUser(ctx, ts)\n\trequire.NoError(t, err)\n\n\t// Create multiple PATs for the same user\n\tpat1Hash := \"first-pat-hash\"\n\terr = ts.AddUserPersonalAccessToken(ctx, user.ID, &storepb.PersonalAccessTokensUserSetting_PersonalAccessToken{\n\t\tTokenId:     \"pat-1\",\n\t\tTokenHash:   pat1Hash,\n\t\tDescription: \"First PAT\",\n\t})\n\trequire.NoError(t, err)\n\n\tpat2Hash := \"second-pat-hash\"\n\terr = ts.AddUserPersonalAccessToken(ctx, user.ID, &storepb.PersonalAccessTokensUserSetting_PersonalAccessToken{\n\t\tTokenId:     \"pat-2\",\n\t\tTokenHash:   pat2Hash,\n\t\tDescription: \"Second PAT\",\n\t})\n\trequire.NoError(t, err)\n\n\t// Both PATs should resolve to the same user\n\tresult1, err := ts.GetUserByPATHash(ctx, pat1Hash)\n\trequire.NoError(t, err)\n\trequire.Equal(t, user.ID, result1.UserID)\n\trequire.Equal(t, \"pat-1\", result1.PAT.TokenId)\n\n\tresult2, err := ts.GetUserByPATHash(ctx, pat2Hash)\n\trequire.NoError(t, err)\n\trequire.Equal(t, user.ID, result2.UserID)\n\trequire.Equal(t, \"pat-2\", result2.PAT.TokenId)\n\n\tts.Close()\n}\n\nfunc TestUserSettingUpdatePATLastUsed(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\tuser, err := createTestingHostUser(ctx, ts)\n\trequire.NoError(t, err)\n\n\t// Create a PAT\n\tpatHash := \"pat-hash-for-update\"\n\terr = ts.AddUserPersonalAccessToken(ctx, user.ID, &storepb.PersonalAccessTokensUserSetting_PersonalAccessToken{\n\t\tTokenId:     \"pat-update-test\",\n\t\tTokenHash:   patHash,\n\t\tDescription: \"PAT for update test\",\n\t})\n\trequire.NoError(t, err)\n\n\t// Update last used timestamp\n\tnow := timestamppb.Now()\n\terr = ts.UpdatePATLastUsed(ctx, user.ID, \"pat-update-test\", now)\n\trequire.NoError(t, err)\n\n\t// Verify the update\n\tpats, err := ts.GetUserPersonalAccessTokens(ctx, user.ID)\n\trequire.NoError(t, err)\n\trequire.Len(t, pats, 1)\n\trequire.NotNil(t, pats[0].LastUsedAt)\n\n\tts.Close()\n}\n\nfunc TestUserSettingGetUserByPATHashWithExpiredToken(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\tuser, err := createTestingHostUser(ctx, ts)\n\trequire.NoError(t, err)\n\n\t// Create a PAT with expiration info\n\tpatHash := \"pat-hash-with-expiry\"\n\texpiresAt := timestamppb.Now()\n\tpat := &storepb.PersonalAccessTokensUserSetting_PersonalAccessToken{\n\t\tTokenId:     \"pat-expiry-test\",\n\t\tTokenHash:   patHash,\n\t\tDescription: \"PAT with expiry\",\n\t\tExpiresAt:   expiresAt,\n\t}\n\terr = ts.AddUserPersonalAccessToken(ctx, user.ID, pat)\n\trequire.NoError(t, err)\n\n\t// Should still be able to look up by hash (expiry check is done at auth level)\n\tresult, err := ts.GetUserByPATHash(ctx, patHash)\n\trequire.NoError(t, err)\n\trequire.NotNil(t, result)\n\trequire.Equal(t, user.ID, result.UserID)\n\trequire.NotNil(t, result.PAT.ExpiresAt)\n\n\tts.Close()\n}\n\nfunc TestUserSettingGetUserByPATHashAfterRemoval(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\tuser, err := createTestingHostUser(ctx, ts)\n\trequire.NoError(t, err)\n\n\t// Create a PAT\n\tpatHash := \"pat-hash-to-remove\"\n\terr = ts.AddUserPersonalAccessToken(ctx, user.ID, &storepb.PersonalAccessTokensUserSetting_PersonalAccessToken{\n\t\tTokenId:     \"pat-remove-test\",\n\t\tTokenHash:   patHash,\n\t\tDescription: \"PAT to be removed\",\n\t})\n\trequire.NoError(t, err)\n\n\t// Verify it exists\n\tresult, err := ts.GetUserByPATHash(ctx, patHash)\n\trequire.NoError(t, err)\n\trequire.NotNil(t, result)\n\n\t// Remove the PAT\n\terr = ts.RemoveUserPersonalAccessToken(ctx, user.ID, \"pat-remove-test\")\n\trequire.NoError(t, err)\n\n\t// Should no longer be found\n\tresult, err = ts.GetUserByPATHash(ctx, patHash)\n\trequire.Error(t, err)\n\trequire.Nil(t, result)\n\n\tts.Close()\n}\n\nfunc TestUserSettingGetUserByPATHashSpecialCharacters(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\tuser, err := createTestingHostUser(ctx, ts)\n\trequire.NoError(t, err)\n\n\t// Create PATs with special characters in hash (simulating real hash values)\n\ttestCases := []struct {\n\t\ttokenID   string\n\t\ttokenHash string\n\t}{\n\t\t{\"pat-special-1\", \"abc123+/=XYZ\"},\n\t\t{\"pat-special-2\", \"sha256:abcdef1234567890\"},\n\t\t{\"pat-special-3\", \"$2a$10$N9qo8uLOickgx2ZMRZoMy\"},\n\t}\n\n\tfor _, tc := range testCases {\n\t\terr = ts.AddUserPersonalAccessToken(ctx, user.ID, &storepb.PersonalAccessTokensUserSetting_PersonalAccessToken{\n\t\t\tTokenId:     tc.tokenID,\n\t\t\tTokenHash:   tc.tokenHash,\n\t\t\tDescription: \"PAT with special chars\",\n\t\t})\n\t\trequire.NoError(t, err)\n\n\t\t// Verify lookup works with special characters\n\t\tresult, err := ts.GetUserByPATHash(ctx, tc.tokenHash)\n\t\trequire.NoError(t, err)\n\t\trequire.NotNil(t, result)\n\t\trequire.Equal(t, tc.tokenID, result.PAT.TokenId)\n\t}\n\n\tts.Close()\n}\n\nfunc TestUserSettingGetUserByPATHashLargeTokenCount(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\tuser, err := createTestingHostUser(ctx, ts)\n\trequire.NoError(t, err)\n\n\t// Create many PATs for the same user\n\ttokenCount := 10\n\thashes := make([]string, tokenCount)\n\tfor i := 0; i < tokenCount; i++ {\n\t\thashes[i] = \"pat-hash-\" + string(rune('A'+i)) + \"-large-test\"\n\t\terr = ts.AddUserPersonalAccessToken(ctx, user.ID, &storepb.PersonalAccessTokensUserSetting_PersonalAccessToken{\n\t\t\tTokenId:     \"pat-large-\" + string(rune('A'+i)),\n\t\t\tTokenHash:   hashes[i],\n\t\t\tDescription: \"PAT for large count test\",\n\t\t})\n\t\trequire.NoError(t, err)\n\t}\n\n\t// Verify each hash can be looked up\n\tfor i, hash := range hashes {\n\t\tresult, err := ts.GetUserByPATHash(ctx, hash)\n\t\trequire.NoError(t, err)\n\t\trequire.NotNil(t, result)\n\t\trequire.Equal(t, user.ID, result.UserID)\n\t\trequire.Equal(t, \"pat-large-\"+string(rune('A'+i)), result.PAT.TokenId)\n\t}\n\n\tts.Close()\n}\n\nfunc TestUserSettingMultipleSettingTypes(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\tuser, err := createTestingHostUser(ctx, ts)\n\trequire.NoError(t, err)\n\n\t// Create GENERAL setting\n\t_, err = ts.UpsertUserSetting(ctx, &storepb.UserSetting{\n\t\tUserId: user.ID,\n\t\tKey:    storepb.UserSetting_GENERAL,\n\t\tValue:  &storepb.UserSetting_General{General: &storepb.GeneralUserSetting{Locale: \"ja\"}},\n\t})\n\trequire.NoError(t, err)\n\n\t// Create SHORTCUTS setting\n\t_, err = ts.UpsertUserSetting(ctx, &storepb.UserSetting{\n\t\tUserId: user.ID,\n\t\tKey:    storepb.UserSetting_SHORTCUTS,\n\t\tValue: &storepb.UserSetting_Shortcuts{Shortcuts: &storepb.ShortcutsUserSetting{\n\t\t\tShortcuts: []*storepb.ShortcutsUserSetting_Shortcut{\n\t\t\t\t{Id: \"s1\", Title: \"Shortcut 1\"},\n\t\t\t},\n\t\t}},\n\t})\n\trequire.NoError(t, err)\n\n\t// Add a PAT\n\terr = ts.AddUserPersonalAccessToken(ctx, user.ID, &storepb.PersonalAccessTokensUserSetting_PersonalAccessToken{\n\t\tTokenId:   \"pat-multi\",\n\t\tTokenHash: \"hash-multi\",\n\t})\n\trequire.NoError(t, err)\n\n\t// List all settings for user\n\tsettings, err := ts.ListUserSettings(ctx, &store.FindUserSetting{UserID: &user.ID})\n\trequire.NoError(t, err)\n\trequire.Len(t, settings, 3)\n\n\t// Verify each setting type\n\tgeneralSetting, err := ts.GetUserSetting(ctx, &store.FindUserSetting{UserID: &user.ID, Key: storepb.UserSetting_GENERAL})\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"ja\", generalSetting.GetGeneral().Locale)\n\n\tshortcutsSetting, err := ts.GetUserSetting(ctx, &store.FindUserSetting{UserID: &user.ID, Key: storepb.UserSetting_SHORTCUTS})\n\trequire.NoError(t, err)\n\trequire.Len(t, shortcutsSetting.GetShortcuts().Shortcuts, 1)\n\n\tpatsSetting, err := ts.GetUserSetting(ctx, &store.FindUserSetting{UserID: &user.ID, Key: storepb.UserSetting_PERSONAL_ACCESS_TOKENS})\n\trequire.NoError(t, err)\n\trequire.Len(t, patsSetting.GetPersonalAccessTokens().Tokens, 1)\n\n\tts.Close()\n}\n\nfunc TestUserSettingShortcutsEdgeCases(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\tuser, err := createTestingHostUser(ctx, ts)\n\trequire.NoError(t, err)\n\n\t// Case 1: Special characters in Filter and Title\n\t// Includes quotes, backslashes, newlines, and other JSON-sensitive characters\n\tspecialCharsFilter := `tag in [\"work\", \"project\"] && content.contains(\"urgent\")`\n\tspecialCharsTitle := `Work \"Urgent\" \\ Notes`\n\tshortcuts := &storepb.ShortcutsUserSetting{\n\t\tShortcuts: []*storepb.ShortcutsUserSetting_Shortcut{\n\t\t\t{Id: \"s1\", Title: specialCharsTitle, Filter: specialCharsFilter},\n\t\t},\n\t}\n\t_, err = ts.UpsertUserSetting(ctx, &storepb.UserSetting{\n\t\tUserId: user.ID,\n\t\tKey:    storepb.UserSetting_SHORTCUTS,\n\t\tValue:  &storepb.UserSetting_Shortcuts{Shortcuts: shortcuts},\n\t})\n\trequire.NoError(t, err)\n\n\tsetting, err := ts.GetUserSetting(ctx, &store.FindUserSetting{\n\t\tUserID: &user.ID,\n\t\tKey:    storepb.UserSetting_SHORTCUTS,\n\t})\n\trequire.NoError(t, err)\n\trequire.NotNil(t, setting)\n\trequire.Len(t, setting.GetShortcuts().Shortcuts, 1)\n\trequire.Equal(t, specialCharsTitle, setting.GetShortcuts().Shortcuts[0].Title)\n\trequire.Equal(t, specialCharsFilter, setting.GetShortcuts().Shortcuts[0].Filter)\n\n\t// Case 2: Unicode characters\n\tunicodeFilter := `tag in [\"你好\", \"世界\"]`\n\tunicodeTitle := `My 🚀 Shortcuts`\n\tshortcuts = &storepb.ShortcutsUserSetting{\n\t\tShortcuts: []*storepb.ShortcutsUserSetting_Shortcut{\n\t\t\t{Id: \"s2\", Title: unicodeTitle, Filter: unicodeFilter},\n\t\t},\n\t}\n\t_, err = ts.UpsertUserSetting(ctx, &storepb.UserSetting{\n\t\tUserId: user.ID,\n\t\tKey:    storepb.UserSetting_SHORTCUTS,\n\t\tValue:  &storepb.UserSetting_Shortcuts{Shortcuts: shortcuts},\n\t})\n\trequire.NoError(t, err)\n\n\tsetting, err = ts.GetUserSetting(ctx, &store.FindUserSetting{\n\t\tUserID: &user.ID,\n\t\tKey:    storepb.UserSetting_SHORTCUTS,\n\t})\n\trequire.NoError(t, err)\n\trequire.NotNil(t, setting)\n\trequire.Len(t, setting.GetShortcuts().Shortcuts, 1)\n\trequire.Equal(t, unicodeTitle, setting.GetShortcuts().Shortcuts[0].Title)\n\trequire.Equal(t, unicodeFilter, setting.GetShortcuts().Shortcuts[0].Filter)\n\n\t// Case 3: Empty shortcuts list\n\t// Should allow saving an empty list (clearing shortcuts)\n\tshortcuts = &storepb.ShortcutsUserSetting{\n\t\tShortcuts: []*storepb.ShortcutsUserSetting_Shortcut{},\n\t}\n\t_, err = ts.UpsertUserSetting(ctx, &storepb.UserSetting{\n\t\tUserId: user.ID,\n\t\tKey:    storepb.UserSetting_SHORTCUTS,\n\t\tValue:  &storepb.UserSetting_Shortcuts{Shortcuts: shortcuts},\n\t})\n\trequire.NoError(t, err)\n\n\tsetting, err = ts.GetUserSetting(ctx, &store.FindUserSetting{\n\t\tUserID: &user.ID,\n\t\tKey:    storepb.UserSetting_SHORTCUTS,\n\t})\n\trequire.NoError(t, err)\n\trequire.NotNil(t, setting)\n\trequire.NotNil(t, setting.GetShortcuts())\n\trequire.Len(t, setting.GetShortcuts().Shortcuts, 0)\n\n\t// Case 4: Large filter string\n\t// Test reasonable large string handling (e.g. 4KB)\n\tlargeFilter := strings.Repeat(\"tag:long_tag_name \", 200)\n\tshortcuts = &storepb.ShortcutsUserSetting{\n\t\tShortcuts: []*storepb.ShortcutsUserSetting_Shortcut{\n\t\t\t{Id: \"s3\", Title: \"Large Filter\", Filter: largeFilter},\n\t\t},\n\t}\n\t_, err = ts.UpsertUserSetting(ctx, &storepb.UserSetting{\n\t\tUserId: user.ID,\n\t\tKey:    storepb.UserSetting_SHORTCUTS,\n\t\tValue:  &storepb.UserSetting_Shortcuts{Shortcuts: shortcuts},\n\t})\n\trequire.NoError(t, err)\n\n\tsetting, err = ts.GetUserSetting(ctx, &store.FindUserSetting{\n\t\tUserID: &user.ID,\n\t\tKey:    storepb.UserSetting_SHORTCUTS,\n\t})\n\trequire.NoError(t, err)\n\trequire.NotNil(t, setting)\n\trequire.Equal(t, largeFilter, setting.GetShortcuts().Shortcuts[0].Filter)\n\n\tts.Close()\n}\n\nfunc TestUserSettingShortcutsPartialUpdate(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\tuser, err := createTestingHostUser(ctx, ts)\n\trequire.NoError(t, err)\n\n\t// Initial set\n\tshortcuts := &storepb.ShortcutsUserSetting{\n\t\tShortcuts: []*storepb.ShortcutsUserSetting_Shortcut{\n\t\t\t{Id: \"s1\", Title: \"Note 1\", Filter: \"tag:1\"},\n\t\t\t{Id: \"s2\", Title: \"Note 2\", Filter: \"tag:2\"},\n\t\t},\n\t}\n\t_, err = ts.UpsertUserSetting(ctx, &storepb.UserSetting{\n\t\tUserId: user.ID,\n\t\tKey:    storepb.UserSetting_SHORTCUTS,\n\t\tValue:  &storepb.UserSetting_Shortcuts{Shortcuts: shortcuts},\n\t})\n\trequire.NoError(t, err)\n\n\t// Update by replacing the whole list (Store Upsert replaces the value for the key)\n\t// We want to verify that we can \"update\" a single item by sending the modified list\n\tupdatedShortcuts := &storepb.ShortcutsUserSetting{\n\t\tShortcuts: []*storepb.ShortcutsUserSetting_Shortcut{\n\t\t\t{Id: \"s1\", Title: \"Note 1 Updated\", Filter: \"tag:1_updated\"},\n\t\t\t{Id: \"s2\", Title: \"Note 2\", Filter: \"tag:2\"},\n\t\t\t{Id: \"s3\", Title: \"Note 3\", Filter: \"tag:3\"}, // Add new one\n\t\t},\n\t}\n\t_, err = ts.UpsertUserSetting(ctx, &storepb.UserSetting{\n\t\tUserId: user.ID,\n\t\tKey:    storepb.UserSetting_SHORTCUTS,\n\t\tValue:  &storepb.UserSetting_Shortcuts{Shortcuts: updatedShortcuts},\n\t})\n\trequire.NoError(t, err)\n\n\tsetting, err := ts.GetUserSetting(ctx, &store.FindUserSetting{\n\t\tUserID: &user.ID,\n\t\tKey:    storepb.UserSetting_SHORTCUTS,\n\t})\n\trequire.NoError(t, err)\n\trequire.NotNil(t, setting)\n\trequire.Len(t, setting.GetShortcuts().Shortcuts, 3)\n\n\t// Verify updates\n\tfor _, s := range setting.GetShortcuts().Shortcuts {\n\t\tif s.Id == \"s1\" {\n\t\t\trequire.Equal(t, \"Note 1 Updated\", s.Title)\n\t\t\trequire.Equal(t, \"tag:1_updated\", s.Filter)\n\t\t} else if s.Id == \"s2\" {\n\t\t\trequire.Equal(t, \"Note 2\", s.Title)\n\t\t} else if s.Id == \"s3\" {\n\t\t\trequire.Equal(t, \"Note 3\", s.Title)\n\t\t}\n\t}\n\n\tts.Close()\n}\n\nfunc TestUserSettingJSONFieldsEdgeCases(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\tuser, err := createTestingHostUser(ctx, ts)\n\trequire.NoError(t, err)\n\n\t// Case 1: Webhook with special characters and Unicode in Title and URL\n\tspecialWebhook := &storepb.WebhooksUserSetting_Webhook{\n\t\tId:    \"wh-special\",\n\t\tTitle: `My \"Special\" & <Webhook> 🚀`,\n\t\tUrl:   \"https://example.com/hook?query=你好&param=\\\"value\\\"\",\n\t}\n\terr = ts.AddUserWebhook(ctx, user.ID, specialWebhook)\n\trequire.NoError(t, err)\n\n\twebhooks, err := ts.GetUserWebhooks(ctx, user.ID)\n\trequire.NoError(t, err)\n\trequire.Len(t, webhooks, 1)\n\trequire.Equal(t, specialWebhook.Title, webhooks[0].Title)\n\trequire.Equal(t, specialWebhook.Url, webhooks[0].Url)\n\n\t// Case 2: PAT with special description\n\tspecialPAT := &storepb.PersonalAccessTokensUserSetting_PersonalAccessToken{\n\t\tTokenId:     \"pat-special\",\n\t\tTokenHash:   \"hash-special\",\n\t\tDescription: \"Token for 'CLI' \\n & \\\"API\\\" \\t with unicode 🔑\",\n\t}\n\terr = ts.AddUserPersonalAccessToken(ctx, user.ID, specialPAT)\n\trequire.NoError(t, err)\n\n\tpats, err := ts.GetUserPersonalAccessTokens(ctx, user.ID)\n\trequire.NoError(t, err)\n\trequire.Len(t, pats, 1)\n\trequire.Equal(t, specialPAT.Description, pats[0].Description)\n\n\t// Case 3: Refresh Token with special description\n\tspecialRefreshToken := &storepb.RefreshTokensUserSetting_RefreshToken{\n\t\tTokenId:     \"rt-special\",\n\t\tDescription: \"Browser: Firefox (Nightly) / OS: Linux 🐧\",\n\t}\n\terr = ts.AddUserRefreshToken(ctx, user.ID, specialRefreshToken)\n\trequire.NoError(t, err)\n\n\ttokens, err := ts.GetUserRefreshTokens(ctx, user.ID)\n\trequire.NoError(t, err)\n\trequire.Len(t, tokens, 1)\n\trequire.Equal(t, specialRefreshToken.Description, tokens[0].Description)\n\n\tts.Close()\n}\n"
  },
  {
    "path": "store/test/user_test.go",
    "content": "package test\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n\t\"golang.org/x/crypto/bcrypt\"\n\n\t\"github.com/usememos/memos/store\"\n)\n\nfunc TestUserStore(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\tuser, err := createTestingHostUser(ctx, ts)\n\trequire.NoError(t, err)\n\tusers, err := ts.ListUsers(ctx, &store.FindUser{})\n\trequire.NoError(t, err)\n\trequire.Equal(t, 1, len(users))\n\trequire.Equal(t, store.RoleAdmin, users[0].Role)\n\trequire.Equal(t, user, users[0])\n\tuserPatchNickname := \"test_nickname_2\"\n\tuserPatch := &store.UpdateUser{\n\t\tID:       user.ID,\n\t\tNickname: &userPatchNickname,\n\t}\n\tuser, err = ts.UpdateUser(ctx, userPatch)\n\trequire.NoError(t, err)\n\trequire.Equal(t, userPatchNickname, user.Nickname)\n\terr = ts.DeleteUser(ctx, &store.DeleteUser{\n\t\tID: user.ID,\n\t})\n\trequire.NoError(t, err)\n\tusers, err = ts.ListUsers(ctx, &store.FindUser{})\n\trequire.NoError(t, err)\n\trequire.Equal(t, 0, len(users))\n\tts.Close()\n}\n\nfunc TestUserGetByID(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\n\tuser, err := createTestingHostUser(ctx, ts)\n\trequire.NoError(t, err)\n\n\t// Get user by ID\n\tfound, err := ts.GetUser(ctx, &store.FindUser{ID: &user.ID})\n\trequire.NoError(t, err)\n\trequire.NotNil(t, found)\n\trequire.Equal(t, user.ID, found.ID)\n\trequire.Equal(t, user.Username, found.Username)\n\n\t// Get non-existent user\n\tnonExistentID := int32(99999)\n\tnotFound, err := ts.GetUser(ctx, &store.FindUser{ID: &nonExistentID})\n\trequire.NoError(t, err)\n\trequire.Nil(t, notFound)\n\n\t// Get system bot\n\tsystemBotID := store.SystemBotID\n\tsystemBot, err := ts.GetUser(ctx, &store.FindUser{ID: &systemBotID})\n\trequire.NoError(t, err)\n\trequire.NotNil(t, systemBot)\n\trequire.Equal(t, store.SystemBotID, systemBot.ID)\n\trequire.Equal(t, \"system_bot\", systemBot.Username)\n\n\tts.Close()\n}\n\nfunc TestUserGetByUsername(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\n\tuser, err := createTestingHostUser(ctx, ts)\n\trequire.NoError(t, err)\n\n\t// Get user by username\n\tfound, err := ts.GetUser(ctx, &store.FindUser{Username: &user.Username})\n\trequire.NoError(t, err)\n\trequire.NotNil(t, found)\n\trequire.Equal(t, user.Username, found.Username)\n\n\t// Get non-existent username\n\tnonExistent := \"nonexistent\"\n\tnotFound, err := ts.GetUser(ctx, &store.FindUser{Username: &nonExistent})\n\trequire.NoError(t, err)\n\trequire.Nil(t, notFound)\n\n\tts.Close()\n}\n\nfunc TestUserListByRole(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\n\t// Create users with different roles\n\t_, err := createTestingHostUser(ctx, ts)\n\trequire.NoError(t, err)\n\n\t_, err = createTestingUserWithRole(ctx, ts, \"admin_user\", store.RoleAdmin)\n\trequire.NoError(t, err)\n\n\tregularUser, err := createTestingUserWithRole(ctx, ts, \"regular_user\", store.RoleUser)\n\trequire.NoError(t, err)\n\n\t// List all users\n\tallUsers, err := ts.ListUsers(ctx, &store.FindUser{})\n\trequire.NoError(t, err)\n\trequire.Equal(t, 3, len(allUsers))\n\n\t// List only ADMIN users\n\tadminRole := store.RoleAdmin\n\tadminOnlyUsers, err := ts.ListUsers(ctx, &store.FindUser{Role: &adminRole})\n\trequire.NoError(t, err)\n\trequire.Equal(t, 2, len(adminOnlyUsers))\n\n\t// List only USER role users\n\tuserRole := store.RoleUser\n\tregularUsers, err := ts.ListUsers(ctx, &store.FindUser{Role: &userRole})\n\trequire.NoError(t, err)\n\trequire.Equal(t, 1, len(regularUsers))\n\trequire.Equal(t, regularUser.ID, regularUsers[0].ID)\n\n\tts.Close()\n}\n\nfunc TestUserUpdateRowStatus(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\n\tuser, err := createTestingHostUser(ctx, ts)\n\trequire.NoError(t, err)\n\trequire.Equal(t, store.Normal, user.RowStatus)\n\n\t// Archive user\n\tarchivedStatus := store.Archived\n\tupdated, err := ts.UpdateUser(ctx, &store.UpdateUser{\n\t\tID:        user.ID,\n\t\tRowStatus: &archivedStatus,\n\t})\n\trequire.NoError(t, err)\n\trequire.Equal(t, store.Archived, updated.RowStatus)\n\n\t// Verify by fetching\n\tfetched, err := ts.GetUser(ctx, &store.FindUser{ID: &user.ID})\n\trequire.NoError(t, err)\n\trequire.Equal(t, store.Archived, fetched.RowStatus)\n\n\t// Restore to normal\n\tnormalStatus := store.Normal\n\trestored, err := ts.UpdateUser(ctx, &store.UpdateUser{\n\t\tID:        user.ID,\n\t\tRowStatus: &normalStatus,\n\t})\n\trequire.NoError(t, err)\n\trequire.Equal(t, store.Normal, restored.RowStatus)\n\n\tts.Close()\n}\n\nfunc TestUserUpdateAllFields(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\n\tuser, err := createTestingHostUser(ctx, ts)\n\trequire.NoError(t, err)\n\n\t// Update all fields\n\tnewUsername := \"updated_username\"\n\tnewEmail := \"updated@test.com\"\n\tnewNickname := \"Updated Nickname\"\n\tnewAvatarURL := \"https://example.com/avatar.png\"\n\tnewDescription := \"Updated description\"\n\tnewRole := store.RoleAdmin\n\tnewPasswordHash := \"new_password_hash\"\n\n\tupdated, err := ts.UpdateUser(ctx, &store.UpdateUser{\n\t\tID:           user.ID,\n\t\tUsername:     &newUsername,\n\t\tEmail:        &newEmail,\n\t\tNickname:     &newNickname,\n\t\tAvatarURL:    &newAvatarURL,\n\t\tDescription:  &newDescription,\n\t\tRole:         &newRole,\n\t\tPasswordHash: &newPasswordHash,\n\t})\n\trequire.NoError(t, err)\n\trequire.Equal(t, newUsername, updated.Username)\n\trequire.Equal(t, newEmail, updated.Email)\n\trequire.Equal(t, newNickname, updated.Nickname)\n\trequire.Equal(t, newAvatarURL, updated.AvatarURL)\n\trequire.Equal(t, newDescription, updated.Description)\n\trequire.Equal(t, newRole, updated.Role)\n\trequire.Equal(t, newPasswordHash, updated.PasswordHash)\n\n\t// Verify by fetching again\n\tfetched, err := ts.GetUser(ctx, &store.FindUser{ID: &user.ID})\n\trequire.NoError(t, err)\n\trequire.Equal(t, newUsername, fetched.Username)\n\n\tts.Close()\n}\n\nfunc TestUserListWithLimit(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tts := NewTestingStore(ctx, t)\n\n\t// Create 5 users\n\tfor i := 0; i < 5; i++ {\n\t\trole := store.RoleUser\n\t\tif i == 0 {\n\t\t\trole = store.RoleAdmin\n\t\t}\n\t\t_, err := createTestingUserWithRole(ctx, ts, fmt.Sprintf(\"user%d\", i), role)\n\t\trequire.NoError(t, err)\n\t}\n\n\t// List with limit\n\tlimit := 3\n\tusers, err := ts.ListUsers(ctx, &store.FindUser{Limit: &limit})\n\trequire.NoError(t, err)\n\trequire.Equal(t, 3, len(users))\n\n\tts.Close()\n}\n\nfunc createTestingHostUser(ctx context.Context, ts *store.Store) (*store.User, error) {\n\treturn createTestingUserWithRole(ctx, ts, \"test\", store.RoleAdmin)\n}\n\nfunc createTestingUserWithRole(ctx context.Context, ts *store.Store, username string, role store.Role) (*store.User, error) {\n\tuserCreate := &store.User{\n\t\tUsername:    username,\n\t\tRole:        role,\n\t\tEmail:       username + \"@test.com\",\n\t\tNickname:    username + \"_nickname\",\n\t\tDescription: username + \"_description\",\n\t}\n\tpasswordHash, err := bcrypt.GenerateFromPassword([]byte(\"test_password\"), bcrypt.DefaultCost)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tuserCreate.PasswordHash = string(passwordHash)\n\tuser, err := ts.CreateUser(ctx, userCreate)\n\treturn user, err\n}\n"
  },
  {
    "path": "store/user.go",
    "content": "package store\n\nimport (\n\t\"context\"\n)\n\n// Role is the type of a role.\ntype Role string\n\nconst (\n\t// RoleAdmin is the ADMIN role.\n\tRoleAdmin Role = \"ADMIN\"\n\t// RoleUser is the USER role.\n\tRoleUser Role = \"USER\"\n)\n\nfunc (e Role) String() string {\n\tswitch e {\n\tcase RoleAdmin:\n\t\treturn \"ADMIN\"\n\tdefault:\n\t\treturn \"USER\"\n\t}\n}\n\nconst (\n\tSystemBotID int32 = 0\n)\n\nvar (\n\tSystemBot = &User{\n\t\tID:       SystemBotID,\n\t\tUsername: \"system_bot\",\n\t\tRole:     RoleAdmin,\n\t\tEmail:    \"\",\n\t\tNickname: \"Bot\",\n\t}\n)\n\ntype User struct {\n\tID int32\n\n\t// Standard fields\n\tRowStatus RowStatus\n\tCreatedTs int64\n\tUpdatedTs int64\n\n\t// Domain specific fields\n\tUsername     string\n\tRole         Role\n\tEmail        string\n\tNickname     string\n\tPasswordHash string\n\tAvatarURL    string\n\tDescription  string\n}\n\ntype UpdateUser struct {\n\tID int32\n\n\tUpdatedTs    *int64\n\tRowStatus    *RowStatus\n\tUsername     *string\n\tRole         *Role\n\tEmail        *string\n\tNickname     *string\n\tPassword     *string\n\tAvatarURL    *string\n\tPasswordHash *string\n\tDescription  *string\n}\n\ntype FindUser struct {\n\tID        *int32\n\tRowStatus *RowStatus\n\tUsername  *string\n\tRole      *Role\n\tEmail     *string\n\tNickname  *string\n\n\t// Domain specific fields\n\tFilters []string\n\n\t// The maximum number of users to return.\n\tLimit *int\n}\n\ntype DeleteUser struct {\n\tID int32\n}\n\nfunc (s *Store) CreateUser(ctx context.Context, create *User) (*User, error) {\n\tuser, err := s.driver.CreateUser(ctx, create)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\ts.userCache.Set(ctx, string(user.ID), user)\n\treturn user, nil\n}\n\nfunc (s *Store) UpdateUser(ctx context.Context, update *UpdateUser) (*User, error) {\n\tuser, err := s.driver.UpdateUser(ctx, update)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\ts.userCache.Set(ctx, string(user.ID), user)\n\treturn user, nil\n}\n\nfunc (s *Store) ListUsers(ctx context.Context, find *FindUser) ([]*User, error) {\n\tlist, err := s.driver.ListUsers(ctx, find)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfor _, user := range list {\n\t\ts.userCache.Set(ctx, string(user.ID), user)\n\t}\n\treturn list, nil\n}\n\nfunc (s *Store) GetUser(ctx context.Context, find *FindUser) (*User, error) {\n\tif find.ID != nil {\n\t\tif *find.ID == SystemBotID {\n\t\t\treturn SystemBot, nil\n\t\t}\n\t\tif cache, ok := s.userCache.Get(ctx, string(*find.ID)); ok {\n\t\t\tuser, ok := cache.(*User)\n\t\t\tif ok {\n\t\t\t\treturn user, nil\n\t\t\t}\n\t\t}\n\t}\n\n\tlist, err := s.ListUsers(ctx, find)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif len(list) == 0 {\n\t\treturn nil, nil\n\t}\n\n\tuser := list[0]\n\ts.userCache.Set(ctx, string(user.ID), user)\n\treturn user, nil\n}\n\nfunc (s *Store) DeleteUser(ctx context.Context, delete *DeleteUser) error {\n\terr := s.driver.DeleteUser(ctx, delete)\n\tif err != nil {\n\t\treturn err\n\t}\n\ts.userCache.Delete(ctx, string(delete.ID))\n\treturn nil\n}\n"
  },
  {
    "path": "store/user_setting.go",
    "content": "package store\n\nimport (\n\t\"context\"\n\n\t\"github.com/pkg/errors\"\n\t\"google.golang.org/protobuf/encoding/protojson\"\n\t\"google.golang.org/protobuf/types/known/timestamppb\"\n\n\tstorepb \"github.com/usememos/memos/proto/gen/store\"\n)\n\ntype UserSetting struct {\n\tUserID int32\n\tKey    storepb.UserSetting_Key\n\tValue  string\n}\n\ntype FindUserSetting struct {\n\tUserID *int32\n\tKey    storepb.UserSetting_Key\n}\n\n// RefreshTokenQueryResult contains the result of querying a refresh token.\ntype RefreshTokenQueryResult struct {\n\tUserID       int32\n\tRefreshToken *storepb.RefreshTokensUserSetting_RefreshToken\n}\n\n// PATQueryResult contains the result of querying a PAT by hash.\ntype PATQueryResult struct {\n\tUserID int32\n\tUser   *User\n\tPAT    *storepb.PersonalAccessTokensUserSetting_PersonalAccessToken\n}\n\nfunc (s *Store) UpsertUserSetting(ctx context.Context, upsert *storepb.UserSetting) (*storepb.UserSetting, error) {\n\tuserSettingRaw, err := convertUserSettingToRaw(upsert)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tuserSettingRaw, err = s.driver.UpsertUserSetting(ctx, userSettingRaw)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tuserSetting, err := convertUserSettingFromRaw(userSettingRaw)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif userSetting == nil {\n\t\treturn nil, errors.New(\"unexpected nil user setting\")\n\t}\n\ts.userSettingCache.Set(ctx, getUserSettingCacheKey(userSetting.UserId, userSetting.Key.String()), userSetting)\n\treturn userSetting, nil\n}\n\nfunc (s *Store) ListUserSettings(ctx context.Context, find *FindUserSetting) ([]*storepb.UserSetting, error) {\n\tuserSettingRawList, err := s.driver.ListUserSettings(ctx, find)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tuserSettings := []*storepb.UserSetting{}\n\tfor _, userSettingRaw := range userSettingRawList {\n\t\tuserSetting, err := convertUserSettingFromRaw(userSettingRaw)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif userSetting == nil {\n\t\t\tcontinue\n\t\t}\n\t\ts.userSettingCache.Set(ctx, getUserSettingCacheKey(userSetting.UserId, userSetting.Key.String()), userSetting)\n\t\tuserSettings = append(userSettings, userSetting)\n\t}\n\treturn userSettings, nil\n}\n\nfunc (s *Store) GetUserSetting(ctx context.Context, find *FindUserSetting) (*storepb.UserSetting, error) {\n\tif find.UserID != nil {\n\t\tif cache, ok := s.userSettingCache.Get(ctx, getUserSettingCacheKey(*find.UserID, find.Key.String())); ok {\n\t\t\tuserSetting, ok := cache.(*storepb.UserSetting)\n\t\t\tif ok {\n\t\t\t\treturn userSetting, nil\n\t\t\t}\n\t\t}\n\t}\n\n\tlist, err := s.ListUserSettings(ctx, find)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif len(list) == 0 {\n\t\treturn nil, nil\n\t}\n\tif len(list) > 1 {\n\t\treturn nil, errors.Errorf(\"expected 1 user setting, but got %d\", len(list))\n\t}\n\n\tuserSetting := list[0]\n\ts.userSettingCache.Set(ctx, getUserSettingCacheKey(userSetting.UserId, userSetting.Key.String()), userSetting)\n\treturn userSetting, nil\n}\n\n// GetUserByPATHash finds a user by PAT hash.\nfunc (s *Store) GetUserByPATHash(ctx context.Context, tokenHash string) (*PATQueryResult, error) {\n\tresult, err := s.driver.GetUserByPATHash(ctx, tokenHash)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Fetch user info\n\tuser, err := s.GetUser(ctx, &FindUser{ID: &result.UserID})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif user == nil {\n\t\treturn nil, errors.New(\"user not found for PAT\")\n\t}\n\tresult.User = user\n\n\treturn result, nil\n}\n\n// GetUserRefreshTokens returns the refresh tokens of the user.\nfunc (s *Store) GetUserRefreshTokens(ctx context.Context, userID int32) ([]*storepb.RefreshTokensUserSetting_RefreshToken, error) {\n\tuserSetting, err := s.GetUserSetting(ctx, &FindUserSetting{\n\t\tUserID: &userID,\n\t\tKey:    storepb.UserSetting_REFRESH_TOKENS,\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif userSetting == nil {\n\t\treturn []*storepb.RefreshTokensUserSetting_RefreshToken{}, nil\n\t}\n\treturn userSetting.GetRefreshTokens().RefreshTokens, nil\n}\n\n// AddUserRefreshToken adds a new refresh token for the user.\nfunc (s *Store) AddUserRefreshToken(ctx context.Context, userID int32, token *storepb.RefreshTokensUserSetting_RefreshToken) error {\n\ttokens, err := s.GetUserRefreshTokens(ctx, userID)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\ttokens = append(tokens, token)\n\n\t_, err = s.UpsertUserSetting(ctx, &storepb.UserSetting{\n\t\tUserId: userID,\n\t\tKey:    storepb.UserSetting_REFRESH_TOKENS,\n\t\tValue: &storepb.UserSetting_RefreshTokens{\n\t\t\tRefreshTokens: &storepb.RefreshTokensUserSetting{\n\t\t\t\tRefreshTokens: tokens,\n\t\t\t},\n\t\t},\n\t})\n\treturn err\n}\n\n// RemoveUserRefreshToken removes a refresh token from the user.\nfunc (s *Store) RemoveUserRefreshToken(ctx context.Context, userID int32, tokenID string) error {\n\texistingTokens, err := s.GetUserRefreshTokens(ctx, userID)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tnewTokens := make([]*storepb.RefreshTokensUserSetting_RefreshToken, 0, len(existingTokens))\n\tfor _, token := range existingTokens {\n\t\tif token.TokenId != tokenID {\n\t\t\tnewTokens = append(newTokens, token)\n\t\t}\n\t}\n\n\t_, err = s.UpsertUserSetting(ctx, &storepb.UserSetting{\n\t\tUserId: userID,\n\t\tKey:    storepb.UserSetting_REFRESH_TOKENS,\n\t\tValue: &storepb.UserSetting_RefreshTokens{\n\t\t\tRefreshTokens: &storepb.RefreshTokensUserSetting{\n\t\t\t\tRefreshTokens: newTokens,\n\t\t\t},\n\t\t},\n\t})\n\treturn err\n}\n\n// GetUserRefreshTokenByID returns a specific refresh token.\nfunc (s *Store) GetUserRefreshTokenByID(ctx context.Context, userID int32, tokenID string) (*storepb.RefreshTokensUserSetting_RefreshToken, error) {\n\ttokens, err := s.GetUserRefreshTokens(ctx, userID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tfor _, token := range tokens {\n\t\tif token.TokenId == tokenID {\n\t\t\treturn token, nil\n\t\t}\n\t}\n\treturn nil, nil\n}\n\n// GetUserPersonalAccessTokens returns the PATs of the user.\nfunc (s *Store) GetUserPersonalAccessTokens(ctx context.Context, userID int32) ([]*storepb.PersonalAccessTokensUserSetting_PersonalAccessToken, error) {\n\tuserSetting, err := s.GetUserSetting(ctx, &FindUserSetting{\n\t\tUserID: &userID,\n\t\tKey:    storepb.UserSetting_PERSONAL_ACCESS_TOKENS,\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif userSetting == nil {\n\t\treturn []*storepb.PersonalAccessTokensUserSetting_PersonalAccessToken{}, nil\n\t}\n\treturn userSetting.GetPersonalAccessTokens().Tokens, nil\n}\n\n// AddUserPersonalAccessToken adds a new PAT for the user.\nfunc (s *Store) AddUserPersonalAccessToken(ctx context.Context, userID int32, token *storepb.PersonalAccessTokensUserSetting_PersonalAccessToken) error {\n\ttokens, err := s.GetUserPersonalAccessTokens(ctx, userID)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\ttokens = append(tokens, token)\n\n\t_, err = s.UpsertUserSetting(ctx, &storepb.UserSetting{\n\t\tUserId: userID,\n\t\tKey:    storepb.UserSetting_PERSONAL_ACCESS_TOKENS,\n\t\tValue: &storepb.UserSetting_PersonalAccessTokens{\n\t\t\tPersonalAccessTokens: &storepb.PersonalAccessTokensUserSetting{\n\t\t\t\tTokens: tokens,\n\t\t\t},\n\t\t},\n\t})\n\treturn err\n}\n\n// RemoveUserPersonalAccessToken removes a PAT from the user.\nfunc (s *Store) RemoveUserPersonalAccessToken(ctx context.Context, userID int32, tokenID string) error {\n\texistingTokens, err := s.GetUserPersonalAccessTokens(ctx, userID)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tnewTokens := make([]*storepb.PersonalAccessTokensUserSetting_PersonalAccessToken, 0, len(existingTokens))\n\tfor _, token := range existingTokens {\n\t\tif token.TokenId != tokenID {\n\t\t\tnewTokens = append(newTokens, token)\n\t\t}\n\t}\n\n\t_, err = s.UpsertUserSetting(ctx, &storepb.UserSetting{\n\t\tUserId: userID,\n\t\tKey:    storepb.UserSetting_PERSONAL_ACCESS_TOKENS,\n\t\tValue: &storepb.UserSetting_PersonalAccessTokens{\n\t\t\tPersonalAccessTokens: &storepb.PersonalAccessTokensUserSetting{\n\t\t\t\tTokens: newTokens,\n\t\t\t},\n\t\t},\n\t})\n\treturn err\n}\n\n// UpdatePATLastUsed updates the last_used_at timestamp of a PAT.\nfunc (s *Store) UpdatePATLastUsed(ctx context.Context, userID int32, tokenID string, lastUsed *timestamppb.Timestamp) error {\n\ttokens, err := s.GetUserPersonalAccessTokens(ctx, userID)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfor _, token := range tokens {\n\t\tif token.TokenId == tokenID {\n\t\t\ttoken.LastUsedAt = lastUsed\n\t\t\tbreak\n\t\t}\n\t}\n\n\t_, err = s.UpsertUserSetting(ctx, &storepb.UserSetting{\n\t\tUserId: userID,\n\t\tKey:    storepb.UserSetting_PERSONAL_ACCESS_TOKENS,\n\t\tValue: &storepb.UserSetting_PersonalAccessTokens{\n\t\t\tPersonalAccessTokens: &storepb.PersonalAccessTokensUserSetting{\n\t\t\t\tTokens: tokens,\n\t\t\t},\n\t\t},\n\t})\n\treturn err\n}\n\n// GetUserWebhooks returns the webhooks of the user.\nfunc (s *Store) GetUserWebhooks(ctx context.Context, userID int32) ([]*storepb.WebhooksUserSetting_Webhook, error) {\n\tuserSetting, err := s.GetUserSetting(ctx, &FindUserSetting{\n\t\tUserID: &userID,\n\t\tKey:    storepb.UserSetting_WEBHOOKS,\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif userSetting == nil {\n\t\treturn []*storepb.WebhooksUserSetting_Webhook{}, nil\n\t}\n\n\twebhooksUserSetting := userSetting.GetWebhooks()\n\treturn webhooksUserSetting.Webhooks, nil\n}\n\n// AddUserWebhook adds a new webhook for the user.\nfunc (s *Store) AddUserWebhook(ctx context.Context, userID int32, webhook *storepb.WebhooksUserSetting_Webhook) error {\n\texistingWebhooks, err := s.GetUserWebhooks(ctx, userID)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Check if webhook already exists, update if it does\n\tvar updatedWebhooks []*storepb.WebhooksUserSetting_Webhook\n\twebhookExists := false\n\tfor _, existing := range existingWebhooks {\n\t\tif existing.Id == webhook.Id {\n\t\t\tupdatedWebhooks = append(updatedWebhooks, webhook)\n\t\t\twebhookExists = true\n\t\t} else {\n\t\t\tupdatedWebhooks = append(updatedWebhooks, existing)\n\t\t}\n\t}\n\n\t// If webhook doesn't exist, add it\n\tif !webhookExists {\n\t\tupdatedWebhooks = append(updatedWebhooks, webhook)\n\t}\n\n\t_, err = s.UpsertUserSetting(ctx, &storepb.UserSetting{\n\t\tUserId: userID,\n\t\tKey:    storepb.UserSetting_WEBHOOKS,\n\t\tValue: &storepb.UserSetting_Webhooks{\n\t\t\tWebhooks: &storepb.WebhooksUserSetting{\n\t\t\t\tWebhooks: updatedWebhooks,\n\t\t\t},\n\t\t},\n\t})\n\n\treturn err\n}\n\n// RemoveUserWebhook removes the webhook of the user.\nfunc (s *Store) RemoveUserWebhook(ctx context.Context, userID int32, webhookID string) error {\n\toldWebhooks, err := s.GetUserWebhooks(ctx, userID)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tnewWebhooks := make([]*storepb.WebhooksUserSetting_Webhook, 0, len(oldWebhooks))\n\tfor _, webhook := range oldWebhooks {\n\t\tif webhookID != webhook.Id {\n\t\t\tnewWebhooks = append(newWebhooks, webhook)\n\t\t}\n\t}\n\n\t_, err = s.UpsertUserSetting(ctx, &storepb.UserSetting{\n\t\tUserId: userID,\n\t\tKey:    storepb.UserSetting_WEBHOOKS,\n\t\tValue: &storepb.UserSetting_Webhooks{\n\t\t\tWebhooks: &storepb.WebhooksUserSetting{\n\t\t\t\tWebhooks: newWebhooks,\n\t\t\t},\n\t\t},\n\t})\n\n\treturn err\n}\n\n// UpdateUserWebhook updates an existing webhook for the user.\nfunc (s *Store) UpdateUserWebhook(ctx context.Context, userID int32, webhook *storepb.WebhooksUserSetting_Webhook) error {\n\twebhooks, err := s.GetUserWebhooks(ctx, userID)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfor i, existing := range webhooks {\n\t\tif existing.Id == webhook.Id {\n\t\t\twebhooks[i] = webhook\n\t\t\tbreak\n\t\t}\n\t}\n\n\t_, err = s.UpsertUserSetting(ctx, &storepb.UserSetting{\n\t\tUserId: userID,\n\t\tKey:    storepb.UserSetting_WEBHOOKS,\n\t\tValue: &storepb.UserSetting_Webhooks{\n\t\t\tWebhooks: &storepb.WebhooksUserSetting{\n\t\t\t\tWebhooks: webhooks,\n\t\t\t},\n\t\t},\n\t})\n\n\treturn err\n}\n\nfunc convertUserSettingFromRaw(raw *UserSetting) (*storepb.UserSetting, error) {\n\tuserSetting := &storepb.UserSetting{\n\t\tUserId: raw.UserID,\n\t\tKey:    raw.Key,\n\t}\n\n\tswitch raw.Key {\n\tcase storepb.UserSetting_SHORTCUTS:\n\t\tshortcutsUserSetting := &storepb.ShortcutsUserSetting{}\n\t\tif err := protojsonUnmarshaler.Unmarshal([]byte(raw.Value), shortcutsUserSetting); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tuserSetting.Value = &storepb.UserSetting_Shortcuts{Shortcuts: shortcutsUserSetting}\n\tcase storepb.UserSetting_GENERAL:\n\t\tgeneralUserSetting := &storepb.GeneralUserSetting{}\n\t\tif err := protojsonUnmarshaler.Unmarshal([]byte(raw.Value), generalUserSetting); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tuserSetting.Value = &storepb.UserSetting_General{General: generalUserSetting}\n\tcase storepb.UserSetting_REFRESH_TOKENS:\n\t\trefreshTokensUserSetting := &storepb.RefreshTokensUserSetting{}\n\t\tif err := protojsonUnmarshaler.Unmarshal([]byte(raw.Value), refreshTokensUserSetting); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tuserSetting.Value = &storepb.UserSetting_RefreshTokens{RefreshTokens: refreshTokensUserSetting}\n\tcase storepb.UserSetting_PERSONAL_ACCESS_TOKENS:\n\t\tpatsUserSetting := &storepb.PersonalAccessTokensUserSetting{}\n\t\tif err := protojsonUnmarshaler.Unmarshal([]byte(raw.Value), patsUserSetting); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tuserSetting.Value = &storepb.UserSetting_PersonalAccessTokens{PersonalAccessTokens: patsUserSetting}\n\tcase storepb.UserSetting_WEBHOOKS:\n\t\twebhooksUserSetting := &storepb.WebhooksUserSetting{}\n\t\tif err := protojsonUnmarshaler.Unmarshal([]byte(raw.Value), webhooksUserSetting); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tuserSetting.Value = &storepb.UserSetting_Webhooks{Webhooks: webhooksUserSetting}\n\tdefault:\n\t\treturn nil, nil\n\t}\n\treturn userSetting, nil\n}\n\nfunc convertUserSettingToRaw(userSetting *storepb.UserSetting) (*UserSetting, error) {\n\traw := &UserSetting{\n\t\tUserID: userSetting.UserId,\n\t\tKey:    userSetting.Key,\n\t}\n\n\tswitch userSetting.Key {\n\tcase storepb.UserSetting_SHORTCUTS:\n\t\tshortcutsUserSetting := userSetting.GetShortcuts()\n\t\tvalue, err := protojson.Marshal(shortcutsUserSetting)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\traw.Value = string(value)\n\tcase storepb.UserSetting_GENERAL:\n\t\tgeneralUserSetting := userSetting.GetGeneral()\n\t\tvalue, err := protojson.Marshal(generalUserSetting)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\traw.Value = string(value)\n\tcase storepb.UserSetting_REFRESH_TOKENS:\n\t\trefreshTokensUserSetting := userSetting.GetRefreshTokens()\n\t\tvalue, err := protojson.Marshal(refreshTokensUserSetting)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\traw.Value = string(value)\n\tcase storepb.UserSetting_PERSONAL_ACCESS_TOKENS:\n\t\tpatsUserSetting := userSetting.GetPersonalAccessTokens()\n\t\tvalue, err := protojson.Marshal(patsUserSetting)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\traw.Value = string(value)\n\tcase storepb.UserSetting_WEBHOOKS:\n\t\twebhooksUserSetting := userSetting.GetWebhooks()\n\t\tvalue, err := protojson.Marshal(webhooksUserSetting)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\traw.Value = string(value)\n\tdefault:\n\t\treturn nil, errors.Errorf(\"unsupported user setting key: %v\", userSetting.Key)\n\t}\n\treturn raw, nil\n}\n"
  },
  {
    "path": "web/.gitignore",
    "content": "node_modules\n.pnpm-store\n.DS_Store\ndist\ndist-ssr\n*.local\nsrc/types/proto/store\n"
  },
  {
    "path": "web/biome.json",
    "content": "{\n\t\"$schema\": \"https://biomejs.dev/schemas/2.3.5/schema.json\",\n\t\"vcs\": {\n\t\t\"enabled\": true,\n\t\t\"clientKind\": \"git\",\n\t\t\"useIgnoreFile\": true\n\t},\n\t\"files\": {\n\t\t\"includes\": [\n\t\t\t\"**\",\n\t\t\t\"!!**/dist\",\n\t\t\t\"!src/types/proto\"\n\t\t],\n\t\t\"ignoreUnknown\": true\n\t},\n\t\"formatter\": {\n\t\t\"enabled\": true,\n\t\t\"formatWithErrors\": false,\n\t\t\"indentStyle\": \"space\",\n\t\t\"indentWidth\": 2,\n\t\t\"lineEnding\": \"lf\",\n\t\t\"lineWidth\": 140,\n\t\t\"attributePosition\": \"auto\",\n\t\t\"bracketSameLine\": false,\n\t\t\"bracketSpacing\": true,\n\t\t\"expand\": \"auto\",\n\t\t\"useEditorconfig\": true\n\t},\n\t\"linter\": {\n\t\t\"enabled\": true,\n\t\t\"rules\": {\n\t\t\t\"recommended\": false,\n\t\t\t\"complexity\": {\n\t\t\t\t\"noAdjacentSpacesInRegex\": \"error\",\n\t\t\t\t\"noExtraBooleanCast\": \"error\",\n\t\t\t\t\"noUselessCatch\": \"error\",\n\t\t\t\t\"noUselessEscapeInRegex\": \"error\",\n\t\t\t\t\"noUselessTypeConstraint\": \"error\"\n\t\t\t},\n\t\t\t\"correctness\": {\n\t\t\t\t\"noConstAssign\": \"error\",\n\t\t\t\t\"noConstantCondition\": \"error\",\n\t\t\t\t\"noEmptyCharacterClassInRegex\": \"error\",\n\t\t\t\t\"noEmptyPattern\": \"error\",\n\t\t\t\t\"noGlobalObjectCalls\": \"error\",\n\t\t\t\t\"noInvalidBuiltinInstantiation\": \"error\",\n\t\t\t\t\"noInvalidConstructorSuper\": \"error\",\n\t\t\t\t\"noNonoctalDecimalEscape\": \"error\",\n\t\t\t\t\"noPrecisionLoss\": \"error\",\n\t\t\t\t\"noSelfAssign\": \"error\",\n\t\t\t\t\"noSetterReturn\": \"error\",\n\t\t\t\t\"noSwitchDeclarations\": \"error\",\n\t\t\t\t\"noUndeclaredVariables\": \"error\",\n\t\t\t\t\"noUnreachable\": \"error\",\n\t\t\t\t\"noUnreachableSuper\": \"error\",\n\t\t\t\t\"noUnsafeFinally\": \"error\",\n\t\t\t\t\"noUnsafeOptionalChaining\": \"error\",\n\t\t\t\t\"noUnusedLabels\": \"error\",\n\t\t\t\t\"noUnusedPrivateClassMembers\": \"error\",\n\t\t\t\t\"noUnusedVariables\": \"error\",\n\t\t\t\t\"useIsNan\": \"error\",\n\t\t\t\t\"useValidForDirection\": \"error\",\n\t\t\t\t\"useValidTypeof\": \"error\",\n\t\t\t\t\"useYield\": \"error\"\n\t\t\t},\n\t\t\t\"style\": {\n\t\t\t\t\"noCommonJs\": \"error\",\n\t\t\t\t\"noNamespace\": \"error\",\n\t\t\t\t\"useArrayLiterals\": \"error\",\n\t\t\t\t\"useAsConstAssertion\": \"error\",\n\t\t\t\t\"useBlockStatements\": \"off\"\n\t\t\t},\n\t\t\t\"suspicious\": {\n\t\t\t\t\"noAsyncPromiseExecutor\": \"error\",\n\t\t\t\t\"noCatchAssign\": \"error\",\n\t\t\t\t\"noClassAssign\": \"error\",\n\t\t\t\t\"noCompareNegZero\": \"error\",\n\t\t\t\t\"noConstantBinaryExpressions\": \"error\",\n\t\t\t\t\"noControlCharactersInRegex\": \"error\",\n\t\t\t\t\"noDebugger\": \"error\",\n\t\t\t\t\"noDuplicateCase\": \"error\",\n\t\t\t\t\"noDuplicateClassMembers\": \"error\",\n\t\t\t\t\"noDuplicateElseIf\": \"error\",\n\t\t\t\t\"noDuplicateObjectKeys\": \"error\",\n\t\t\t\t\"noDuplicateParameters\": \"error\",\n\t\t\t\t\"noEmptyBlockStatements\": \"off\",\n\t\t\t\t\"noExplicitAny\": \"error\",\n\t\t\t\t\"noExtraNonNullAssertion\": \"error\",\n\t\t\t\t\"noFallthroughSwitchClause\": \"error\",\n\t\t\t\t\"noFunctionAssign\": \"error\",\n\t\t\t\t\"noGlobalAssign\": \"error\",\n\t\t\t\t\"noImportAssign\": \"error\",\n\t\t\t\t\"noIrregularWhitespace\": \"error\",\n\t\t\t\t\"noMisleadingCharacterClass\": \"error\",\n\t\t\t\t\"noMisleadingInstantiator\": \"error\",\n\t\t\t\t\"noNonNullAssertedOptionalChain\": \"error\",\n\t\t\t\t\"noPrototypeBuiltins\": \"error\",\n\t\t\t\t\"noRedeclare\": \"error\",\n\t\t\t\t\"noShadowRestrictedNames\": \"error\",\n\t\t\t\t\"noSparseArray\": \"error\",\n\t\t\t\t\"noUnsafeDeclarationMerging\": \"error\",\n\t\t\t\t\"noUnsafeNegation\": \"error\",\n\t\t\t\t\"noUselessRegexBackrefs\": \"error\",\n\t\t\t\t\"noWith\": \"error\",\n\t\t\t\t\"useGetterReturn\": \"error\",\n\t\t\t\t\"useNamespaceKeyword\": \"error\"\n\t\t\t}\n\t\t},\n\t\t\"includes\": [\n\t\t\t\"**\",\n\t\t\t\"!**/dist/**\",\n\t\t\t\"!**/node_modules/**\",\n\t\t\t\"!src/types/proto/**\"\n\t\t]\n\t},\n\t\"javascript\": {\n\t\t\"formatter\": {\n\t\t\t\"jsxQuoteStyle\": \"double\",\n\t\t\t\"quoteProperties\": \"asNeeded\",\n\t\t\t\"trailingCommas\": \"all\",\n\t\t\t\"semicolons\": \"always\",\n\t\t\t\"arrowParentheses\": \"always\",\n\t\t\t\"bracketSameLine\": false,\n\t\t\t\"quoteStyle\": \"double\",\n\t\t\t\"attributePosition\": \"auto\",\n\t\t\t\"bracketSpacing\": true\n\t\t},\n\t\t\"globals\": []\n\t},\n\t\"css\": {\n\t\t\"parser\": {\n\t\t\t\"cssModules\": false,\n\t\t\t\"allowWrongLineComments\": true,\n\t\t\t\"tailwindDirectives\": true\n\t\t}\n\t},\n\t\"html\": {\n\t\t\"formatter\": {\n\t\t\t\"indentScriptAndStyle\": false,\n\t\t\t\"selfCloseVoidElements\": \"always\"\n\t\t}\n\t},\n\t\"overrides\": [\n\t\t{\n\t\t\t\"includes\": [\n\t\t\t\t\"**/*.ts\",\n\t\t\t\t\"**/*.tsx\",\n\t\t\t\t\"**/*.mts\",\n\t\t\t\t\"**/*.cts\"\n\t\t\t],\n\t\t\t\"linter\": {\n\t\t\t\t\"rules\": {\n\t\t\t\t\t\"complexity\": {\n\t\t\t\t\t\t\"noArguments\": \"error\"\n\t\t\t\t\t},\n\t\t\t\t\t\"correctness\": {\n\t\t\t\t\t\t\"noConstAssign\": \"off\",\n\t\t\t\t\t\t\"noGlobalObjectCalls\": \"off\",\n\t\t\t\t\t\t\"noInvalidBuiltinInstantiation\": \"off\",\n\t\t\t\t\t\t\"noInvalidConstructorSuper\": \"off\",\n\t\t\t\t\t\t\"noSetterReturn\": \"off\",\n\t\t\t\t\t\t\"noUndeclaredVariables\": \"off\",\n\t\t\t\t\t\t\"noUnreachable\": \"off\",\n\t\t\t\t\t\t\"noUnreachableSuper\": \"off\"\n\t\t\t\t\t},\n\t\t\t\t\t\"style\": {\n\t\t\t\t\t\t\"useConst\": \"error\"\n\t\t\t\t\t},\n\t\t\t\t\t\"suspicious\": {\n\t\t\t\t\t\t\"noClassAssign\": \"off\",\n\t\t\t\t\t\t\"noDuplicateClassMembers\": \"off\",\n\t\t\t\t\t\t\"noDuplicateObjectKeys\": \"off\",\n\t\t\t\t\t\t\"noDuplicateParameters\": \"off\",\n\t\t\t\t\t\t\"noFunctionAssign\": \"off\",\n\t\t\t\t\t\t\"noImportAssign\": \"off\",\n\t\t\t\t\t\t\"noRedeclare\": \"off\",\n\t\t\t\t\t\t\"noUnsafeNegation\": \"off\",\n\t\t\t\t\t\t\"noVar\": \"error\",\n\t\t\t\t\t\t\"noWith\": \"off\",\n\t\t\t\t\t\t\"useGetterReturn\": \"off\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t\"includes\": [\n\t\t\t\t\"src/utils/i18n.ts\"\n\t\t\t],\n\t\t\t\"linter\": {\n\t\t\t\t\"rules\": {}\n\t\t\t}\n\t\t}\n\t],\n\t\"assist\": {\n\t\t\"enabled\": true,\n\t\t\"actions\": {\n\t\t\t\"source\": {\n\t\t\t\t\"organizeImports\": \"on\"\n\t\t\t}\n\t\t}\n\t}\n}"
  },
  {
    "path": "web/components.json",
    "content": "{\n  \"$schema\": \"https://ui.shadcn.com/schema.json\",\n  \"style\": \"new-york\",\n  \"rsc\": false,\n  \"tsx\": true,\n  \"tailwind\": {\n    \"config\": \"\",\n    \"css\": \"src/index.css\",\n    \"baseColor\": \"zinc\",\n    \"cssVariables\": true,\n    \"prefix\": \"\"\n  },\n  \"aliases\": {\n    \"components\": \"@/components\",\n    \"utils\": \"@/lib/utils\",\n    \"ui\": \"@/components/ui\",\n    \"lib\": \"@/lib\",\n    \"hooks\": \"@/hooks\"\n  },\n  \"iconLibrary\": \"lucide\"\n}"
  },
  {
    "path": "web/docs/auth-architecture.md",
    "content": "# Authentication State Architecture\n\n## Current Approach: AuthContext\n\nThe application uses **AuthContext** for authentication state management, not React Query's `useCurrentUserQuery`. This is an intentional architectural decision.\n\n### Why AuthContext Instead of React Query?\n\n#### 1. **Synchronous Initialization**\n- AuthContext fetches user data during app initialization (`main.tsx`)\n- Provides synchronous access to `currentUser` throughout the app\n- No need to handle loading states in every component\n\n#### 2. **Single Source of Truth**\n- User data fetched once on mount\n- All components get consistent, up-to-date user info\n- No race conditions from multiple query instances\n\n#### 3. **Integration with React Query**\n- AuthContext pre-populates React Query cache after fetch (line 81-82 in `AuthContext.tsx`)\n- Best of both worlds: synchronous access + cache consistency\n- React Query hooks like `useNotifications()` can still use the cached user data\n\n#### 4. **Simpler Component Code**\n```typescript\n// With AuthContext (current)\nconst user = useCurrentUser(); // Always returns User | undefined\n\n// With React Query (alternative)\nconst { data: user, isLoading } = useCurrentUserQuery();\nif (isLoading) return <Spinner />;\n// Need loading handling everywhere\n```\n\n### When to Use React Query for Auth?\n\nConsider migrating auth to React Query if:\n- App needs real-time user profile updates from external sources\n- Multiple tabs need instant sync\n- User data changes frequently during a session\n\nFor Memos (a notes app where user profile rarely changes), AuthContext is the right choice.\n\n### Future Considerations\n\nThe unused `useCurrentUserQuery()` hook in `useUserQueries.ts` is kept for potential future use. If requirements change (e.g., real-time collaboration on user profiles), migration path is clear:\n\n1. Remove AuthContext\n2. Use `useCurrentUserQuery()` everywhere\n3. Handle loading states in components\n4. Add suspense boundaries if needed\n\n## Recommendation\n\n**Keep the current AuthContext approach.** It provides better DX and performance for this use case.\n"
  },
  {
    "path": "web/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta http-equiv=\"Cache-Control\" content=\"no-cache, no-store, must-revalidate\" />\n    <meta http-equiv=\"Pragma\" content=\"no-cache\" />\n    <meta http-equiv=\"Expires\" content=\"0\" />\n    <link rel=\"apple-touch-icon\" sizes=\"180x180\" href=\"/apple-touch-icon.png\" />\n    <link rel=\"icon\" type=\"image/webp\" href=\"/logo.webp\" />\n    <link rel=\"manifest\" href=\"/site.webmanifest\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, user-scalable=no\" />\n    <meta name=\"theme-color\" content=\"#faf9f5\" />\n    <meta name=\"mobile-web-app-capable\" content=\"yes\" />\n    <meta name=\"apple-mobile-web-app-capable\" content=\"yes\" />\n    <meta name=\"apple-mobile-web-app-status-bar-style\" content=\"default\" />\n    <!-- memos.metadata.head -->\n    <title>Memos</title>\n  </head>\n  <body class=\"text-base w-full min-h-svh\">\n    <div id=\"root\" class=\"relative w-full min-h-full\"></div>\n    <script type=\"module\" src=\"/src/main.tsx\"></script>\n    <!-- memos.metadata.body -->\n  </body>\n</html>\n"
  },
  {
    "path": "web/package.json",
    "content": "{\n  \"name\": \"memos\",\n  \"private\": true,\n  \"engines\": {\n    \"node\": \">=24\"\n  },\n  \"scripts\": {\n    \"dev\": \"vite\",\n    \"build\": \"vite build\",\n    \"release\": \"vite build --mode release --outDir=../server/router/frontend/dist --emptyOutDir\",\n    \"lint\": \"tsc --noEmit --skipLibCheck && biome check src\",\n    \"lint:fix\": \"biome check --write src\",\n    \"format\": \"biome format --write src\"\n  },\n  \"dependencies\": {\n    \"@connectrpc/connect\": \"^2.1.1\",\n    \"@connectrpc/connect-web\": \"^2.1.1\",\n    \"@emotion/react\": \"^11.14.0\",\n    \"@emotion/styled\": \"^11.14.1\",\n    \"@github/relative-time-element\": \"^4.5.0\",\n    \"@radix-ui/react-checkbox\": \"^1.3.3\",\n    \"@radix-ui/react-dialog\": \"^1.1.15\",\n    \"@radix-ui/react-dropdown-menu\": \"^2.1.16\",\n    \"@radix-ui/react-label\": \"^2.1.8\",\n    \"@radix-ui/react-popover\": \"^1.1.15\",\n    \"@radix-ui/react-radio-group\": \"^1.3.8\",\n    \"@radix-ui/react-select\": \"^2.2.6\",\n    \"@radix-ui/react-separator\": \"^1.1.8\",\n    \"@radix-ui/react-slot\": \"^1.2.4\",\n    \"@radix-ui/react-switch\": \"^1.2.6\",\n    \"@radix-ui/react-tooltip\": \"^1.2.8\",\n    \"@tailwindcss/vite\": \"^4.2.1\",\n    \"@tanstack/react-query\": \"^5.90.21\",\n    \"@tanstack/react-query-devtools\": \"^5.91.3\",\n    \"class-variance-authority\": \"^0.7.1\",\n    \"clsx\": \"^2.1.1\",\n    \"copy-to-clipboard\": \"^3.3.3\",\n    \"dayjs\": \"^1.11.20\",\n    \"fuse.js\": \"^7.1.0\",\n    \"highlight.js\": \"^11.11.1\",\n    \"i18next\": \"^25.8.18\",\n    \"katex\": \"^0.16.38\",\n    \"leaflet\": \"^1.9.4\",\n    \"leaflet.markercluster\": \"^1.5.3\",\n    \"lodash-es\": \"^4.17.23\",\n    \"lucide-react\": \"^0.577.0\",\n    \"mdast-util-from-markdown\": \"^2.0.3\",\n    \"mdast-util-gfm\": \"^3.1.0\",\n    \"mermaid\": \"^11.13.0\",\n    \"micromark-extension-gfm\": \"^3.0.0\",\n    \"mime\": \"^4.1.0\",\n    \"react\": \"^18.3.1\",\n    \"react-dom\": \"^18.3.1\",\n    \"react-force-graph-2d\": \"^1.29.1\",\n    \"react-hot-toast\": \"^2.6.0\",\n    \"react-i18next\": \"^15.7.4\",\n    \"react-leaflet\": \"^4.2.1\",\n    \"react-leaflet-cluster\": \"^2.1.0\",\n    \"react-markdown\": \"^10.1.0\",\n    \"react-router-dom\": \"^7.13.1\",\n    \"react-use\": \"^17.6.0\",\n    \"rehype-katex\": \"^7.0.1\",\n    \"rehype-raw\": \"^7.0.0\",\n    \"rehype-sanitize\": \"^6.0.0\",\n    \"remark-breaks\": \"^4.0.0\",\n    \"remark-gfm\": \"^4.0.1\",\n    \"remark-math\": \"^6.0.0\",\n    \"tailwind-merge\": \"^3.5.0\",\n    \"tailwindcss\": \"^4.2.1\",\n    \"textarea-caret\": \"^3.1.0\",\n    \"unist-util-visit\": \"^5.1.0\",\n    \"uuid\": \"^11.1.0\"\n  },\n  \"devDependencies\": {\n    \"@biomejs/biome\": \"^2.4.7\",\n    \"baseline-browser-mapping\": \"^2.10.8\",\n    \"@bufbuild/protobuf\": \"^2.11.0\",\n    \"@types/d3\": \"^7.4.3\",\n    \"@types/hast\": \"^3.0.4\",\n    \"@types/katex\": \"^0.16.8\",\n    \"@types/leaflet\": \"^1.9.21\",\n    \"@types/lodash-es\": \"^4.17.12\",\n    \"@types/mdast\": \"^4.0.4\",\n    \"@types/node\": \"^24.10.1\",\n    \"@types/qs\": \"^6.15.0\",\n    \"@types/react\": \"^18.3.27\",\n    \"@types/react-dom\": \"^18.3.7\",\n    \"@types/textarea-caret\": \"^3.0.4\",\n    \"@types/unist\": \"^3.0.3\",\n    \"@types/uuid\": \"^10.0.0\",\n    \"@vitejs/plugin-react\": \"^4.7.0\",\n    \"long\": \"^5.3.2\",\n    \"terser\": \"^5.46.1\",\n    \"tw-animate-css\": \"^1.4.0\",\n    \"typescript\": \"^5.9.3\",\n    \"vite\": \"^7.2.4\"\n  },\n  \"pnpm\": {\n    \"onlyBuiltDependencies\": [\n      \"esbuild\"\n    ]\n  }\n}\n"
  },
  {
    "path": "web/public/site.webmanifest",
    "content": "{\n  \"name\": \"Memos\",\n  \"short_name\": \"Memos\",\n  \"description\": \"An open-source, self-hosted note-taking tool. Capture thoughts instantly. Own them completely.\",\n  \"icons\": [\n    { \"src\": \"/android-chrome-192x192.png\", \"sizes\": \"192x192\", \"type\": \"image/png\" },\n    { \"src\": \"/android-chrome-512x512.png\", \"sizes\": \"512x512\", \"type\": \"image/png\" }\n  ],\n  \"display\": \"standalone\",\n  \"scope\": \"/\",\n  \"start_url\": \"/\",\n  \"theme_color\": \"#faf9f5\",\n  \"background_color\": \"#faf9f5\"\n}\n"
  },
  {
    "path": "web/src/App.tsx",
    "content": "import { useEffect } from \"react\";\nimport { Outlet } from \"react-router-dom\";\nimport { useInstance } from \"./contexts/InstanceContext\";\nimport { MemoFilterProvider } from \"./contexts/MemoFilterContext\";\nimport useNavigateTo from \"./hooks/useNavigateTo\";\nimport { useUserLocale } from \"./hooks/useUserLocale\";\nimport { useUserTheme } from \"./hooks/useUserTheme\";\nimport { cleanupExpiredOAuthState } from \"./utils/oauth\";\n\nconst App = () => {\n  const navigateTo = useNavigateTo();\n  const { profile: instanceProfile, profileLoaded, generalSetting: instanceGeneralSetting } = useInstance();\n\n  // Apply user preferences reactively\n  useUserLocale();\n  useUserTheme();\n\n  // Clean up expired OAuth states on app initialization\n  useEffect(() => {\n    cleanupExpiredOAuthState();\n  }, []);\n\n  // Redirect to sign up page if instance not initialized (no admin account exists yet).\n  // Guard with profileLoaded so a fetch failure doesn't incorrectly trigger the redirect.\n  useEffect(() => {\n    if (profileLoaded && !instanceProfile.admin) {\n      navigateTo(\"/auth/signup\");\n    }\n  }, [profileLoaded, instanceProfile.admin, navigateTo]);\n\n  useEffect(() => {\n    if (instanceGeneralSetting.additionalStyle) {\n      const styleEl = document.createElement(\"style\");\n      styleEl.innerHTML = instanceGeneralSetting.additionalStyle;\n      styleEl.setAttribute(\"type\", \"text/css\");\n      document.body.insertAdjacentElement(\"beforeend\", styleEl);\n    }\n  }, [instanceGeneralSetting.additionalStyle]);\n\n  useEffect(() => {\n    if (instanceGeneralSetting.additionalScript) {\n      const scriptEl = document.createElement(\"script\");\n      scriptEl.innerHTML = instanceGeneralSetting.additionalScript;\n      document.head.appendChild(scriptEl);\n    }\n  }, [instanceGeneralSetting.additionalScript]);\n\n  // Dynamic update metadata with customized profile\n  useEffect(() => {\n    if (!instanceGeneralSetting.customProfile) {\n      return;\n    }\n\n    document.title = instanceGeneralSetting.customProfile.title;\n    const link = document.querySelector(\"link[rel~='icon']\") as HTMLLinkElement;\n    link.href = instanceGeneralSetting.customProfile.logoUrl || \"/logo.webp\";\n  }, [instanceGeneralSetting.customProfile]);\n\n  return (\n    <MemoFilterProvider>\n      <Outlet />\n    </MemoFilterProvider>\n  );\n};\n\nexport default App;\n"
  },
  {
    "path": "web/src/auth-state.ts",
    "content": "// Access token storage using localStorage for persistence across tabs and sessions.\n// Tokens are cleared on logout or expiry.\nlet accessToken: string | null = null;\nlet tokenExpiresAt: Date | null = null;\n\nconst TOKEN_KEY = \"memos_access_token\";\nconst EXPIRES_KEY = \"memos_token_expires_at\";\n\n// BroadcastChannel lets tabs share freshly-refreshed tokens so that only one\n// tab needs to hit the refresh endpoint. When another tab successfully refreshes\n// we adopt the new token immediately, avoiding a redundant (and potentially\n// conflicting) refresh request of our own.\nconst TOKEN_CHANNEL_NAME = \"memos_token_sync\";\n\n// Token refresh policy:\n// - REQUEST_TOKEN_EXPIRY_BUFFER_MS: used for normal API requests.\n// - FOCUS_TOKEN_EXPIRY_BUFFER_MS: used on tab visibility restore to refresh earlier.\nexport const REQUEST_TOKEN_EXPIRY_BUFFER_MS = 30 * 1000;\nexport const FOCUS_TOKEN_EXPIRY_BUFFER_MS = 2 * 60 * 1000;\n\ninterface TokenBroadcastMessage {\n  token: string;\n  expiresAt: string; // ISO string\n}\n\nlet tokenChannel: BroadcastChannel | null = null;\n\nfunction getTokenChannel(): BroadcastChannel | null {\n  if (tokenChannel) return tokenChannel;\n  try {\n    tokenChannel = new BroadcastChannel(TOKEN_CHANNEL_NAME);\n    tokenChannel.onmessage = (event: MessageEvent<TokenBroadcastMessage>) => {\n      const { token, expiresAt } = event.data ?? {};\n      if (token && expiresAt) {\n        // Another tab refreshed — adopt the token in-memory so we don't\n        // fire our own refresh request.\n        accessToken = token;\n        tokenExpiresAt = new Date(expiresAt);\n      }\n    };\n  } catch {\n    // BroadcastChannel not available (e.g. some privacy modes)\n    tokenChannel = null;\n  }\n  return tokenChannel;\n}\n\n// Initialize the channel at module load so the listener is registered\n// before any token refresh can occur in any tab.\ngetTokenChannel();\n\nexport const getAccessToken = (): string | null => {\n  if (!accessToken) {\n    try {\n      const storedToken = localStorage.getItem(TOKEN_KEY);\n      const storedExpires = localStorage.getItem(EXPIRES_KEY);\n\n      if (storedToken && storedExpires) {\n        const expiresAt = new Date(storedExpires);\n        if (expiresAt > new Date()) {\n          accessToken = storedToken;\n          tokenExpiresAt = expiresAt;\n        }\n        // Do NOT remove expired tokens here. Callers such as InstanceContext.initialize()\n        // run concurrently with AuthContext.initialize() via Promise.all. If we eagerly\n        // delete the expired token from localStorage, hasStoredToken() (called synchronously\n        // inside AuthContext.initialize()) finds nothing and skips the refresh attempt,\n        // logging the user out even when the refresh-token cookie is still valid.\n        // clearAccessToken() handles proper cleanup after a confirmed auth failure or logout.\n      }\n    } catch (e) {\n      // localStorage might not be available (e.g., in some privacy modes)\n      console.warn(\"Failed to access localStorage:\", e);\n    }\n  }\n  return accessToken;\n};\n\nexport const setAccessToken = (token: string | null, expiresAt?: Date): void => {\n  accessToken = token;\n  tokenExpiresAt = expiresAt || null;\n\n  try {\n    if (token && expiresAt) {\n      localStorage.setItem(TOKEN_KEY, token);\n      localStorage.setItem(EXPIRES_KEY, expiresAt.toISOString());\n      // Broadcast to other tabs so they adopt the new token without refreshing.\n      const msg: TokenBroadcastMessage = { token, expiresAt: expiresAt.toISOString() };\n      getTokenChannel()?.postMessage(msg);\n    } else {\n      localStorage.removeItem(TOKEN_KEY);\n      localStorage.removeItem(EXPIRES_KEY);\n    }\n  } catch (e) {\n    // localStorage might not be available (e.g., in some privacy modes)\n    console.warn(\"Failed to write to localStorage:\", e);\n  }\n};\n\nexport const isTokenExpired = (bufferMs: number = REQUEST_TOKEN_EXPIRY_BUFFER_MS): boolean => {\n  if (!tokenExpiresAt) return true;\n  // Consider expired with a safety buffer before actual expiry.\n  return new Date() >= new Date(tokenExpiresAt.getTime() - bufferMs);\n};\n\n// Returns true if a token exists in localStorage, even if it is expired.\n// Used to decide whether to attempt GetCurrentUser on app init — if no token\n// was ever stored, the user is definitively not logged in and there is nothing\n// to refresh, so we can skip the network round-trip entirely.\nexport const hasStoredToken = (): boolean => {\n  if (accessToken) return true;\n  try {\n    return !!localStorage.getItem(TOKEN_KEY);\n  } catch {\n    return false;\n  }\n};\n\nexport const clearAccessToken = (): void => {\n  accessToken = null;\n  tokenExpiresAt = null;\n\n  try {\n    localStorage.removeItem(TOKEN_KEY);\n    localStorage.removeItem(EXPIRES_KEY);\n  } catch (e) {\n    console.warn(\"Failed to clear localStorage:\", e);\n  }\n};\n"
  },
  {
    "path": "web/src/components/ActivityCalendar/CalendarCell.tsx",
    "content": "import { memo } from \"react\";\nimport { Tooltip, TooltipContent, TooltipTrigger } from \"@/components/ui/tooltip\";\nimport { cn } from \"@/lib/utils\";\nimport { DEFAULT_CELL_SIZE, SMALL_CELL_SIZE } from \"./constants\";\nimport type { CalendarDayCell, CalendarSize } from \"./types\";\nimport { getCellIntensityClass } from \"./utils\";\n\nexport interface CalendarCellProps {\n  day: CalendarDayCell;\n  maxCount: number;\n  tooltipText: string;\n  onClick?: (date: string) => void;\n  size?: CalendarSize;\n  disableTooltip?: boolean;\n}\n\nexport const CalendarCell = memo((props: CalendarCellProps) => {\n  const { day, maxCount, tooltipText, onClick, size = \"default\", disableTooltip = false } = props;\n\n  const handleClick = () => {\n    if (day.count > 0 && onClick) {\n      onClick(day.date);\n    }\n  };\n\n  const sizeConfig = size === \"small\" ? SMALL_CELL_SIZE : DEFAULT_CELL_SIZE;\n  const smallExtraClasses = size === \"small\" ? `${SMALL_CELL_SIZE.dimensions} min-h-0` : \"\";\n\n  const baseClasses = cn(\n    \"aspect-square w-full flex items-center justify-center text-center transition-all duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/40 focus-visible:ring-offset-2 select-none border border-border/10 bg-muted/20\",\n    sizeConfig.font,\n    sizeConfig.borderRadius,\n    smallExtraClasses,\n  );\n  const isInteractive = Boolean(onClick && day.count > 0);\n  const ariaLabel = day.isSelected ? `${tooltipText} (selected)` : tooltipText;\n\n  if (!day.isCurrentMonth) {\n    return <div className={cn(baseClasses, \"text-muted-foreground/30 bg-transparent border-transparent cursor-default\")}>{day.label}</div>;\n  }\n\n  const intensityClass = getCellIntensityClass(day, maxCount);\n\n  const buttonClasses = cn(\n    baseClasses,\n    intensityClass,\n    day.isToday && \"ring-2 ring-primary/30 ring-offset-1 font-semibold z-10\",\n    day.isSelected && \"ring-2 ring-primary ring-offset-1 font-bold z-10\",\n    isInteractive ? \"cursor-pointer hover:bg-muted/40 hover:border-border/30\" : \"cursor-default\",\n  );\n\n  const button = (\n    <button\n      type=\"button\"\n      onClick={handleClick}\n      tabIndex={isInteractive ? 0 : -1}\n      aria-label={ariaLabel}\n      aria-current={day.isToday ? \"date\" : undefined}\n      aria-disabled={!isInteractive}\n      className={buttonClasses}\n    >\n      {day.label}\n    </button>\n  );\n\n  const shouldShowTooltip = tooltipText && day.count > 0 && !disableTooltip;\n\n  if (!shouldShowTooltip) {\n    return button;\n  }\n\n  return (\n    <Tooltip>\n      <TooltipTrigger asChild>{button}</TooltipTrigger>\n      <TooltipContent side=\"top\">\n        <p>{tooltipText}</p>\n      </TooltipContent>\n    </Tooltip>\n  );\n});\n\nCalendarCell.displayName = \"CalendarCell\";\n"
  },
  {
    "path": "web/src/components/ActivityCalendar/MonthCalendar.tsx",
    "content": "import { memo, useMemo } from \"react\";\nimport { useInstance } from \"@/contexts/InstanceContext\";\nimport { cn } from \"@/lib/utils\";\nimport { useTranslate } from \"@/utils/i18n\";\nimport { CalendarCell } from \"./CalendarCell\";\nimport { useTodayDate, useWeekdayLabels } from \"./hooks\";\nimport type { CalendarSize, MonthCalendarProps } from \"./types\";\nimport { useCalendarMatrix } from \"./useCalendar\";\nimport { getTooltipText } from \"./utils\";\n\nconst GRID_STYLES: Record<CalendarSize, { gap: string; headerText: string }> = {\n  small: { gap: \"gap-1.5\", headerText: \"text-[10px]\" },\n  default: { gap: \"gap-2\", headerText: \"text-xs\" },\n};\n\ninterface WeekdayHeaderProps {\n  weekDays: string[];\n  size: CalendarSize;\n}\n\nconst WeekdayHeader = memo(({ weekDays, size }: WeekdayHeaderProps) => (\n  <div className={cn(\"grid grid-cols-7 mb-1\", GRID_STYLES[size].gap, GRID_STYLES[size].headerText)} role=\"row\">\n    {weekDays.map((label, index) => (\n      <div\n        key={index}\n        className=\"flex h-4 items-center justify-center font-medium uppercase tracking-wide text-muted-foreground/60\"\n        role=\"columnheader\"\n        aria-label={label}\n      >\n        {label}\n      </div>\n    ))}\n  </div>\n));\nWeekdayHeader.displayName = \"WeekdayHeader\";\n\nexport const MonthCalendar = memo((props: MonthCalendarProps) => {\n  const { month, data, maxCount, size = \"default\", onClick, className, disableTooltips = false } = props;\n  const t = useTranslate();\n  const { generalSetting } = useInstance();\n  const today = useTodayDate();\n  const weekDays = useWeekdayLabels();\n\n  const { weeks, weekDays: rotatedWeekDays } = useCalendarMatrix({\n    month,\n    data,\n    weekDays,\n    weekStartDayOffset: generalSetting.weekStartDayOffset,\n    today,\n    selectedDate: \"\",\n  });\n\n  const flatDays = useMemo(() => weeks.flatMap((week) => week.days), [weeks]);\n\n  return (\n    <div className={cn(\"flex flex-col\", className)} role=\"grid\" aria-label={`Calendar for ${month}`}>\n      <WeekdayHeader weekDays={rotatedWeekDays} size={size} />\n\n      <div className={cn(\"grid grid-cols-7\", GRID_STYLES[size].gap)} role=\"rowgroup\">\n        {flatDays.map((day) => (\n          <CalendarCell\n            key={day.date}\n            day={day}\n            maxCount={maxCount}\n            tooltipText={getTooltipText(day.count, day.date, t)}\n            onClick={onClick}\n            size={size}\n            disableTooltip={disableTooltips}\n          />\n        ))}\n      </div>\n    </div>\n  );\n});\n\nMonthCalendar.displayName = \"MonthCalendar\";\n"
  },
  {
    "path": "web/src/components/ActivityCalendar/YearCalendar.tsx",
    "content": "import { ChevronLeftIcon, ChevronRightIcon } from \"lucide-react\";\nimport { memo, useMemo } from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport { cn } from \"@/lib/utils\";\nimport { useTranslate } from \"@/utils/i18n\";\nimport { getMaxYear, MIN_YEAR } from \"./constants\";\nimport { MonthCalendar } from \"./MonthCalendar\";\nimport type { YearCalendarProps } from \"./types\";\nimport { calculateYearMaxCount, filterDataByYear, generateMonthsForYear, getMonthLabel } from \"./utils\";\n\ninterface YearNavigationProps {\n  selectedYear: number;\n  currentYear: number;\n  onPrev: () => void;\n  onNext: () => void;\n  onToday: () => void;\n  canGoPrev: boolean;\n  canGoNext: boolean;\n}\n\nconst YearNavigation = memo(({ selectedYear, currentYear, onPrev, onNext, onToday, canGoPrev, canGoNext }: YearNavigationProps) => {\n  const t = useTranslate();\n  const isCurrentYear = selectedYear === currentYear;\n\n  return (\n    <div className=\"flex items-center justify-between px-1\">\n      <h2 className=\"text-2xl font-semibold text-foreground tracking-tight\">{selectedYear}</h2>\n\n      <nav className=\"inline-flex items-center gap-0.5 rounded-lg border border-border/30 bg-muted/10 p-0.5\" aria-label=\"Year navigation\">\n        <Button\n          variant=\"ghost\"\n          size=\"sm\"\n          onClick={onPrev}\n          disabled={!canGoPrev}\n          aria-label=\"Previous year\"\n          className=\"h-7 w-7 p-0 rounded-md text-muted-foreground hover:text-foreground hover:bg-muted/40\"\n        >\n          <ChevronLeftIcon className=\"w-4 h-4\" />\n        </Button>\n\n        <Button\n          variant=\"ghost\"\n          size=\"sm\"\n          onClick={onToday}\n          disabled={isCurrentYear}\n          aria-label={t(\"common.today\")}\n          className={cn(\n            \"h-7 px-2.5 rounded-md text-[10px] font-medium uppercase tracking-wider\",\n            isCurrentYear ? \"text-muted-foreground/50 cursor-default\" : \"text-muted-foreground hover:text-foreground hover:bg-muted/40\",\n          )}\n        >\n          {t(\"common.today\")}\n        </Button>\n\n        <Button\n          variant=\"ghost\"\n          size=\"sm\"\n          onClick={onNext}\n          disabled={!canGoNext}\n          aria-label=\"Next year\"\n          className=\"h-7 w-7 p-0 rounded-md text-muted-foreground hover:text-foreground hover:bg-muted/40\"\n        >\n          <ChevronRightIcon className=\"w-4 h-4\" />\n        </Button>\n      </nav>\n    </div>\n  );\n});\nYearNavigation.displayName = \"YearNavigation\";\n\ninterface MonthCardProps {\n  month: string;\n  data: Record<string, number>;\n  maxCount: number;\n  onDateClick: (date: string) => void;\n}\n\nconst MonthCard = memo(({ month, data, maxCount, onDateClick }: MonthCardProps) => (\n  <article className=\"flex flex-col gap-2 rounded-xl border border-border/20 bg-muted/5 p-3 transition-colors hover:bg-muted/10\">\n    <header className=\"text-[10px] font-medium text-muted-foreground/80 uppercase tracking-widest\">{getMonthLabel(month)}</header>\n    <MonthCalendar month={month} data={data} maxCount={maxCount} size=\"small\" onClick={onDateClick} disableTooltips />\n  </article>\n));\nMonthCard.displayName = \"MonthCard\";\n\nexport const YearCalendar = memo(({ selectedYear, data, onYearChange, onDateClick, className }: YearCalendarProps) => {\n  const currentYear = useMemo(() => new Date().getFullYear(), []);\n  const yearData = useMemo(() => filterDataByYear(data, selectedYear), [data, selectedYear]);\n  const months = useMemo(() => generateMonthsForYear(selectedYear), [selectedYear]);\n  const yearMaxCount = useMemo(() => calculateYearMaxCount(yearData), [yearData]);\n\n  const canGoPrev = selectedYear > MIN_YEAR;\n  const canGoNext = selectedYear < getMaxYear();\n\n  return (\n    <section className={cn(\"w-full flex flex-col gap-5 px-4 py-4 select-none\", className)} aria-label={`Year ${selectedYear} calendar`}>\n      <YearNavigation\n        selectedYear={selectedYear}\n        currentYear={currentYear}\n        onPrev={() => canGoPrev && onYearChange(selectedYear - 1)}\n        onNext={() => canGoNext && onYearChange(selectedYear + 1)}\n        onToday={() => onYearChange(currentYear)}\n        canGoPrev={canGoPrev}\n        canGoNext={canGoNext}\n      />\n\n      <div className=\"grid gap-4 grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 animate-fade-in\">\n        {months.map((month) => (\n          <MonthCard key={month} month={month} data={yearData} maxCount={yearMaxCount} onDateClick={onDateClick} />\n        ))}\n      </div>\n    </section>\n  );\n});\n\nYearCalendar.displayName = \"YearCalendar\";\n"
  },
  {
    "path": "web/src/components/ActivityCalendar/constants.ts",
    "content": "export const DAYS_IN_WEEK = 7;\nexport const MONTHS_IN_YEAR = 12;\nexport const WEEKEND_DAYS = [0, 6] as const;\nexport const MIN_COUNT = 1;\n\nexport const MIN_YEAR = 1970;\nexport const getMaxYear = () => new Date().getFullYear() + 1;\n\nexport const INTENSITY_THRESHOLDS = {\n  HIGH: 0.75,\n  MEDIUM: 0.5,\n  LOW: 0.25,\n  MINIMAL: 0,\n} as const;\n\nexport const CELL_STYLES = {\n  HIGH: \"bg-primary text-primary-foreground shadow-sm border-transparent\",\n  MEDIUM: \"bg-primary/85 text-primary-foreground shadow-sm border-transparent\",\n  LOW: \"bg-primary/70 text-primary-foreground border-transparent\",\n  MINIMAL: \"bg-primary/50 text-foreground border-transparent\",\n  EMPTY: \"bg-muted/20 text-muted-foreground hover:bg-muted/30 border-border/10\",\n} as const;\n\nexport const SMALL_CELL_SIZE = {\n  font: \"text-[11px]\",\n  dimensions: \"w-full h-full\",\n  borderRadius: \"rounded-lg\",\n  gap: \"gap-1.5\",\n} as const;\n\nexport const DEFAULT_CELL_SIZE = {\n  font: \"text-xs\",\n  borderRadius: \"rounded-lg\",\n  gap: \"gap-2\",\n} as const;\n"
  },
  {
    "path": "web/src/components/ActivityCalendar/hooks.ts",
    "content": "import dayjs from \"dayjs\";\nimport { useMemo } from \"react\";\nimport { useTranslate } from \"@/utils/i18n\";\n\nexport const useWeekdayLabels = () => {\n  const t = useTranslate();\n  return useMemo(\n    () => [\n      t(\"common.days.sun\"),\n      t(\"common.days.mon\"),\n      t(\"common.days.tue\"),\n      t(\"common.days.wed\"),\n      t(\"common.days.thu\"),\n      t(\"common.days.fri\"),\n      t(\"common.days.sat\"),\n    ],\n    [t],\n  );\n};\n\nexport const useTodayDate = () => {\n  return dayjs().format(\"YYYY-MM-DD\");\n};\n"
  },
  {
    "path": "web/src/components/ActivityCalendar/index.ts",
    "content": "export * from \"./MonthCalendar\";\nexport * from \"./types\";\nexport * from \"./utils\";\nexport * from \"./YearCalendar\";\n"
  },
  {
    "path": "web/src/components/ActivityCalendar/types.ts",
    "content": "export type CalendarSize = \"default\" | \"small\";\n\nexport interface CalendarDayCell {\n  date: string;\n  label: number;\n  count: number;\n  isCurrentMonth: boolean;\n  isToday: boolean;\n  isSelected: boolean;\n  isWeekend: boolean;\n}\n\nexport interface CalendarDayRow {\n  days: CalendarDayCell[];\n}\n\nexport interface CalendarMatrixResult {\n  weeks: CalendarDayRow[];\n  weekDays: string[];\n  maxCount: number;\n}\n\nexport interface MonthCalendarProps {\n  month: string;\n  data: Record<string, number>;\n  maxCount: number;\n  size?: CalendarSize;\n  onClick?: (date: string) => void;\n  className?: string;\n  disableTooltips?: boolean;\n}\n\nexport interface YearCalendarProps {\n  selectedYear: number;\n  data: Record<string, number>;\n  onYearChange: (year: number) => void;\n  onDateClick: (date: string) => void;\n  className?: string;\n}\n"
  },
  {
    "path": "web/src/components/ActivityCalendar/useCalendar.ts",
    "content": "import dayjs from \"dayjs\";\nimport { useMemo } from \"react\";\nimport { DAYS_IN_WEEK, MIN_COUNT, WEEKEND_DAYS } from \"./constants\";\nimport type { CalendarDayCell, CalendarMatrixResult } from \"./types\";\n\nexport interface UseCalendarMatrixParams {\n  month: string;\n  data: Record<string, number>;\n  weekDays: string[];\n  weekStartDayOffset: number;\n  today: string;\n  selectedDate: string;\n}\n\nconst createCalendarDayCell = (\n  current: dayjs.Dayjs,\n  monthKey: string,\n  data: Record<string, number>,\n  today: string,\n  selectedDate: string,\n): CalendarDayCell => {\n  const isoDate = current.format(\"YYYY-MM-DD\");\n  const isCurrentMonth = current.format(\"YYYY-MM\") === monthKey;\n  const count = data[isoDate] ?? 0;\n\n  return {\n    date: isoDate,\n    label: current.date(),\n    count,\n    isCurrentMonth,\n    isToday: isoDate === today,\n    isSelected: isoDate === selectedDate,\n    isWeekend: WEEKEND_DAYS.includes(current.day() as 0 | 6),\n  };\n};\n\nconst calculateCalendarBoundaries = (monthStart: dayjs.Dayjs, weekStartDayOffset: number) => {\n  const monthEnd = monthStart.endOf(\"month\");\n  const startOffset = (monthStart.day() - weekStartDayOffset + DAYS_IN_WEEK) % DAYS_IN_WEEK;\n  const endOffset = (weekStartDayOffset + (DAYS_IN_WEEK - 1) - monthEnd.day() + DAYS_IN_WEEK) % DAYS_IN_WEEK;\n  const calendarStart = monthStart.subtract(startOffset, \"day\");\n  const calendarEnd = monthEnd.add(endOffset, \"day\");\n  const dayCount = calendarEnd.diff(calendarStart, \"day\") + 1;\n\n  return { calendarStart, dayCount };\n};\n\n/**\n * Generates a matrix of calendar days for a given month, handling week alignment and data mapping.\n */\nexport const useCalendarMatrix = ({\n  month,\n  data,\n  weekDays,\n  weekStartDayOffset,\n  today,\n  selectedDate,\n}: UseCalendarMatrixParams): CalendarMatrixResult => {\n  return useMemo(() => {\n    // Determine the start of the month and its formatted key (YYYY-MM)\n    const monthStart = dayjs(month).startOf(\"month\");\n    const monthKey = monthStart.format(\"YYYY-MM\");\n\n    // Rotate week labels based on the user's preferred start of the week\n    const rotatedWeekDays = weekDays.slice(weekStartDayOffset).concat(weekDays.slice(0, weekStartDayOffset));\n\n    // Calculate the start and end dates for the calendar grid to ensure full weeks\n    const { calendarStart, dayCount } = calculateCalendarBoundaries(monthStart, weekStartDayOffset);\n\n    const weeks: CalendarMatrixResult[\"weeks\"] = [];\n    let maxCount = 0;\n\n    // Iterate through each day in the calendar grid\n    for (let index = 0; index < dayCount; index += 1) {\n      const current = calendarStart.add(index, \"day\");\n      const weekIndex = Math.floor(index / DAYS_IN_WEEK);\n\n      if (!weeks[weekIndex]) {\n        weeks[weekIndex] = { days: [] };\n      }\n\n      // Create the day cell object with data and status flags\n      const dayCell = createCalendarDayCell(current, monthKey, data, today, selectedDate);\n      weeks[weekIndex].days.push(dayCell);\n      maxCount = Math.max(maxCount, dayCell.count);\n    }\n\n    return {\n      weeks,\n      weekDays: rotatedWeekDays,\n      maxCount: Math.max(maxCount, MIN_COUNT),\n    };\n  }, [month, data, weekDays, weekStartDayOffset, today, selectedDate]);\n};\n"
  },
  {
    "path": "web/src/components/ActivityCalendar/utils.ts",
    "content": "import dayjs from \"dayjs\";\nimport isSameOrAfter from \"dayjs/plugin/isSameOrAfter\";\nimport isSameOrBefore from \"dayjs/plugin/isSameOrBefore\";\nimport { useTranslate } from \"@/utils/i18n\";\nimport { CELL_STYLES, INTENSITY_THRESHOLDS, MIN_COUNT, MONTHS_IN_YEAR } from \"./constants\";\nimport type { CalendarDayCell } from \"./types\";\n\ndayjs.extend(isSameOrAfter);\ndayjs.extend(isSameOrBefore);\n\nexport type TranslateFunction = ReturnType<typeof useTranslate>;\n\nexport const getCellIntensityClass = (day: CalendarDayCell, maxCount: number): string => {\n  if (!day.isCurrentMonth || day.count === 0) {\n    return CELL_STYLES.EMPTY;\n  }\n\n  const ratio = day.count / maxCount;\n  if (ratio > INTENSITY_THRESHOLDS.HIGH) return CELL_STYLES.HIGH;\n  if (ratio > INTENSITY_THRESHOLDS.MEDIUM) return CELL_STYLES.MEDIUM;\n  if (ratio > INTENSITY_THRESHOLDS.LOW) return CELL_STYLES.LOW;\n  return CELL_STYLES.MINIMAL;\n};\n\nexport const generateMonthsForYear = (year: number): string[] => {\n  return Array.from({ length: MONTHS_IN_YEAR }, (_, i) => dayjs(`${year}-01-01`).add(i, \"month\").format(\"YYYY-MM\"));\n};\n\nexport const calculateYearMaxCount = (data: Record<string, number>): number => {\n  let max = 0;\n  for (const count of Object.values(data)) {\n    max = Math.max(max, count);\n  }\n  return Math.max(max, MIN_COUNT);\n};\n\nexport const getMonthLabel = (month: string): string => {\n  return dayjs(month).format(\"MMM\");\n};\n\nexport const filterDataByYear = (data: Record<string, number>, year: number): Record<string, number> => {\n  if (!data) return {};\n\n  const filtered: Record<string, number> = {};\n  const yearStart = dayjs(`${year}-01-01`);\n  const yearEnd = dayjs(`${year}-12-31`);\n\n  for (const [dateStr, count] of Object.entries(data)) {\n    const date = dayjs(dateStr);\n    if (date.isSameOrAfter(yearStart, \"day\") && date.isSameOrBefore(yearEnd, \"day\")) {\n      filtered[dateStr] = count;\n    }\n  }\n\n  return filtered;\n};\n\nexport const hasActivityData = (data: Record<string, number>): boolean => {\n  return Object.values(data).some((count) => count > 0);\n};\n\nexport const getTooltipText = (count: number, date: string, t: TranslateFunction): string => {\n  if (count === 0) {\n    return date;\n  }\n\n  return t(\"memo.count-memos-in-date\", {\n    count,\n    memos: count === 1 ? t(\"common.memo\") : t(\"common.memos\"),\n    date,\n  }).toLowerCase();\n};\n"
  },
  {
    "path": "web/src/components/AttachmentIcon.tsx",
    "content": "import {\n  BinaryIcon,\n  BookIcon,\n  FileArchiveIcon,\n  FileAudioIcon,\n  FileEditIcon,\n  FileIcon,\n  FileTextIcon,\n  FileVideo2Icon,\n  SheetIcon,\n} from \"lucide-react\";\nimport React, { useState } from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport { Attachment } from \"@/types/proto/api/v1/attachment_service_pb\";\nimport { getAttachmentThumbnailUrl, getAttachmentType, getAttachmentUrl } from \"@/utils/attachment\";\nimport SquareDiv from \"./kit/SquareDiv\";\nimport PreviewImageDialog from \"./PreviewImageDialog\";\n\ninterface Props {\n  attachment: Attachment;\n  className?: string;\n  strokeWidth?: number;\n}\n\nconst AttachmentIcon = (props: Props) => {\n  const { attachment } = props;\n  const [previewImage, setPreviewImage] = useState<{ open: boolean; urls: string[]; index: number }>({\n    open: false,\n    urls: [],\n    index: 0,\n  });\n  const resourceType = getAttachmentType(attachment);\n  const attachmentUrl = getAttachmentUrl(attachment);\n  const className = cn(\"w-full h-auto\", props.className);\n  const strokeWidth = props.strokeWidth;\n\n  const previewResource = () => {\n    window.open(attachmentUrl);\n  };\n\n  const handleImageClick = () => {\n    setPreviewImage({ open: true, urls: [attachmentUrl], index: 0 });\n  };\n\n  if (resourceType === \"image/*\") {\n    return (\n      <>\n        <SquareDiv className={cn(className, \"flex items-center justify-center overflow-clip\")}>\n          <img\n            className=\"min-w-full min-h-full object-cover\"\n            src={getAttachmentThumbnailUrl(attachment)}\n            onClick={handleImageClick}\n            onError={(e) => {\n              // Fallback to original image if thumbnail fails\n              const target = e.target as HTMLImageElement;\n              if (target.src.includes(\"?thumbnail=true\")) {\n                console.warn(\"Thumbnail failed, falling back to original image:\", attachmentUrl);\n                target.src = attachmentUrl;\n              }\n            }}\n            decoding=\"async\"\n            loading=\"lazy\"\n          />\n        </SquareDiv>\n\n        <PreviewImageDialog\n          open={previewImage.open}\n          onOpenChange={(open) => setPreviewImage((prev) => ({ ...prev, open }))}\n          imgUrls={previewImage.urls}\n          initialIndex={previewImage.index}\n        />\n      </>\n    );\n  }\n\n  const getAttachmentIcon = () => {\n    switch (resourceType) {\n      case \"video/*\":\n        return <FileVideo2Icon strokeWidth={strokeWidth} className=\"w-full h-auto\" />;\n      case \"audio/*\":\n        return <FileAudioIcon strokeWidth={strokeWidth} className=\"w-full h-auto\" />;\n      case \"text/*\":\n        return <FileTextIcon strokeWidth={strokeWidth} className=\"w-full h-auto\" />;\n      case \"application/epub+zip\":\n        return <BookIcon strokeWidth={strokeWidth} className=\"w-full h-auto\" />;\n      case \"application/pdf\":\n        return <BookIcon strokeWidth={strokeWidth} className=\"w-full h-auto\" />;\n      case \"application/msword\":\n        return <FileEditIcon strokeWidth={strokeWidth} className=\"w-full h-auto\" />;\n      case \"application/msexcel\":\n        return <SheetIcon strokeWidth={strokeWidth} className=\"w-full h-auto\" />;\n      case \"application/zip\":\n        return <FileArchiveIcon onClick={previewResource} strokeWidth={strokeWidth} className=\"w-full h-auto\" />;\n      case \"application/x-java-archive\":\n        return <BinaryIcon strokeWidth={strokeWidth} className=\"w-full h-auto\" />;\n      default:\n        return <FileIcon strokeWidth={strokeWidth} className=\"w-full h-auto\" />;\n    }\n  };\n\n  return (\n    <div onClick={previewResource} className={cn(className, \"max-w-16 opacity-50\")}>\n      {getAttachmentIcon()}\n    </div>\n  );\n};\n\nexport default React.memo(AttachmentIcon);\n"
  },
  {
    "path": "web/src/components/AuthFooter.tsx",
    "content": "import { useState } from \"react\";\nimport { useTranslation } from \"react-i18next\";\nimport { cn } from \"@/lib/utils\";\nimport { loadLocale } from \"@/utils/i18n\";\nimport { getInitialTheme, loadTheme, Theme } from \"@/utils/theme\";\nimport LocaleSelect from \"./LocaleSelect\";\nimport ThemeSelect from \"./ThemeSelect\";\n\ninterface Props {\n  className?: string;\n}\n\nconst AuthFooter = ({ className }: Props) => {\n  const { i18n: i18nInstance } = useTranslation();\n  const currentLocale = i18nInstance.language as Locale;\n  const [currentTheme, setCurrentTheme] = useState(getInitialTheme());\n\n  const handleLocaleChange = (locale: Locale) => {\n    loadLocale(locale);\n  };\n\n  const handleThemeChange = (theme: string) => {\n    loadTheme(theme);\n    setCurrentTheme(theme as Theme);\n  };\n\n  return (\n    <div className={cn(\"mt-4 flex flex-row items-center justify-center w-full gap-2\", className)}>\n      <LocaleSelect value={currentLocale} onChange={handleLocaleChange} />\n      <ThemeSelect value={currentTheme} onValueChange={handleThemeChange} />\n    </div>\n  );\n};\n\nexport default AuthFooter;\n"
  },
  {
    "path": "web/src/components/ChangeMemberPasswordDialog.tsx",
    "content": "import { useState } from \"react\";\nimport { toast } from \"react-hot-toast\";\nimport { Button } from \"@/components/ui/button\";\nimport { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from \"@/components/ui/dialog\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport { useUpdateUser } from \"@/hooks/useUserQueries\";\nimport { handleError } from \"@/lib/error\";\nimport { User } from \"@/types/proto/api/v1/user_service_pb\";\nimport { useTranslate } from \"@/utils/i18n\";\n\ninterface Props {\n  open: boolean;\n  onOpenChange: (open: boolean) => void;\n  user?: User;\n  onSuccess?: () => void;\n}\n\nfunction ChangeMemberPasswordDialog({ open, onOpenChange, user, onSuccess }: Props) {\n  const t = useTranslate();\n  const { mutateAsync: updateUser } = useUpdateUser();\n  const [newPassword, setNewPassword] = useState(\"\");\n  const [newPasswordAgain, setNewPasswordAgain] = useState(\"\");\n\n  const handleCloseBtnClick = () => {\n    onOpenChange(false);\n  };\n\n  const handleNewPasswordChanged = (e: React.ChangeEvent<HTMLInputElement>) => {\n    const text = e.target.value as string;\n    setNewPassword(text);\n  };\n\n  const handleNewPasswordAgainChanged = (e: React.ChangeEvent<HTMLInputElement>) => {\n    const text = e.target.value as string;\n    setNewPasswordAgain(text);\n  };\n\n  const handleSaveBtnClick = async () => {\n    if (!user) return;\n\n    if (newPassword === \"\" || newPasswordAgain === \"\") {\n      toast.error(t(\"message.fill-all\"));\n      return;\n    }\n\n    if (newPassword !== newPasswordAgain) {\n      toast.error(t(\"message.new-password-not-match\"));\n      setNewPasswordAgain(\"\");\n      return;\n    }\n\n    try {\n      await updateUser({\n        user: {\n          name: user.name,\n          password: newPassword,\n        },\n        updateMask: [\"password\"],\n      });\n      toast(t(\"message.password-changed\"));\n      onSuccess?.();\n      onOpenChange(false);\n    } catch (error: unknown) {\n      await handleError(error, toast.error, {\n        context: \"Change member password\",\n      });\n    }\n  };\n\n  if (!user) return null;\n\n  return (\n    <Dialog open={open} onOpenChange={onOpenChange}>\n      <DialogContent className=\"max-w-md\">\n        <DialogHeader>\n          <DialogTitle>\n            {t(\"setting.account.change-password\")} ({user.displayName})\n          </DialogTitle>\n        </DialogHeader>\n        <div className=\"flex flex-col gap-4\">\n          <div className=\"grid gap-2\">\n            <Label htmlFor=\"newPassword\">{t(\"auth.new-password\")}</Label>\n            <Input\n              id=\"newPassword\"\n              type=\"password\"\n              placeholder={t(\"auth.new-password\")}\n              value={newPassword}\n              onChange={handleNewPasswordChanged}\n            />\n          </div>\n          <div className=\"grid gap-2\">\n            <Label htmlFor=\"newPasswordAgain\">{t(\"auth.repeat-new-password\")}</Label>\n            <Input\n              id=\"newPasswordAgain\"\n              type=\"password\"\n              placeholder={t(\"auth.repeat-new-password\")}\n              value={newPasswordAgain}\n              onChange={handleNewPasswordAgainChanged}\n            />\n          </div>\n        </div>\n        <DialogFooter>\n          <Button variant=\"ghost\" onClick={handleCloseBtnClick}>\n            {t(\"common.cancel\")}\n          </Button>\n          <Button onClick={handleSaveBtnClick}>{t(\"common.save\")}</Button>\n        </DialogFooter>\n      </DialogContent>\n    </Dialog>\n  );\n}\n\nexport default ChangeMemberPasswordDialog;\n"
  },
  {
    "path": "web/src/components/ConfirmDialog/README.md",
    "content": "# ConfirmDialog - Accessible Confirmation Dialog\n\n## Overview\n\n`ConfirmDialog` standardizes confirmation flows across the app. It replaces ad‑hoc `window.confirm` usage with an accessible, themeable dialog that supports asynchronous operations.\n\n## Key Features\n\n### 1. Accessibility & UX\n- Uses shared `Dialog` primitives (focus trap, ARIA roles)\n- Blocks dismissal while async confirm is pending\n- Clear separation of title (action) vs description (context)\n\n### 2. Async-Aware\n- Accepts sync or async `onConfirm`\n- Auto-closes on resolve; remains open on error for retry / toast\n\n### 3. Internationalization Ready\n- All labels / text provided by caller through i18n hook\n- Supports interpolation for dynamic context\n\n### 4. Minimal Surface, Easy Extension\n- Lightweight API (few required props)\n- Style hook via `.container` class (SCSS module)\n\n## Architecture\n\n```\nConfirmDialog\n├── State: loading (tracks pending confirm action)\n├── Dialog primitives: Header (title + description), Footer (buttons)\n└── External control: parent owns open state via onOpenChange\n```\n\n## Usage\n\n```tsx\nimport { useTranslate } from \"@/utils/i18n\";\nimport ConfirmDialog from \"@/components/ConfirmDialog\";\n\nconst t = useTranslate();\n\n<ConfirmDialog\n  open={open}\n  onOpenChange={setOpen}\n  title={t(\"memo.delete-confirm\")}\n  description={t(\"memo.delete-confirm-description\")}\n  confirmLabel={t(\"common.delete\")}\n  cancelLabel={t(\"common.cancel\")}\n  onConfirm={handleDelete}\n  confirmVariant=\"destructive\"\n/>;\n```\n\n## Props\n\n| Prop | Type | Required | Acceptable Values |\n|------|------|----------|------------------|\n| `open` | `boolean` | Yes | `true` (visible) / `false` (hidden) |\n| `onOpenChange` | `(open: boolean) => void` | Yes | Callback receiving next state; should update parent state |\n| `title` | `React.ReactNode` | Yes | Short localized action summary (text / node) |\n| `description` | `React.ReactNode` | No | Optional contextual message |\n| `confirmLabel` | `string` | Yes | Non-empty localized action text (1–2 words) |\n| `cancelLabel` | `string` | Yes | Localized cancel label |\n| `onConfirm` | `() => void | Promise<void>` | Yes | Sync or async handler; resolve = close, reject = stay open |\n| `confirmVariant` | `\"default\" | \"destructive\"` | No | Defaults to `\"default\"`; use `\"destructive\"` for irreversible actions |\n\n## Benefits vs Previous Implementation\n\n### Before (window.confirm / ad‑hoc dialogs)\n- Blocking native prompt, inconsistent styling\n- No async progress handling\n- No rich formatting\n- Hard to localize consistently\n\n### After (ConfirmDialog)\n- Unified styling + accessibility semantics\n- Async-safe with loading state shielding\n- Plain description flexibility\n- i18n-first via externalized labels\n\n## Technical Implementation Details\n\n### Async Handling\n```tsx\nconst handleConfirm = async () => {\n  setLoading(true);\n  try {\n    await onConfirm(); // resolve -> close\n    onOpenChange(false);\n  } catch (e) {\n    console.error(e); // remain open for retry\n  } finally {\n    setLoading(false);\n  }\n};\n```\n\n### Close Guard\n```tsx\n<Dialog open={open} onOpenChange={(next) => !loading && onOpenChange(next)} />\n```\n\n## Browser / Environment Support\n- Works anywhere the existing `Dialog` primitives work (modern browsers)\n- No ResizeObserver / layout dependencies\n\n## Performance Considerations\n1. Minimal renders: loading state toggles once per confirm attempt\n2. No portal churn—relies on underlying dialog infra\n\n## Future Enhancements\n1. Severity icon / header accent\n2. Auto-focus destructive button toggle\n3. Secondary action (e.g. \"Archive\" vs \"Delete\")\n4. Built-in retry / error slot\n5. Optional checkbox confirmation (\"I understand the consequences\")\n6. Motion/animation tokens integration\n\n## Styling\nThe `ConfirmDialog.module.scss` file provides a `.container` hook. It currently only hosts a harmless custom property so the stylesheet is non-empty. Add real layout or variant tokens there instead of inline styles.\n\n## Internationalization\nAll visible strings must come from the translation system. Use `useTranslate()` and pass localized values into props. Separate keys for title/description.\n\n## Error Handling\nErrors thrown in `onConfirm` are caught and logged. The dialog stays open so the caller can surface a toast or inline message and allow retry. (Consider routing serious errors to a higher-level handler.)\n\n---\n\nIf you extend this component, update this README to keep usage discoverable.\n"
  },
  {
    "path": "web/src/components/ConfirmDialog/index.tsx",
    "content": "import * as React from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from \"@/components/ui/dialog\";\n\nexport interface ConfirmDialogProps {\n  open: boolean;\n  onOpenChange: (open: boolean) => void;\n  title: React.ReactNode;\n  description?: React.ReactNode;\n  confirmLabel: string;\n  cancelLabel: string;\n  onConfirm: () => void | Promise<void>;\n  confirmVariant?: \"default\" | \"destructive\";\n}\n\nexport default function ConfirmDialog({\n  open,\n  onOpenChange,\n  title,\n  description,\n  confirmLabel,\n  cancelLabel,\n  onConfirm,\n  confirmVariant = \"default\",\n}: ConfirmDialogProps) {\n  const [loading, setLoading] = React.useState(false);\n\n  const handleConfirm = async () => {\n    try {\n      setLoading(true);\n      await onConfirm();\n      onOpenChange(false);\n    } catch (e) {\n      // Intentionally swallow errors so user can retry; surface via caller's toast/logging\n      console.error(\"ConfirmDialog error for action:\", title, e);\n    } finally {\n      setLoading(false);\n    }\n  };\n\n  return (\n    <Dialog open={open} onOpenChange={(o: boolean) => !loading && onOpenChange(o)}>\n      <DialogContent size=\"sm\">\n        <DialogHeader>\n          <DialogTitle>{title}</DialogTitle>\n          {description ? <DialogDescription>{description}</DialogDescription> : null}\n        </DialogHeader>\n        <DialogFooter>\n          <Button variant=\"ghost\" disabled={loading} onClick={() => onOpenChange(false)}>\n            {cancelLabel}\n          </Button>\n          <Button variant={confirmVariant} disabled={loading} onClick={handleConfirm} data-loading={loading}>\n            {confirmLabel}\n          </Button>\n        </DialogFooter>\n      </DialogContent>\n    </Dialog>\n  );\n}\n"
  },
  {
    "path": "web/src/components/CreateAccessTokenDialog.tsx",
    "content": "import copy from \"copy-to-clipboard\";\nimport React, { useEffect, useState } from \"react\";\nimport { toast } from \"react-hot-toast\";\nimport { Button } from \"@/components/ui/button\";\nimport { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from \"@/components/ui/dialog\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport { RadioGroup, RadioGroupItem } from \"@/components/ui/radio-group\";\nimport { Textarea } from \"@/components/ui/textarea\";\nimport { userServiceClient } from \"@/connect\";\nimport useCurrentUser from \"@/hooks/useCurrentUser\";\nimport useLoading from \"@/hooks/useLoading\";\nimport { handleError } from \"@/lib/error\";\nimport { CreatePersonalAccessTokenResponse } from \"@/types/proto/api/v1/user_service_pb\";\nimport { useTranslate } from \"@/utils/i18n\";\n\ninterface Props {\n  open: boolean;\n  onOpenChange: (open: boolean) => void;\n  onSuccess: (response: CreatePersonalAccessTokenResponse) => void;\n}\n\ninterface State {\n  description: string;\n  expiration: number;\n}\n\nfunction CreateAccessTokenDialog({ open, onOpenChange, onSuccess }: Props) {\n  const t = useTranslate();\n  const currentUser = useCurrentUser();\n  const [state, setState] = useState({\n    description: \"\",\n    expiration: 30, // Default: 30 days\n  });\n  const [createdToken, setCreatedToken] = useState<string | null>(null);\n  const requestState = useLoading(false);\n\n  // Expiration options in days (0 = never expires)\n  const expirationOptions = [\n    {\n      label: t(\"setting.access-token.create-dialog.duration-1m\"),\n      value: 30,\n    },\n    {\n      label: \"90 Days\",\n      value: 90,\n    },\n    {\n      label: t(\"setting.access-token.create-dialog.duration-never\"),\n      value: 0,\n    },\n  ];\n\n  const setPartialState = (partialState: Partial<State>) => {\n    setState({\n      ...state,\n      ...partialState,\n    });\n  };\n\n  const handleDescriptionInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n    setPartialState({\n      description: e.target.value,\n    });\n  };\n\n  const handleRoleInputChange = (value: string) => {\n    setPartialState({\n      expiration: Number(value),\n    });\n  };\n\n  const handleSaveBtnClick = async () => {\n    if (!state.description) {\n      toast.error(t(\"message.description-is-required\"));\n      return;\n    }\n\n    try {\n      requestState.setLoading();\n      const response = await userServiceClient.createPersonalAccessToken({\n        parent: currentUser?.name,\n        description: state.description,\n        expiresInDays: state.expiration,\n      });\n\n      requestState.setFinish();\n      onSuccess(response);\n      if (response.token) {\n        setCreatedToken(response.token);\n      } else {\n        onOpenChange(false);\n      }\n    } catch (error: unknown) {\n      handleError(error, toast.error, {\n        context: \"Create access token\",\n        onError: () => requestState.setError(),\n      });\n    }\n  };\n\n  const handleCopyToken = () => {\n    if (!createdToken) return;\n    copy(createdToken);\n    toast.success(t(\"message.copied\"));\n  };\n\n  useEffect(() => {\n    if (!open) return;\n    setState({\n      description: \"\",\n      expiration: 30,\n    });\n    setCreatedToken(null);\n  }, [open]);\n\n  return (\n    <Dialog open={open} onOpenChange={onOpenChange}>\n      <DialogContent className=\"max-w-md\">\n        <DialogHeader>\n          <DialogTitle>{t(\"setting.access-token.create-dialog.create-access-token\")}</DialogTitle>\n        </DialogHeader>\n        {createdToken ? (\n          <div className=\"flex flex-col gap-4\">\n            <div className=\"grid gap-2\">\n              <Label>{t(\"setting.access-token.token\")}</Label>\n              <Textarea value={createdToken} readOnly rows={3} className=\"font-mono text-xs\" />\n            </div>\n          </div>\n        ) : (\n          <div className=\"flex flex-col gap-4\">\n            <div className=\"grid gap-2\">\n              <Label htmlFor=\"description\">\n                {t(\"setting.access-token.create-dialog.description\")} <span className=\"text-destructive\">*</span>\n              </Label>\n              <Input\n                id=\"description\"\n                type=\"text\"\n                placeholder={t(\"setting.access-token.create-dialog.some-description\")}\n                value={state.description}\n                onChange={handleDescriptionInputChange}\n              />\n            </div>\n            <div className=\"grid gap-2\">\n              <Label>\n                {t(\"setting.access-token.create-dialog.expiration\")} <span className=\"text-destructive\">*</span>\n              </Label>\n              <RadioGroup value={state.expiration.toString()} onValueChange={handleRoleInputChange} className=\"flex flex-row gap-4\">\n                {expirationOptions.map((option) => (\n                  <div key={option.value} className=\"flex items-center space-x-2\">\n                    <RadioGroupItem value={option.value.toString()} id={`expiration-${option.value}`} />\n                    <Label htmlFor={`expiration-${option.value}`}>{option.label}</Label>\n                  </div>\n                ))}\n              </RadioGroup>\n            </div>\n          </div>\n        )}\n        <DialogFooter>\n          {createdToken ? (\n            <>\n              <Button variant=\"ghost\" onClick={handleCopyToken}>\n                {t(\"common.copy\")}\n              </Button>\n              <Button onClick={() => onOpenChange(false)}>{t(\"common.close\")}</Button>\n            </>\n          ) : (\n            <>\n              <Button variant=\"ghost\" disabled={requestState.isLoading} onClick={() => onOpenChange(false)}>\n                {t(\"common.cancel\")}\n              </Button>\n              <Button disabled={requestState.isLoading} onClick={handleSaveBtnClick}>\n                {t(\"common.create\")}\n              </Button>\n            </>\n          )}\n        </DialogFooter>\n      </DialogContent>\n    </Dialog>\n  );\n}\n\nexport default CreateAccessTokenDialog;\n"
  },
  {
    "path": "web/src/components/CreateIdentityProviderDialog.tsx",
    "content": "import { create } from \"@bufbuild/protobuf\";\nimport { FieldMaskSchema } from \"@bufbuild/protobuf/wkt\";\nimport { useEffect, useState } from \"react\";\nimport { toast } from \"react-hot-toast\";\nimport { Button } from \"@/components/ui/button\";\nimport { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from \"@/components/ui/dialog\";\nimport { Input } from \"@/components/ui/input\";\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from \"@/components/ui/select\";\nimport { Separator } from \"@/components/ui/separator\";\nimport { identityProviderServiceClient } from \"@/connect\";\nimport { absolutifyLink } from \"@/helpers/utils\";\nimport { handleError } from \"@/lib/error\";\nimport {\n  FieldMapping,\n  FieldMappingSchema,\n  IdentityProvider,\n  IdentityProvider_Type,\n  IdentityProviderConfigSchema,\n  IdentityProviderSchema,\n  OAuth2Config,\n  OAuth2ConfigSchema,\n} from \"@/types/proto/api/v1/idp_service_pb\";\nimport { useTranslate } from \"@/utils/i18n\";\n\nconst templateList: IdentityProvider[] = [\n  create(IdentityProviderSchema, {\n    name: \"\",\n    title: \"GitHub\",\n    type: IdentityProvider_Type.OAUTH2,\n    identifierFilter: \"\",\n    config: create(IdentityProviderConfigSchema, {\n      config: {\n        case: \"oauth2Config\",\n        value: create(OAuth2ConfigSchema, {\n          clientId: \"\",\n          clientSecret: \"\",\n          authUrl: \"https://github.com/login/oauth/authorize\",\n          tokenUrl: \"https://github.com/login/oauth/access_token\",\n          userInfoUrl: \"https://api.github.com/user\",\n          scopes: [\"read:user\"],\n          fieldMapping: create(FieldMappingSchema, {\n            identifier: \"login\",\n            displayName: \"name\",\n            email: \"email\",\n          }),\n        }),\n      },\n    }),\n  }),\n  create(IdentityProviderSchema, {\n    name: \"\",\n    title: \"GitLab\",\n    type: IdentityProvider_Type.OAUTH2,\n    identifierFilter: \"\",\n    config: create(IdentityProviderConfigSchema, {\n      config: {\n        case: \"oauth2Config\",\n        value: create(OAuth2ConfigSchema, {\n          clientId: \"\",\n          clientSecret: \"\",\n          authUrl: \"https://gitlab.com/oauth/authorize\",\n          tokenUrl: \"https://gitlab.com/oauth/token\",\n          userInfoUrl: \"https://gitlab.com/oauth/userinfo\",\n          scopes: [\"openid\"],\n          fieldMapping: create(FieldMappingSchema, {\n            identifier: \"name\",\n            displayName: \"name\",\n            email: \"email\",\n          }),\n        }),\n      },\n    }),\n  }),\n  create(IdentityProviderSchema, {\n    name: \"\",\n    title: \"Google\",\n    type: IdentityProvider_Type.OAUTH2,\n    identifierFilter: \"\",\n    config: create(IdentityProviderConfigSchema, {\n      config: {\n        case: \"oauth2Config\",\n        value: create(OAuth2ConfigSchema, {\n          clientId: \"\",\n          clientSecret: \"\",\n          authUrl: \"https://accounts.google.com/o/oauth2/v2/auth\",\n          tokenUrl: \"https://oauth2.googleapis.com/token\",\n          userInfoUrl: \"https://www.googleapis.com/oauth2/v2/userinfo\",\n          scopes: [\"https://www.googleapis.com/auth/userinfo.email\", \"https://www.googleapis.com/auth/userinfo.profile\"],\n          fieldMapping: create(FieldMappingSchema, {\n            identifier: \"email\",\n            displayName: \"name\",\n            email: \"email\",\n          }),\n        }),\n      },\n    }),\n  }),\n  create(IdentityProviderSchema, {\n    name: \"\",\n    title: \"Custom\",\n    type: IdentityProvider_Type.OAUTH2,\n    identifierFilter: \"\",\n    config: create(IdentityProviderConfigSchema, {\n      config: {\n        case: \"oauth2Config\",\n        value: create(OAuth2ConfigSchema, {\n          clientId: \"\",\n          clientSecret: \"\",\n          authUrl: \"\",\n          tokenUrl: \"\",\n          userInfoUrl: \"\",\n          scopes: [],\n          fieldMapping: create(FieldMappingSchema, {\n            identifier: \"\",\n            displayName: \"\",\n            email: \"\",\n          }),\n        }),\n      },\n    }),\n  }),\n];\n\ninterface Props {\n  open: boolean;\n  onOpenChange: (open: boolean) => void;\n  identityProvider?: IdentityProvider;\n  onSuccess?: () => void;\n}\n\nfunction CreateIdentityProviderDialog({ open, onOpenChange, identityProvider, onSuccess }: Props) {\n  const t = useTranslate();\n  const identityProviderTypes = [...new Set(templateList.map((t) => t.type))];\n  const [basicInfo, setBasicInfo] = useState({\n    title: \"\",\n    identifier: \"\",\n    identifierFilter: \"\",\n  });\n  const [type, setType] = useState<IdentityProvider_Type>(IdentityProvider_Type.OAUTH2);\n  const [oauth2Config, setOAuth2Config] = useState<OAuth2Config>(\n    create(OAuth2ConfigSchema, {\n      clientId: \"\",\n      clientSecret: \"\",\n      authUrl: \"\",\n      tokenUrl: \"\",\n      userInfoUrl: \"\",\n      scopes: [],\n      fieldMapping: create(FieldMappingSchema, {\n        identifier: \"\",\n        displayName: \"\",\n        email: \"\",\n      }),\n    }),\n  );\n  const [oauth2Scopes, setOAuth2Scopes] = useState<string>(\"\");\n  const [selectedTemplate, setSelectedTemplate] = useState<string>(\"GitHub\");\n  const isCreating = identityProvider === undefined;\n\n  // Reset state when dialog is closed\n  useEffect(() => {\n    if (!open) {\n      // Reset to default state when dialog is closed\n      setBasicInfo({\n        title: \"\",\n        identifier: \"\",\n        identifierFilter: \"\",\n      });\n      setType(IdentityProvider_Type.OAUTH2);\n      setOAuth2Config(\n        create(OAuth2ConfigSchema, {\n          clientId: \"\",\n          clientSecret: \"\",\n          authUrl: \"\",\n          tokenUrl: \"\",\n          userInfoUrl: \"\",\n          scopes: [],\n          fieldMapping: create(FieldMappingSchema, {\n            identifier: \"\",\n            displayName: \"\",\n            email: \"\",\n          }),\n        }),\n      );\n      setOAuth2Scopes(\"\");\n      setSelectedTemplate(\"GitHub\");\n    }\n  }, [open]);\n\n  // Load existing identity provider data when editing\n  useEffect(() => {\n    if (open && identityProvider) {\n      setBasicInfo({\n        title: identityProvider.title,\n        identifier: \"\",\n        identifierFilter: identityProvider.identifierFilter,\n      });\n      setType(identityProvider.type);\n      if (identityProvider.type === IdentityProvider_Type.OAUTH2 && identityProvider.config?.config?.case === \"oauth2Config\") {\n        const oauth2Config = create(OAuth2ConfigSchema, identityProvider.config.config.value || {});\n        setOAuth2Config(oauth2Config);\n        setOAuth2Scopes(oauth2Config.scopes.join(\" \"));\n      }\n    }\n  }, [open, identityProvider]);\n\n  // Load template data when creating new IDP\n  useEffect(() => {\n    if (!isCreating || !open) {\n      return;\n    }\n\n    const template = templateList.find((t) => t.title === selectedTemplate);\n    if (template) {\n      setBasicInfo({\n        title: template.title,\n        identifier: template.title.toLowerCase().replace(/[^a-z0-9]+/g, \"-\"),\n        identifierFilter: template.identifierFilter,\n      });\n      setType(template.type);\n      if (template.type === IdentityProvider_Type.OAUTH2 && template.config?.config?.case === \"oauth2Config\") {\n        const oauth2Config = create(OAuth2ConfigSchema, template.config.config.value || {});\n        setOAuth2Config(oauth2Config);\n        setOAuth2Scopes(oauth2Config.scopes.join(\" \"));\n      }\n    }\n  }, [selectedTemplate, isCreating, open]);\n\n  const handleCloseBtnClick = () => {\n    onOpenChange(false);\n  };\n\n  const allowConfirmAction = () => {\n    if (basicInfo.title === \"\") {\n      return false;\n    }\n    if (isCreating && basicInfo.identifier === \"\") {\n      return false;\n    }\n    if (type === IdentityProvider_Type.OAUTH2) {\n      if (\n        oauth2Config.clientId === \"\" ||\n        oauth2Config.authUrl === \"\" ||\n        oauth2Config.tokenUrl === \"\" ||\n        oauth2Config.userInfoUrl === \"\" ||\n        oauth2Scopes === \"\" ||\n        oauth2Config.fieldMapping?.identifier === \"\"\n      ) {\n        return false;\n      }\n      if (isCreating) {\n        if (oauth2Config.clientSecret === \"\") {\n          return false;\n        }\n      }\n    }\n\n    return true;\n  };\n\n  const handleConfirmBtnClick = async () => {\n    try {\n      if (isCreating) {\n        await identityProviderServiceClient.createIdentityProvider({\n          identityProviderId: basicInfo.identifier,\n          identityProvider: create(IdentityProviderSchema, {\n            title: basicInfo.title,\n            identifierFilter: basicInfo.identifierFilter,\n            type: type,\n            config: create(IdentityProviderConfigSchema, {\n              config: {\n                case: \"oauth2Config\",\n                value: {\n                  ...oauth2Config,\n                  scopes: oauth2Scopes.split(\" \"),\n                },\n              },\n            }),\n          }),\n        });\n        toast.success(t(\"setting.sso.sso-created\", { name: basicInfo.title }));\n      } else {\n        await identityProviderServiceClient.updateIdentityProvider({\n          identityProvider: create(IdentityProviderSchema, {\n            ...basicInfo,\n            name: identityProvider!.name,\n            type: type,\n            config: create(IdentityProviderConfigSchema, {\n              config: {\n                case: \"oauth2Config\",\n                value: {\n                  ...oauth2Config,\n                  scopes: oauth2Scopes.split(\" \"),\n                },\n              },\n            }),\n          }),\n          updateMask: create(FieldMaskSchema, { paths: [\"title\", \"identifier_filter\", \"config\"] }),\n        });\n        toast.success(t(\"setting.sso.sso-updated\", { name: basicInfo.title }));\n      }\n    } catch (error: unknown) {\n      await handleError(error, toast.error, {\n        context: isCreating ? \"Create identity provider\" : \"Update identity provider\",\n      });\n    }\n    onSuccess?.();\n    onOpenChange(false);\n  };\n\n  const setPartialOAuth2Config = (state: Partial<OAuth2Config>) => {\n    setOAuth2Config({\n      ...oauth2Config,\n      ...state,\n    });\n  };\n\n  return (\n    <Dialog open={open} onOpenChange={onOpenChange}>\n      <DialogContent className=\"max-w-2xl max-h-[80vh] overflow-y-auto\">\n        <DialogHeader>\n          <DialogTitle>{t(isCreating ? \"setting.sso.create-sso\" : \"setting.sso.update-sso\")}</DialogTitle>\n        </DialogHeader>\n        <div className=\"flex flex-col justify-start items-start w-full space-y-4\">\n          {isCreating && (\n            <>\n              <p className=\"mb-1!\">{t(\"common.type\")}</p>\n              <Select value={String(type)} onValueChange={(value) => setType(parseInt(value) as unknown as IdentityProvider_Type)}>\n                <SelectTrigger className=\"w-full mb-4\">\n                  <SelectValue />\n                </SelectTrigger>\n                <SelectContent>\n                  {identityProviderTypes.map((kind) => (\n                    <SelectItem key={kind} value={String(kind)}>\n                      {IdentityProvider_Type[kind] || kind}\n                    </SelectItem>\n                  ))}\n                </SelectContent>\n              </Select>\n              <p className=\"mb-2 text-sm font-medium\">{t(\"setting.sso.template\")}</p>\n              <Select value={selectedTemplate} onValueChange={(value) => setSelectedTemplate(value)}>\n                <SelectTrigger className=\"mb-1 h-auto w-full\">\n                  <SelectValue />\n                </SelectTrigger>\n                <SelectContent>\n                  {templateList.map((template) => (\n                    <SelectItem key={template.title} value={template.title}>\n                      {template.title}\n                    </SelectItem>\n                  ))}\n                </SelectContent>\n              </Select>\n              <Separator className=\"my-2\" />\n            </>\n          )}\n          {isCreating && (\n            <>\n              <p className=\"mb-1 text-sm font-medium\">\n                ID\n                <span className=\"text-destructive\">*</span>\n              </p>\n              <Input\n                className=\"mb-2 w-full font-mono\"\n                placeholder=\"e.g. github, okta-corp\"\n                maxLength={32}\n                value={basicInfo.identifier}\n                onChange={(e) =>\n                  setBasicInfo({\n                    ...basicInfo,\n                    identifier: e.target.value\n                      .toLowerCase()\n                      .replace(/[^a-z0-9-]/g, \"-\")\n                      .replace(/--+/g, \"-\"),\n                  })\n                }\n              />\n              <p className=\"mb-2 text-xs text-muted-foreground\">\n                A unique identifier for this provider. Lowercase letters, numbers, and hyphens only.\n              </p>\n            </>\n          )}\n          <p className=\"mb-1 text-sm font-medium\">\n            {t(\"common.name\")}\n            <span className=\"text-destructive\">*</span>\n          </p>\n          <Input\n            className=\"mb-2 w-full\"\n            placeholder={t(\"common.name\")}\n            value={basicInfo.title}\n            onChange={(e) =>\n              setBasicInfo({\n                ...basicInfo,\n                title: e.target.value,\n              })\n            }\n          />\n          <p className=\"mb-1 text-sm font-medium\">{t(\"setting.sso.identifier-filter\")}</p>\n          <Input\n            className=\"mb-2 w-full\"\n            placeholder={t(\"setting.sso.identifier-filter\")}\n            value={basicInfo.identifierFilter}\n            onChange={(e) =>\n              setBasicInfo({\n                ...basicInfo,\n                identifierFilter: e.target.value,\n              })\n            }\n          />\n          <Separator className=\"my-2\" />\n          {type === IdentityProvider_Type.OAUTH2 && (\n            <>\n              {isCreating && (\n                <p className=\"border border-border rounded-md p-2 text-sm w-full mb-2 break-all\">\n                  {t(\"setting.sso.redirect-url\")}: {absolutifyLink(\"/auth/callback\")}\n                </p>\n              )}\n              <p className=\"mb-1 text-sm font-medium\">\n                {t(\"setting.sso.client-id\")}\n                <span className=\"text-destructive\">*</span>\n              </p>\n              <Input\n                className=\"mb-2 w-full\"\n                placeholder={t(\"setting.sso.client-id\")}\n                value={oauth2Config.clientId}\n                onChange={(e) => setPartialOAuth2Config({ clientId: e.target.value })}\n              />\n              <p className=\"mb-1 text-sm font-medium\">\n                {t(\"setting.sso.client-secret\")}\n                <span className=\"text-destructive\">*</span>\n              </p>\n              <Input\n                className=\"mb-2 w-full\"\n                placeholder={t(\"setting.sso.client-secret\")}\n                value={oauth2Config.clientSecret}\n                onChange={(e) => setPartialOAuth2Config({ clientSecret: e.target.value })}\n              />\n              <p className=\"mb-1 text-sm font-medium\">\n                {t(\"setting.sso.authorization-endpoint\")}\n                <span className=\"text-destructive\">*</span>\n              </p>\n              <Input\n                className=\"mb-2 w-full\"\n                placeholder={t(\"setting.sso.authorization-endpoint\")}\n                value={oauth2Config.authUrl}\n                onChange={(e) => setPartialOAuth2Config({ authUrl: e.target.value })}\n              />\n              <p className=\"mb-1 text-sm font-medium\">\n                {t(\"setting.sso.token-endpoint\")}\n                <span className=\"text-destructive\">*</span>\n              </p>\n              <Input\n                className=\"mb-2 w-full\"\n                placeholder={t(\"setting.sso.token-endpoint\")}\n                value={oauth2Config.tokenUrl}\n                onChange={(e) => setPartialOAuth2Config({ tokenUrl: e.target.value })}\n              />\n              <p className=\"mb-1 text-sm font-medium\">\n                {t(\"setting.sso.user-endpoint\")}\n                <span className=\"text-destructive\">*</span>\n              </p>\n              <Input\n                className=\"mb-2 w-full\"\n                placeholder={t(\"setting.sso.user-endpoint\")}\n                value={oauth2Config.userInfoUrl}\n                onChange={(e) => setPartialOAuth2Config({ userInfoUrl: e.target.value })}\n              />\n              <p className=\"mb-1 text-sm font-medium\">\n                {t(\"setting.sso.scopes\")}\n                <span className=\"text-destructive\">*</span>\n              </p>\n              <Input\n                className=\"mb-2 w-full\"\n                placeholder={t(\"setting.sso.scopes\")}\n                value={oauth2Scopes}\n                onChange={(e) => setOAuth2Scopes(e.target.value)}\n              />\n              <Separator className=\"my-2\" />\n              <p className=\"mb-1 text-sm font-medium\">\n                {t(\"setting.sso.identifier\")}\n                <span className=\"text-destructive\">*</span>\n              </p>\n              <Input\n                className=\"mb-2 w-full\"\n                placeholder={t(\"setting.sso.identifier\")}\n                value={oauth2Config.fieldMapping!.identifier}\n                onChange={(e) =>\n                  setPartialOAuth2Config({ fieldMapping: { ...oauth2Config.fieldMapping, identifier: e.target.value } as FieldMapping })\n                }\n              />\n              <p className=\"mb-1 text-sm font-medium\">{t(\"setting.sso.display-name\")}</p>\n              <Input\n                className=\"mb-2 w-full\"\n                placeholder={t(\"setting.sso.display-name\")}\n                value={oauth2Config.fieldMapping!.displayName}\n                onChange={(e) =>\n                  setPartialOAuth2Config({ fieldMapping: { ...oauth2Config.fieldMapping, displayName: e.target.value } as FieldMapping })\n                }\n              />\n              <p className=\"mb-1 text-sm font-medium\">{t(\"common.email\")}</p>\n              <Input\n                className=\"mb-2 w-full\"\n                placeholder={t(\"common.email\")}\n                value={oauth2Config.fieldMapping!.email}\n                onChange={(e) =>\n                  setPartialOAuth2Config({ fieldMapping: { ...oauth2Config.fieldMapping, email: e.target.value } as FieldMapping })\n                }\n              />\n              <p className=\"mb-1 text-sm font-medium\">Avatar URL</p>\n              <Input\n                className=\"mb-2 w-full\"\n                placeholder={\"Avatar URL\"}\n                value={oauth2Config.fieldMapping!.avatarUrl}\n                onChange={(e) =>\n                  setPartialOAuth2Config({ fieldMapping: { ...oauth2Config.fieldMapping, avatarUrl: e.target.value } as FieldMapping })\n                }\n              />\n            </>\n          )}\n        </div>\n        <DialogFooter>\n          <Button variant=\"ghost\" onClick={handleCloseBtnClick}>\n            {t(\"common.cancel\")}\n          </Button>\n          <Button onClick={handleConfirmBtnClick} disabled={!allowConfirmAction()}>\n            {t(isCreating ? \"common.create\" : \"common.update\")}\n          </Button>\n        </DialogFooter>\n      </DialogContent>\n    </Dialog>\n  );\n}\n\nexport default CreateIdentityProviderDialog;\n"
  },
  {
    "path": "web/src/components/CreateShortcutDialog.tsx",
    "content": "import { create } from \"@bufbuild/protobuf\";\nimport { FieldMaskSchema } from \"@bufbuild/protobuf/wkt\";\nimport { useEffect, useState } from \"react\";\nimport { toast } from \"react-hot-toast\";\nimport { Button } from \"@/components/ui/button\";\nimport { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from \"@/components/ui/dialog\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport { Textarea } from \"@/components/ui/textarea\";\nimport { shortcutServiceClient } from \"@/connect\";\nimport { useAuth } from \"@/contexts/AuthContext\";\nimport useCurrentUser from \"@/hooks/useCurrentUser\";\nimport useLoading from \"@/hooks/useLoading\";\nimport { handleError } from \"@/lib/error\";\nimport { Shortcut, ShortcutSchema } from \"@/types/proto/api/v1/shortcut_service_pb\";\nimport { useTranslate } from \"@/utils/i18n\";\n\ninterface Props {\n  open: boolean;\n  onOpenChange: (open: boolean) => void;\n  shortcut?: Shortcut;\n  onSuccess?: () => void;\n}\n\nfunction CreateShortcutDialog({ open, onOpenChange, shortcut: initialShortcut, onSuccess }: Props) {\n  const t = useTranslate();\n  const user = useCurrentUser();\n  const { refetchSettings } = useAuth();\n  const [shortcut, setShortcut] = useState<Shortcut>(\n    create(ShortcutSchema, {\n      name: initialShortcut?.name || \"\",\n      title: initialShortcut?.title || \"\",\n      filter: initialShortcut?.filter || \"\",\n    }),\n  );\n  const requestState = useLoading(false);\n  const isCreating = shortcut.name === \"\";\n\n  useEffect(() => {\n    setShortcut(\n      create(ShortcutSchema, {\n        name: initialShortcut?.name || \"\",\n        title: initialShortcut?.title || \"\",\n        filter: initialShortcut?.filter || \"\",\n      }),\n    );\n  }, [initialShortcut]);\n\n  const onShortcutTitleChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n    setPartialState({\n      title: e.target.value,\n    });\n  };\n\n  const onShortcutFilterChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {\n    setPartialState({\n      filter: e.target.value,\n    });\n  };\n\n  const setPartialState = (partialState: Partial<Shortcut>) => {\n    setShortcut({\n      ...shortcut,\n      ...partialState,\n    });\n  };\n\n  const handleSaveBtnClick = async () => {\n    if (!shortcut.title || !shortcut.filter) {\n      toast.error(\"Title and filter cannot be empty\");\n      return;\n    }\n\n    try {\n      requestState.setLoading();\n      if (isCreating) {\n        await shortcutServiceClient.createShortcut({\n          parent: user?.name,\n          shortcut: {\n            name: \"\",\n            title: shortcut.title,\n            filter: shortcut.filter,\n          },\n        });\n        toast.success(\"Create shortcut successfully\");\n      } else {\n        await shortcutServiceClient.updateShortcut({\n          shortcut: {\n            ...shortcut,\n            name: initialShortcut!.name,\n          },\n          updateMask: create(FieldMaskSchema, { paths: [\"title\", \"filter\"] }),\n        });\n        toast.success(\"Update shortcut successfully\");\n      }\n      await refetchSettings();\n      requestState.setFinish();\n      onSuccess?.();\n      onOpenChange(false);\n    } catch (error: unknown) {\n      await handleError(error, toast.error, {\n        context: isCreating ? \"Create shortcut\" : \"Update shortcut\",\n        onError: () => requestState.setError(),\n      });\n    }\n  };\n\n  return (\n    <Dialog open={open} onOpenChange={onOpenChange}>\n      <DialogContent className=\"max-w-md\">\n        <DialogHeader>\n          <DialogTitle>{`${isCreating ? t(\"common.create\") : t(\"common.edit\")} ${t(\"common.shortcuts\")}`}</DialogTitle>\n        </DialogHeader>\n        <div className=\"flex flex-col gap-4\">\n          <div className=\"grid gap-2\">\n            <Label htmlFor=\"title\">{t(\"common.title\")}</Label>\n            <Input id=\"title\" type=\"text\" placeholder=\"\" value={shortcut.title} onChange={onShortcutTitleChange} />\n          </div>\n          <div className=\"grid gap-2\">\n            <Label htmlFor=\"filter\">{t(\"common.filter\")}</Label>\n            <Textarea\n              id=\"filter\"\n              rows={3}\n              placeholder={t(\"common.shortcut-filter\")}\n              value={shortcut.filter}\n              onChange={onShortcutFilterChange}\n            />\n          </div>\n          <div className=\"text-sm text-muted-foreground\">\n            <p className=\"mb-2\">{t(\"common.learn-more\")}:</p>\n            <ul className=\"list-disc list-inside space-y-1\">\n              <li>\n                <a\n                  className=\"text-primary hover:underline\"\n                  href=\"https://www.usememos.com/docs/usage/shortcuts\"\n                  target=\"_blank\"\n                  rel=\"noopener noreferrer\"\n                >\n                  Docs - Shortcuts\n                </a>\n              </li>\n            </ul>\n          </div>\n        </div>\n        <DialogFooter>\n          <Button variant=\"ghost\" disabled={requestState.isLoading} onClick={() => onOpenChange(false)}>\n            {t(\"common.cancel\")}\n          </Button>\n          <Button disabled={requestState.isLoading} onClick={handleSaveBtnClick}>\n            {t(\"common.save\")}\n          </Button>\n        </DialogFooter>\n      </DialogContent>\n    </Dialog>\n  );\n}\n\nexport default CreateShortcutDialog;\n"
  },
  {
    "path": "web/src/components/CreateUserDialog.tsx",
    "content": "import { create } from \"@bufbuild/protobuf\";\nimport { FieldMaskSchema } from \"@bufbuild/protobuf/wkt\";\nimport { useEffect, useState } from \"react\";\nimport { toast } from \"react-hot-toast\";\nimport { Button } from \"@/components/ui/button\";\nimport { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from \"@/components/ui/dialog\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport { RadioGroup, RadioGroupItem } from \"@/components/ui/radio-group\";\nimport { userServiceClient } from \"@/connect\";\nimport useLoading from \"@/hooks/useLoading\";\nimport { handleError } from \"@/lib/error\";\nimport { User, User_Role, UserSchema } from \"@/types/proto/api/v1/user_service_pb\";\nimport { useTranslate } from \"@/utils/i18n\";\n\ninterface Props {\n  open: boolean;\n  onOpenChange: (open: boolean) => void;\n  user?: User;\n  onSuccess?: () => void;\n}\n\nfunction CreateUserDialog({ open, onOpenChange, user: initialUser, onSuccess }: Props) {\n  const t = useTranslate();\n  const [user, setUser] = useState(\n    create(UserSchema, initialUser ? { name: initialUser.name, username: initialUser.username, role: initialUser.role } : {}),\n  );\n  const requestState = useLoading(false);\n  const isCreating = !initialUser;\n\n  useEffect(() => {\n    if (initialUser) {\n      setUser(create(UserSchema, { name: initialUser.name, username: initialUser.username, role: initialUser.role }));\n    } else {\n      setUser(create(UserSchema, {}));\n    }\n  }, [initialUser]);\n\n  const setPartialUser = (state: Partial<User>) => {\n    setUser({\n      ...user,\n      ...state,\n    });\n  };\n\n  const handleConfirm = async () => {\n    if (isCreating && (!user.username || !user.password)) {\n      toast.error(\"Username and password cannot be empty\");\n      return;\n    }\n\n    try {\n      requestState.setLoading();\n      if (isCreating) {\n        await userServiceClient.createUser({ user });\n        toast.success(\"Create user successfully\");\n      } else {\n        const updateMask = [];\n        if (user.username !== initialUser?.username) {\n          updateMask.push(\"username\");\n        }\n        if (user.password) {\n          updateMask.push(\"password\");\n        }\n        if (user.role !== initialUser?.role) {\n          updateMask.push(\"role\");\n        }\n        const userToUpdate = create(UserSchema, { ...user, name: initialUser?.name ?? user.name });\n        await userServiceClient.updateUser({ user: userToUpdate, updateMask: create(FieldMaskSchema, { paths: updateMask }) });\n        toast.success(\"Update user successfully\");\n      }\n      requestState.setFinish();\n      onSuccess?.();\n      onOpenChange(false);\n    } catch (error: unknown) {\n      handleError(error, toast.error, {\n        context: user ? \"Update user\" : \"Create user\",\n        onError: () => requestState.setError(),\n      });\n    }\n  };\n\n  return (\n    <Dialog open={open} onOpenChange={onOpenChange}>\n      <DialogContent className=\"max-w-md\">\n        <DialogHeader>\n          <DialogTitle>{`${isCreating ? t(\"common.create\") : t(\"common.edit\")} ${t(\"common.user\")}`}</DialogTitle>\n        </DialogHeader>\n        <div className=\"flex flex-col gap-4\">\n          <div className=\"grid gap-2\">\n            <Label htmlFor=\"username\">{t(\"common.username\")}</Label>\n            <Input\n              id=\"username\"\n              type=\"text\"\n              placeholder={t(\"common.username\")}\n              value={user.username}\n              onChange={(e) =>\n                setPartialUser({\n                  username: e.target.value,\n                })\n              }\n            />\n          </div>\n          <div className=\"grid gap-2\">\n            <Label htmlFor=\"password\">{t(\"common.password\")}</Label>\n            <Input\n              id=\"password\"\n              type=\"password\"\n              placeholder={t(\"common.password\")}\n              autoComplete=\"off\"\n              value={user.password}\n              onChange={(e) =>\n                setPartialUser({\n                  password: e.target.value,\n                })\n              }\n            />\n          </div>\n          <div className=\"grid gap-2\">\n            <Label>{t(\"common.role\")}</Label>\n            <RadioGroup\n              value={String(user.role)}\n              onValueChange={(value) => setPartialUser({ role: Number(value) as User_Role })}\n              className=\"flex flex-row gap-4\"\n            >\n              <div className=\"flex items-center space-x-2\">\n                <RadioGroupItem value={String(User_Role.USER)} id=\"user\" />\n                <Label htmlFor=\"user\">{t(\"setting.member.user\")}</Label>\n              </div>\n              <div className=\"flex items-center space-x-2\">\n                <RadioGroupItem value={String(User_Role.ADMIN)} id=\"admin\" />\n                <Label htmlFor=\"admin\">{t(\"setting.member.admin\")}</Label>\n              </div>\n            </RadioGroup>\n          </div>\n        </div>\n        <DialogFooter>\n          <Button variant=\"ghost\" disabled={requestState.isLoading} onClick={() => onOpenChange(false)}>\n            {t(\"common.cancel\")}\n          </Button>\n          <Button disabled={requestState.isLoading} onClick={handleConfirm}>\n            {t(\"common.confirm\")}\n          </Button>\n        </DialogFooter>\n      </DialogContent>\n    </Dialog>\n  );\n}\n\nexport default CreateUserDialog;\n"
  },
  {
    "path": "web/src/components/CreateWebhookDialog.tsx",
    "content": "import { create } from \"@bufbuild/protobuf\";\nimport { FieldMaskSchema } from \"@bufbuild/protobuf/wkt\";\nimport React, { useEffect, useState } from \"react\";\nimport { toast } from \"react-hot-toast\";\nimport { Button } from \"@/components/ui/button\";\nimport { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from \"@/components/ui/dialog\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport { userServiceClient } from \"@/connect\";\nimport useCurrentUser from \"@/hooks/useCurrentUser\";\nimport useLoading from \"@/hooks/useLoading\";\nimport { handleError } from \"@/lib/error\";\nimport { useTranslate } from \"@/utils/i18n\";\n\ninterface Props {\n  open: boolean;\n  onOpenChange: (open: boolean) => void;\n  webhookName?: string;\n  onSuccess?: () => void;\n}\n\ninterface State {\n  displayName: string;\n  url: string;\n}\n\nfunction CreateWebhookDialog({ open, onOpenChange, webhookName, onSuccess }: Props) {\n  const t = useTranslate();\n  const currentUser = useCurrentUser();\n  const [state, setState] = useState<State>({\n    displayName: \"\",\n    url: \"\",\n  });\n  const requestState = useLoading(false);\n  const isCreating = webhookName === undefined;\n\n  useEffect(() => {\n    if (webhookName && currentUser) {\n      // For editing, we need to get the webhook data\n      // Since we're using user webhooks now, we need to list all webhooks and find the one we want\n      userServiceClient\n        .listUserWebhooks({\n          parent: currentUser.name,\n        })\n        .then((response) => {\n          const webhook = response.webhooks.find((w) => w.name === webhookName);\n          if (webhook) {\n            setState({\n              displayName: webhook.displayName,\n              url: webhook.url,\n            });\n          }\n        });\n    }\n  }, [webhookName, currentUser]);\n\n  const setPartialState = (partialState: Partial<State>) => {\n    setState({\n      ...state,\n      ...partialState,\n    });\n  };\n\n  const handleTitleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n    setPartialState({\n      displayName: e.target.value,\n    });\n  };\n\n  const handleUrlInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n    setPartialState({\n      url: e.target.value,\n    });\n  };\n\n  const handleSaveBtnClick = async () => {\n    if (!state.displayName || !state.url) {\n      toast.error(t(\"message.fill-all-required-fields\"));\n      return;\n    }\n\n    if (!currentUser) {\n      toast.error(\"User not authenticated\");\n      return;\n    }\n\n    try {\n      requestState.setLoading();\n      if (isCreating) {\n        await userServiceClient.createUserWebhook({\n          parent: currentUser.name,\n          webhook: {\n            displayName: state.displayName,\n            url: state.url,\n          },\n        });\n      } else {\n        await userServiceClient.updateUserWebhook({\n          webhook: {\n            name: webhookName,\n            displayName: state.displayName,\n            url: state.url,\n          },\n          updateMask: create(FieldMaskSchema, { paths: [\"display_name\", \"url\"] }),\n        });\n      }\n\n      onSuccess?.();\n      onOpenChange(false);\n      requestState.setFinish();\n    } catch (error: unknown) {\n      handleError(error, toast.error, {\n        context: webhookName ? \"Update webhook\" : \"Create webhook\",\n        onError: () => requestState.setError(),\n      });\n    }\n  };\n\n  return (\n    <Dialog open={open} onOpenChange={onOpenChange}>\n      <DialogContent className=\"max-w-md\">\n        <DialogHeader>\n          <DialogTitle>\n            {isCreating ? t(\"setting.webhook.create-dialog.create-webhook\") : t(\"setting.webhook.create-dialog.edit-webhook\")}\n          </DialogTitle>\n        </DialogHeader>\n        <div className=\"flex flex-col gap-4\">\n          <div className=\"grid gap-2\">\n            <Label htmlFor=\"displayName\">\n              {t(\"setting.webhook.create-dialog.title\")} <span className=\"text-destructive\">*</span>\n            </Label>\n            <Input\n              id=\"displayName\"\n              type=\"text\"\n              placeholder={t(\"setting.webhook.create-dialog.an-easy-to-remember-name\")}\n              value={state.displayName}\n              onChange={handleTitleInputChange}\n            />\n          </div>\n          <div className=\"grid gap-2\">\n            <Label htmlFor=\"url\">\n              {t(\"setting.webhook.create-dialog.payload-url\")} <span className=\"text-destructive\">*</span>\n            </Label>\n            <Input\n              id=\"url\"\n              type=\"text\"\n              placeholder={t(\"setting.webhook.create-dialog.url-example-post-receive\")}\n              value={state.url}\n              onChange={handleUrlInputChange}\n            />\n          </div>\n        </div>\n        <DialogFooter>\n          <Button variant=\"ghost\" disabled={requestState.isLoading} onClick={() => onOpenChange(false)}>\n            {t(\"common.cancel\")}\n          </Button>\n          <Button disabled={requestState.isLoading} onClick={handleSaveBtnClick}>\n            {t(\"common.create\")}\n          </Button>\n        </DialogFooter>\n      </DialogContent>\n    </Dialog>\n  );\n}\n\nexport default CreateWebhookDialog;\n"
  },
  {
    "path": "web/src/components/DateTimeInput.tsx",
    "content": "import dayjs from \"dayjs\";\nimport toast from \"react-hot-toast\";\nimport { cn } from \"@/lib/utils\";\n\n// must be compatible with JS Date.parse(), we use ISO 8601 (almost)\nconst DATE_TIME_FORMAT = \"YYYY-MM-DD HH:mm:ss\";\n\n// convert Date to datetime string.\nconst formatDate = (date: Date): string => {\n  return dayjs(date).format(DATE_TIME_FORMAT);\n};\n\ninterface Props {\n  value: Date;\n  onChange: (date: Date) => void;\n}\n\nconst DateTimeInput: React.FC<Props> = ({ value, onChange }) => {\n  return (\n    <input\n      type=\"datetime-local\"\n      className={cn(\"px-1 bg-transparent rounded text-xs transition-all\", \"border-transparent outline-none focus:border-border\", \"border\")}\n      defaultValue={formatDate(value)}\n      onBlur={(e) => {\n        const inputValue = e.target.value;\n        if (inputValue) {\n          // note: inputValue must be compatible with JS Date.parse()\n          const date = dayjs(inputValue).toDate();\n          // Check if the date is valid.\n          if (!isNaN(date.getTime())) {\n            onChange(date);\n          } else {\n            toast.error(\"Invalid datetime format. Use format: 2023-12-31 23:59:59\");\n            e.target.value = formatDate(value);\n          }\n        }\n      }}\n      placeholder={DATE_TIME_FORMAT}\n    />\n  );\n};\n\nexport default DateTimeInput;\n"
  },
  {
    "path": "web/src/components/Empty.tsx",
    "content": "import { BirdIcon } from \"lucide-react\";\n\nconst Empty = () => {\n  return (\n    <div className=\"mx-auto\">\n      <BirdIcon strokeWidth={0.5} absoluteStrokeWidth={true} className=\"w-24 h-auto text-muted-foreground\" />\n    </div>\n  );\n};\n\nexport default Empty;\n"
  },
  {
    "path": "web/src/components/ErrorBoundary.tsx",
    "content": "import { AlertCircle, RefreshCw } from \"lucide-react\";\nimport { Component, type ErrorInfo, type ReactNode } from \"react\";\nimport { useRouteError } from \"react-router-dom\";\nimport { Button } from \"./ui/button\";\n\ninterface Props {\n  children: ReactNode;\n  fallback?: ReactNode;\n}\n\ninterface State {\n  hasError: boolean;\n  error: Error | null;\n}\n\nexport class ErrorBoundary extends Component<Props, State> {\n  constructor(props: Props) {\n    super(props);\n    this.state = { hasError: false, error: null };\n  }\n\n  static getDerivedStateFromError(error: Error): State {\n    return { hasError: true, error };\n  }\n\n  componentDidCatch(error: Error, errorInfo: ErrorInfo) {\n    console.error(\"ErrorBoundary caught an error:\", error, errorInfo);\n  }\n\n  handleReset = () => {\n    this.setState({ hasError: false, error: null });\n    window.location.reload();\n  };\n\n  render() {\n    if (this.state.hasError) {\n      if (this.props.fallback) {\n        return this.props.fallback;\n      }\n\n      return (\n        <div className=\"flex items-center justify-center min-h-screen bg-background\">\n          <div className=\"max-w-md w-full p-6 space-y-4\">\n            <div className=\"flex items-center gap-3 text-destructive\">\n              <AlertCircle className=\"w-8 h-8\" />\n              <h1 className=\"text-2xl font-bold\">Something went wrong</h1>\n            </div>\n\n            <p className=\"text-foreground/70\">\n              An unexpected error occurred. This could be due to a network issue or a problem with the application.\n            </p>\n\n            {this.state.error && (\n              <details className=\"bg-muted p-3 rounded-md text-sm\">\n                <summary className=\"cursor-pointer font-medium mb-2\">Error details</summary>\n                <pre className=\"whitespace-pre-wrap break-words text-xs text-foreground/60\">{this.state.error.message}</pre>\n              </details>\n            )}\n\n            <Button onClick={this.handleReset} className=\"w-full gap-2\">\n              <RefreshCw className=\"w-4 h-4\" />\n              Reload Application\n            </Button>\n          </div>\n        </div>\n      );\n    }\n\n    return this.props.children;\n  }\n}\n\n// React Router errorElement for route-level errors (e.g., failed chunk loads after redeployment).\nexport function ChunkLoadErrorFallback() {\n  const error = useRouteError() as Error | undefined;\n\n  return (\n    <div className=\"flex items-center justify-center min-h-screen bg-background\">\n      <div className=\"max-w-md w-full p-6 space-y-4\">\n        <div className=\"flex items-center gap-3 text-destructive\">\n          <AlertCircle className=\"w-8 h-8\" />\n          <h1 className=\"text-2xl font-bold\">Something went wrong</h1>\n        </div>\n\n        <p className=\"text-foreground/70\">\n          An unexpected error occurred. This could be due to a network issue or an application update. Reloading usually fixes it.\n        </p>\n\n        {error?.message && (\n          <details className=\"bg-muted p-3 rounded-md text-sm\">\n            <summary className=\"cursor-pointer font-medium mb-2\">Error details</summary>\n            <pre className=\"whitespace-pre-wrap break-words text-xs text-foreground/60\">{error.message}</pre>\n          </details>\n        )}\n\n        <Button onClick={() => window.location.reload()} className=\"w-full gap-2\">\n          <RefreshCw className=\"w-4 h-4\" />\n          Reload Application\n        </Button>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "web/src/components/Inbox/MemoCommentMessage.tsx",
    "content": "import { create } from \"@bufbuild/protobuf\";\nimport { FieldMaskSchema, timestampDate } from \"@bufbuild/protobuf/wkt\";\nimport { CheckIcon, MessageCircleIcon, TrashIcon, XIcon } from \"lucide-react\";\nimport { useState } from \"react\";\nimport toast from \"react-hot-toast\";\nimport UserAvatar from \"@/components/UserAvatar\";\nimport { memoServiceClient, userServiceClient } from \"@/connect\";\nimport useAsyncEffect from \"@/hooks/useAsyncEffect\";\nimport useNavigateTo from \"@/hooks/useNavigateTo\";\nimport { useUser } from \"@/hooks/useUserQueries\";\nimport { handleError } from \"@/lib/error\";\nimport { cn } from \"@/lib/utils\";\nimport { Memo } from \"@/types/proto/api/v1/memo_service_pb\";\nimport { UserNotification, UserNotification_Status } from \"@/types/proto/api/v1/user_service_pb\";\nimport { useTranslate } from \"@/utils/i18n\";\n\ninterface Props {\n  notification: UserNotification;\n}\n\nfunction MemoCommentMessage({ notification }: Props) {\n  const t = useTranslate();\n  const navigateTo = useNavigateTo();\n  const [relatedMemo, setRelatedMemo] = useState<Memo | undefined>(undefined);\n  const [commentMemo, setCommentMemo] = useState<Memo | undefined>(undefined);\n  const [senderName, setSenderName] = useState<string | undefined>(undefined);\n  const [initialized, setInitialized] = useState<boolean>(false);\n  const [hasError, setHasError] = useState<boolean>(false);\n\n  const { data: sender } = useUser(senderName || \"\", { enabled: !!senderName });\n\n  useAsyncEffect(async () => {\n    if (notification.payload?.case !== \"memoComment\") {\n      setHasError(true);\n      return;\n    }\n\n    try {\n      const memoCommentPayload = notification.payload.value;\n      const memo = await memoServiceClient.getMemo({\n        name: memoCommentPayload.relatedMemo,\n      });\n      setRelatedMemo(memo);\n\n      const comment = await memoServiceClient.getMemo({\n        name: memoCommentPayload.memo,\n      });\n      setCommentMemo(comment);\n\n      setSenderName(notification.sender);\n      setInitialized(true);\n    } catch (error) {\n      handleError(error, () => {}, {\n        context: \"Failed to fetch memo comment notification\",\n        onError: () => setHasError(true),\n      });\n      return;\n    }\n  }, [notification.payload, notification.sender]);\n\n  const handleNavigateToMemo = async () => {\n    if (!relatedMemo) {\n      return;\n    }\n\n    navigateTo(`/${relatedMemo.name}`);\n    if (notification.status === UserNotification_Status.UNREAD) {\n      handleArchiveMessage(true);\n    }\n  };\n\n  const handleArchiveMessage = async (silence = false) => {\n    await userServiceClient.updateUserNotification({\n      notification: {\n        name: notification.name,\n        status: UserNotification_Status.ARCHIVED,\n      },\n      updateMask: create(FieldMaskSchema, { paths: [\"status\"] }),\n    });\n    if (!silence) {\n      toast.success(t(\"message.archived-successfully\"));\n    }\n  };\n\n  const handleDeleteMessage = async () => {\n    await userServiceClient.deleteUserNotification({\n      name: notification.name,\n    });\n    toast.success(t(\"message.deleted-successfully\"));\n  };\n\n  if (!initialized && !hasError) {\n    return (\n      <div className=\"w-full px-5 py-4 border-b border-border/60 last:border-b-0 bg-muted/10 animate-pulse\">\n        <div className=\"flex items-start gap-3\">\n          <div className=\"w-10 h-10 rounded-full bg-muted/50 shrink-0\" />\n          <div className=\"flex-1 space-y-3\">\n            <div className=\"h-4 bg-muted/50 rounded-md w-2/5\" />\n            <div className=\"h-3 bg-muted/40 rounded-md w-3/4\" />\n            <div className=\"h-20 bg-muted/30 rounded-xl\" />\n          </div>\n        </div>\n      </div>\n    );\n  }\n\n  if (hasError) {\n    return (\n      <div className=\"w-full px-5 py-4 border-b border-border/60 last:border-b-0 bg-destructive/[0.04] group\">\n        <div className=\"flex items-center justify-between\">\n          <div className=\"flex items-center gap-3\">\n            <div className=\"w-10 h-10 rounded-full bg-destructive/15 flex items-center justify-center shrink-0 ring-1 ring-destructive/20\">\n              <XIcon className=\"w-5 h-5 text-destructive\" strokeWidth={2} />\n            </div>\n            <span className=\"text-sm text-destructive/80 font-medium\">{t(\"inbox.failed-to-load\")}</span>\n          </div>\n          <button\n            onClick={handleDeleteMessage}\n            className=\"p-1.5 hover:bg-destructive/15 rounded-lg transition-all duration-150 opacity-0 group-hover:opacity-100\"\n            title={t(\"common.delete\")}\n          >\n            <TrashIcon className=\"w-4 h-4 text-destructive/70 hover:text-destructive transition-colors\" strokeWidth={2} />\n          </button>\n        </div>\n      </div>\n    );\n  }\n\n  const isUnread = notification.status === UserNotification_Status.UNREAD;\n\n  return (\n    <div\n      className={cn(\n        \"w-full px-5 py-4 border-b border-border/60 last:border-b-0 transition-all duration-200 group relative\",\n        isUnread ? \"bg-primary/[0.03] hover:bg-primary/[0.05]\" : \"hover:bg-muted/30\",\n      )}\n    >\n      {/* Unread indicator bar */}\n      {isUnread && <div className=\"absolute left-0 top-0 bottom-0 w-0.5 bg-gradient-to-b from-primary to-primary/60\" />}\n\n      <div className=\"flex items-start gap-3\">\n        {/* Avatar & Icon */}\n        <div className=\"relative shrink-0\">\n          <UserAvatar className=\"w-10 h-10 ring-1 ring-border/40\" avatarUrl={sender?.avatarUrl} />\n          <div\n            className={cn(\n              \"absolute -bottom-1 -right-1 w-5 h-5 rounded-full border-2 border-background flex items-center justify-center shadow-md transition-all\",\n              isUnread ? \"bg-primary text-primary-foreground\" : \"bg-muted/80 text-muted-foreground\",\n            )}\n          >\n            <MessageCircleIcon className=\"w-2.5 h-2.5\" strokeWidth={2.5} />\n          </div>\n        </div>\n\n        {/* Content */}\n        <div className=\"flex-1 min-w-0\">\n          {/* Header */}\n          <div className=\"flex items-center justify-between gap-3 mb-1\">\n            <div className=\"flex items-center gap-1.5 flex-wrap min-w-0\">\n              <span className=\"font-semibold text-sm text-foreground/95\">{sender?.displayName || sender?.username}</span>\n              <span className=\"text-sm text-muted-foreground/80\">commented on your memo</span>\n              <span className=\"text-xs text-muted-foreground/60\">\n                {notification.createTime &&\n                  timestampDate(notification.createTime)?.toLocaleDateString([], { month: \"short\", day: \"numeric\" })}{\" \"}\n                at{\" \"}\n                {notification.createTime &&\n                  timestampDate(notification.createTime)?.toLocaleTimeString([], { hour: \"2-digit\", minute: \"2-digit\" })}\n              </span>\n            </div>\n            <div className=\"flex items-center gap-1 shrink-0\">\n              {isUnread ? (\n                <button\n                  onClick={() => handleArchiveMessage()}\n                  className=\"p-1.5 hover:bg-primary/10 rounded-lg transition-all duration-150 opacity-0 group-hover:opacity-100\"\n                  title={t(\"common.archive\")}\n                >\n                  <CheckIcon className=\"w-4 h-4 text-muted-foreground hover:text-primary transition-colors\" strokeWidth={2} />\n                </button>\n              ) : (\n                <button\n                  onClick={handleDeleteMessage}\n                  className=\"p-1.5 hover:bg-destructive/10 rounded-lg transition-all duration-150 opacity-0 group-hover:opacity-100\"\n                  title={t(\"common.delete\")}\n                >\n                  <TrashIcon className=\"w-4 h-4 text-muted-foreground hover:text-destructive transition-colors\" strokeWidth={2} />\n                </button>\n              )}\n            </div>\n          </div>\n\n          {/* Original Memo Snippet */}\n          {relatedMemo && (\n            <div className=\"pl-3 border-l-2 border-muted-foreground/20 mb-3\">\n              <p className=\"text-sm text-foreground/60 line-clamp-1 leading-relaxed\">\n                <span className=\"text-xs text-muted-foreground/50 font-medium mr-2 uppercase tracking-wide\">Original:</span>\n                {relatedMemo.content || <span className=\"italic text-muted-foreground/40\">Empty memo</span>}\n              </p>\n            </div>\n          )}\n\n          {/* Comment Preview */}\n          {commentMemo && (\n            <div\n              onClick={handleNavigateToMemo}\n              className=\"p-2 sm:p-3 rounded-lg bg-gradient-to-br from-primary/[0.06] to-primary/[0.03] hover:from-primary/[0.1] hover:to-primary/[0.06] cursor-pointer border border-primary/30 hover:border-primary/50 transition-all duration-200 group/comment shadow-sm hover:shadow\"\n            >\n              <div className=\"flex items-start gap-2\">\n                <div className=\"w-5 h-5 flex items-center justify-center shrink-0\">\n                  <MessageCircleIcon className=\"w-4 h-4 text-primary\" />\n                </div>\n                <div className=\"flex-1 min-w-0\">\n                  <p className=\"text-xs text-primary/60 font-semibold mb-1 uppercase tracking-wider\">Comment</p>\n                  <p className=\"text-sm text-foreground/90 line-clamp-2\">\n                    {commentMemo.content || <span className=\"italic text-muted-foreground/50\">Empty comment</span>}\n                  </p>\n                </div>\n              </div>\n            </div>\n          )}\n        </div>\n      </div>\n    </div>\n  );\n}\n\nexport default MemoCommentMessage;\n"
  },
  {
    "path": "web/src/components/LearnMore.tsx",
    "content": "import { ExternalLinkIcon } from \"lucide-react\";\nimport { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from \"@/components/ui/tooltip\";\nimport { useTranslate } from \"@/utils/i18n\";\n\ninterface Props {\n  className?: string;\n  url: string;\n  title?: string;\n}\n\nconst LearnMore: React.FC<Props> = (props: Props) => {\n  const { className, url, title } = props;\n  const t = useTranslate();\n\n  return (\n    <TooltipProvider>\n      <Tooltip>\n        <TooltipTrigger asChild>\n          <a className={`text-muted-foreground hover:text-primary ${className}`} href={url} target=\"_blank\">\n            <ExternalLinkIcon className=\"w-4 h-auto\" />\n          </a>\n        </TooltipTrigger>\n        <TooltipContent>\n          <p>{title ?? t(\"common.learn-more\")}</p>\n        </TooltipContent>\n      </Tooltip>\n    </TooltipProvider>\n  );\n};\n\nexport default LearnMore;\n"
  },
  {
    "path": "web/src/components/LocaleSelect.tsx",
    "content": "import { GlobeIcon } from \"lucide-react\";\nimport { FC } from \"react\";\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from \"@/components/ui/select\";\nimport { locales } from \"@/i18n\";\nimport { getLocaleDisplayName, loadLocale } from \"@/utils/i18n\";\n\ninterface Props {\n  value: Locale;\n  onChange: (locale: Locale) => void;\n}\n\nconst LocaleSelect: FC<Props> = (props: Props) => {\n  const { onChange, value } = props;\n\n  const handleSelectChange = async (locale: Locale) => {\n    // Apply locale globally immediately\n    loadLocale(locale);\n    // Also notify parent component\n    onChange(locale);\n  };\n\n  return (\n    <Select value={value} onValueChange={handleSelectChange}>\n      <SelectTrigger>\n        <div className=\"flex items-center gap-2\">\n          <GlobeIcon className=\"w-4 h-auto\" />\n          <SelectValue placeholder=\"Select language\" />\n        </div>\n      </SelectTrigger>\n      <SelectContent>\n        {locales.map((locale) => (\n          <SelectItem key={locale} value={locale}>\n            {getLocaleDisplayName(locale)}\n          </SelectItem>\n        ))}\n      </SelectContent>\n    </Select>\n  );\n};\n\nexport default LocaleSelect;\n"
  },
  {
    "path": "web/src/components/MemoActionMenu/MemoActionMenu.tsx",
    "content": "import {\n  ArchiveIcon,\n  ArchiveRestoreIcon,\n  BookmarkMinusIcon,\n  BookmarkPlusIcon,\n  CopyIcon,\n  Edit3Icon,\n  FileTextIcon,\n  LinkIcon,\n  MoreVerticalIcon,\n  TrashIcon,\n} from \"lucide-react\";\nimport { useState } from \"react\";\nimport ConfirmDialog from \"@/components/ConfirmDialog\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuSub,\n  DropdownMenuSubContent,\n  DropdownMenuSubTrigger,\n  DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu\";\nimport { State } from \"@/types/proto/api/v1/common_pb\";\nimport { useTranslate } from \"@/utils/i18n\";\nimport { useMemoActionHandlers } from \"./hooks\";\nimport type { MemoActionMenuProps } from \"./types\";\n\nconst MemoActionMenu = (props: MemoActionMenuProps) => {\n  const { memo, readonly } = props;\n  const t = useTranslate();\n\n  // Dialog state\n  const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);\n\n  // Derived state\n  const isComment = Boolean(memo.parent);\n  const isArchived = memo.state === State.ARCHIVED;\n\n  // Action handlers\n  const {\n    handleTogglePinMemoBtnClick,\n    handleEditMemoClick,\n    handleToggleMemoStatusClick,\n    handleCopyLink,\n    handleCopyContent,\n    handleDeleteMemoClick,\n    confirmDeleteMemo,\n  } = useMemoActionHandlers({\n    memo,\n    onEdit: props.onEdit,\n    setDeleteDialogOpen,\n  });\n\n  return (\n    <DropdownMenu>\n      <DropdownMenuTrigger asChild>\n        <Button variant=\"ghost\" size=\"icon\" className=\"size-4\">\n          <MoreVerticalIcon className=\"text-muted-foreground\" />\n        </Button>\n      </DropdownMenuTrigger>\n      <DropdownMenuContent align=\"end\" sideOffset={2}>\n        {/* Edit actions (non-readonly, non-archived) */}\n        {!readonly && !isArchived && (\n          <>\n            {!isComment && (\n              <DropdownMenuItem onClick={handleTogglePinMemoBtnClick}>\n                {memo.pinned ? <BookmarkMinusIcon className=\"w-4 h-auto\" /> : <BookmarkPlusIcon className=\"w-4 h-auto\" />}\n                {memo.pinned ? t(\"common.unpin\") : t(\"common.pin\")}\n              </DropdownMenuItem>\n            )}\n            <DropdownMenuItem onClick={handleEditMemoClick}>\n              <Edit3Icon className=\"w-4 h-auto\" />\n              {t(\"common.edit\")}\n            </DropdownMenuItem>\n          </>\n        )}\n\n        {/* Copy submenu (non-archived) */}\n        {!isArchived && (\n          <DropdownMenuSub>\n            <DropdownMenuSubTrigger>\n              <CopyIcon className=\"w-4 h-auto\" />\n              {t(\"common.copy\")}\n            </DropdownMenuSubTrigger>\n            <DropdownMenuSubContent>\n              <DropdownMenuItem onClick={handleCopyLink}>\n                <LinkIcon className=\"w-4 h-auto\" />\n                {t(\"memo.copy-link\")}\n              </DropdownMenuItem>\n              <DropdownMenuItem onClick={handleCopyContent}>\n                <FileTextIcon className=\"w-4 h-auto\" />\n                {t(\"memo.copy-content\")}\n              </DropdownMenuItem>\n            </DropdownMenuSubContent>\n          </DropdownMenuSub>\n        )}\n\n        {/* Write actions (non-readonly) */}\n        {!readonly && (\n          <>\n            {/* Archive/Restore (non-comment) */}\n            {!isComment && (\n              <DropdownMenuItem onClick={handleToggleMemoStatusClick}>\n                {isArchived ? <ArchiveRestoreIcon className=\"w-4 h-auto\" /> : <ArchiveIcon className=\"w-4 h-auto\" />}\n                {isArchived ? t(\"common.restore\") : t(\"common.archive\")}\n              </DropdownMenuItem>\n            )}\n\n            {/* Delete */}\n            <DropdownMenuItem onClick={handleDeleteMemoClick}>\n              <TrashIcon className=\"w-4 h-auto\" />\n              {t(\"common.delete\")}\n            </DropdownMenuItem>\n          </>\n        )}\n      </DropdownMenuContent>\n\n      {/* Delete confirmation dialog */}\n      <ConfirmDialog\n        open={deleteDialogOpen}\n        onOpenChange={setDeleteDialogOpen}\n        title={t(\"memo.delete-confirm\")}\n        confirmLabel={t(\"common.delete\")}\n        description={t(\"memo.delete-confirm-description\")}\n        cancelLabel={t(\"common.cancel\")}\n        onConfirm={confirmDeleteMemo}\n        confirmVariant=\"destructive\"\n      />\n    </DropdownMenu>\n  );\n};\n\nexport default MemoActionMenu;\n"
  },
  {
    "path": "web/src/components/MemoActionMenu/hooks.ts",
    "content": "import { useQueryClient } from \"@tanstack/react-query\";\nimport copy from \"copy-to-clipboard\";\nimport { useCallback } from \"react\";\nimport toast from \"react-hot-toast\";\nimport { useLocation } from \"react-router-dom\";\nimport { useInstance } from \"@/contexts/InstanceContext\";\nimport { memoKeys, useDeleteMemo, useUpdateMemo } from \"@/hooks/useMemoQueries\";\nimport useNavigateTo from \"@/hooks/useNavigateTo\";\nimport { userKeys } from \"@/hooks/useUserQueries\";\nimport { handleError } from \"@/lib/error\";\nimport { State } from \"@/types/proto/api/v1/common_pb\";\nimport type { Memo } from \"@/types/proto/api/v1/memo_service_pb\";\nimport { useTranslate } from \"@/utils/i18n\";\n\ninterface UseMemoActionHandlersOptions {\n  memo: Memo;\n  onEdit?: () => void;\n  setDeleteDialogOpen: (open: boolean) => void;\n}\n\nexport const useMemoActionHandlers = ({ memo, onEdit, setDeleteDialogOpen }: UseMemoActionHandlersOptions) => {\n  const t = useTranslate();\n  const location = useLocation();\n  const navigateTo = useNavigateTo();\n  const queryClient = useQueryClient();\n  const { profile } = useInstance();\n  const { mutateAsync: updateMemo } = useUpdateMemo();\n  const { mutateAsync: deleteMemo } = useDeleteMemo();\n  const isInMemoDetailPage = location.pathname.startsWith(`/${memo.name}`);\n\n  const memoUpdatedCallback = useCallback(() => {\n    // Invalidate user stats to trigger refetch\n    queryClient.invalidateQueries({ queryKey: userKeys.stats() });\n  }, [queryClient]);\n\n  const handleTogglePinMemoBtnClick = useCallback(async () => {\n    try {\n      await updateMemo({\n        update: {\n          name: memo.name,\n          pinned: !memo.pinned,\n        },\n        updateMask: [\"pinned\"],\n      });\n    } catch {\n      // do nothing\n    }\n  }, [memo.name, memo.pinned, updateMemo]);\n\n  const handleEditMemoClick = useCallback(() => {\n    onEdit?.();\n  }, [onEdit]);\n\n  const handleToggleMemoStatusClick = useCallback(async () => {\n    const isArchiving = memo.state !== State.ARCHIVED;\n    const state = memo.state === State.ARCHIVED ? State.NORMAL : State.ARCHIVED;\n    const message = memo.state === State.ARCHIVED ? t(\"message.restored-successfully\") : t(\"message.archived-successfully\");\n\n    try {\n      await updateMemo({\n        update: {\n          name: memo.name,\n          state,\n        },\n        updateMask: [\"state\"],\n      });\n      toast.success(message);\n    } catch (error: unknown) {\n      handleError(error, toast.error, {\n        context: `${isArchiving ? \"Archive\" : \"Restore\"} memo`,\n        fallbackMessage: \"An error occurred\",\n      });\n      return;\n    }\n\n    if (isInMemoDetailPage) {\n      navigateTo(memo.state === State.ARCHIVED ? \"/\" : \"/archived\");\n    }\n    memoUpdatedCallback();\n  }, [memo.name, memo.state, t, isInMemoDetailPage, navigateTo, memoUpdatedCallback, updateMemo]);\n\n  const handleCopyLink = useCallback(() => {\n    let host = profile.instanceUrl;\n    if (host === \"\") {\n      host = window.location.origin;\n    }\n    copy(`${host}/${memo.name}`);\n    toast.success(t(\"message.succeed-copy-link\"));\n  }, [memo.name, t, profile.instanceUrl]);\n\n  const handleCopyContent = useCallback(() => {\n    copy(memo.content);\n    toast.success(t(\"message.succeed-copy-content\"));\n  }, [memo.content, t]);\n\n  const handleDeleteMemoClick = useCallback(() => {\n    setDeleteDialogOpen(true);\n  }, [setDeleteDialogOpen]);\n\n  const confirmDeleteMemo = useCallback(async () => {\n    try {\n      await deleteMemo(memo.name);\n    } catch (error: unknown) {\n      handleError(error, toast.error, { context: \"Delete memo\", fallbackMessage: \"An error occurred\" });\n      return;\n    }\n    toast.success(t(\"message.deleted-successfully\"));\n    if (memo.parent) {\n      queryClient.invalidateQueries({ queryKey: memoKeys.comments(memo.parent) });\n    }\n    if (isInMemoDetailPage) {\n      navigateTo(\"/\");\n    }\n    memoUpdatedCallback();\n  }, [memo.name, memo.parent, t, isInMemoDetailPage, navigateTo, memoUpdatedCallback, deleteMemo, queryClient]);\n\n  return {\n    handleTogglePinMemoBtnClick,\n    handleEditMemoClick,\n    handleToggleMemoStatusClick,\n    handleCopyLink,\n    handleCopyContent,\n    handleDeleteMemoClick,\n    confirmDeleteMemo,\n  };\n};\n"
  },
  {
    "path": "web/src/components/MemoActionMenu/index.ts",
    "content": "export { useMemoActionHandlers } from \"./hooks\";\nexport { default, default as MemoActionMenu } from \"./MemoActionMenu\";\nexport type { MemoActionMenuProps, UseMemoActionHandlersReturn } from \"./types\";\n"
  },
  {
    "path": "web/src/components/MemoActionMenu/types.ts",
    "content": "import type { Memo } from \"@/types/proto/api/v1/memo_service_pb\";\n\nexport interface MemoActionMenuProps {\n  memo: Memo;\n  readonly?: boolean;\n  className?: string;\n  onEdit?: () => void;\n}\n\nexport interface UseMemoActionHandlersReturn {\n  handleTogglePinMemoBtnClick: () => Promise<void>;\n  handleEditMemoClick: () => void;\n  handleToggleMemoStatusClick: () => Promise<void>;\n  handleCopyLink: () => void;\n  handleCopyContent: () => void;\n  handleDeleteMemoClick: () => void;\n  confirmDeleteMemo: () => Promise<void>;\n  handleRemoveCompletedTaskListItemsClick: () => void;\n  confirmRemoveCompletedTaskListItems: () => Promise<void>;\n}\n"
  },
  {
    "path": "web/src/components/MemoAttachment.tsx",
    "content": "import { Attachment } from \"@/types/proto/api/v1/attachment_service_pb\";\nimport { getAttachmentUrl, isMidiFile } from \"@/utils/attachment\";\nimport AttachmentIcon from \"./AttachmentIcon\";\n\ninterface Props {\n  attachment: Attachment;\n  className?: string;\n}\n\nconst MemoAttachment: React.FC<Props> = (props: Props) => {\n  const { className, attachment } = props;\n  const attachmentUrl = getAttachmentUrl(attachment);\n\n  const handlePreviewBtnClick = () => {\n    window.open(attachmentUrl);\n  };\n\n  return (\n    <div\n      className={`w-auto flex flex-row justify-start items-center text-muted-foreground hover:text-foreground hover:bg-accent rounded px-2 py-1 transition-colors ${className}`}\n    >\n      {attachment.type.startsWith(\"audio\") && !isMidiFile(attachment.type) ? (\n        <audio src={attachmentUrl} controls></audio>\n      ) : (\n        <>\n          <AttachmentIcon className=\"w-4! h-4! mr-1\" attachment={attachment} />\n          <span className=\"text-sm max-w-[256px] truncate cursor-pointer\" onClick={handlePreviewBtnClick}>\n            {attachment.filename}\n          </span>\n        </>\n      )}\n    </div>\n  );\n};\n\nexport default MemoAttachment;\n"
  },
  {
    "path": "web/src/components/MemoContent/CodeBlock.tsx",
    "content": "import copy from \"copy-to-clipboard\";\nimport hljs from \"highlight.js\";\nimport { CheckIcon, CopyIcon } from \"lucide-react\";\nimport { useEffect, useMemo, useState } from \"react\";\nimport { useAuth } from \"@/contexts/AuthContext\";\nimport { cn } from \"@/lib/utils\";\nimport { getThemeWithFallback, resolveTheme } from \"@/utils/theme\";\nimport { MermaidBlock } from \"./MermaidBlock\";\nimport type { ReactMarkdownProps } from \"./markdown/types\";\nimport { extractCodeContent, extractLanguage } from \"./utils\";\n\ninterface CodeBlockProps extends ReactMarkdownProps {\n  children?: React.ReactNode;\n  className?: string;\n}\n\nexport const CodeBlock = ({ children, className, node: _node, ...props }: CodeBlockProps) => {\n  const { userGeneralSetting } = useAuth();\n  const [copied, setCopied] = useState(false);\n\n  const codeElement = children as React.ReactElement;\n  const codeClassName = codeElement?.props?.className || \"\";\n  const codeContent = extractCodeContent(children);\n  const language = extractLanguage(codeClassName);\n\n  // If it's a mermaid block, render with MermaidBlock component\n  if (language === \"mermaid\") {\n    return (\n      <pre className=\"relative\">\n        <MermaidBlock className={cn(className)} {...props}>\n          {children}\n        </MermaidBlock>\n      </pre>\n    );\n  }\n\n  const theme = getThemeWithFallback(userGeneralSetting?.theme);\n  const resolvedTheme = resolveTheme(theme);\n  const isDarkTheme = resolvedTheme.includes(\"dark\");\n\n  // Dynamically load highlight.js theme based on app theme\n  useEffect(() => {\n    const dynamicImportStyle = async () => {\n      // Remove any existing highlight.js style\n      const existingStyle = document.querySelector(\"style[data-hljs-theme]\");\n      if (existingStyle) {\n        existingStyle.remove();\n      }\n\n      try {\n        const cssModule = isDarkTheme\n          ? await import(\"highlight.js/styles/github-dark-dimmed.css?inline\")\n          : await import(\"highlight.js/styles/github.css?inline\");\n\n        // Create and inject the style\n        const style = document.createElement(\"style\");\n        style.textContent = cssModule.default;\n        style.setAttribute(\"data-hljs-theme\", isDarkTheme ? \"dark\" : \"light\");\n        document.head.appendChild(style);\n      } catch (error) {\n        console.warn(\"Failed to load highlight.js theme:\", error);\n      }\n    };\n\n    dynamicImportStyle();\n  }, [resolvedTheme, isDarkTheme]);\n\n  // Highlight code using highlight.js\n  const highlightedCode = useMemo(() => {\n    try {\n      const lang = hljs.getLanguage(language);\n      if (lang) {\n        return hljs.highlight(codeContent, {\n          language: language,\n        }).value;\n      }\n    } catch {\n      // Skip error and use default highlighted code.\n    }\n\n    // Escape any HTML entities when rendering original content.\n    return Object.assign(document.createElement(\"span\"), {\n      textContent: codeContent,\n    }).innerHTML;\n  }, [language, codeContent]);\n\n  const handleCopy = async () => {\n    try {\n      // Try native clipboard API first (requires HTTPS or localhost)\n      if (navigator.clipboard && window.isSecureContext) {\n        await navigator.clipboard.writeText(codeContent);\n        setCopied(true);\n        setTimeout(() => setCopied(false), 2000);\n      } else {\n        // Fallback to copy-to-clipboard library for non-secure contexts\n        const success = copy(codeContent);\n        if (success) {\n          setCopied(true);\n          setTimeout(() => setCopied(false), 2000);\n        } else {\n          console.error(\"Failed to copy code\");\n        }\n      }\n    } catch (err) {\n      // If native API fails, try fallback\n      console.warn(\"Native clipboard failed, using fallback:\", err);\n      const success = copy(codeContent);\n      if (success) {\n        setCopied(true);\n        setTimeout(() => setCopied(false), 2000);\n      } else {\n        console.error(\"Failed to copy code:\", err);\n      }\n    }\n  };\n\n  return (\n    <pre className=\"relative my-2 rounded-lg border border-border bg-muted/20 overflow-hidden\">\n      {/* Header with language label and copy button */}\n      <div className=\"flex items-center justify-between px-2 py-1 border-b border-border bg-muted/30\">\n        <span className=\"text-xs text-foreground select-none\">{language || \"text\"}</span>\n        <button\n          onClick={handleCopy}\n          className={cn(\n            \"inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-xs\",\n            \"transition-colors duration-200\",\n            \"hover:bg-accent active:scale-95\",\n            copied ? \"text-primary\" : \"text-muted-foreground hover:text-foreground\",\n          )}\n          aria-label={copied ? \"Copied\" : \"Copy code\"}\n          title={copied ? \"Copied!\" : \"Copy code\"}\n        >\n          {copied ? (\n            <>\n              <CheckIcon className=\"w-3.5 h-3.5\" />\n              <span>Copied</span>\n            </>\n          ) : (\n            <>\n              <CopyIcon className=\"w-3.5 h-3.5\" />\n              <span>Copy</span>\n            </>\n          )}\n        </button>\n      </div>\n\n      {/* Code content */}\n      <div className=\"overflow-x-auto\">\n        <code\n          className={cn(\"block px-3 py-2 text-sm leading-relaxed\", `language-${language}`)}\n          dangerouslySetInnerHTML={{ __html: highlightedCode }}\n        />\n      </div>\n    </pre>\n  );\n};\n"
  },
  {
    "path": "web/src/components/MemoContent/ConditionalComponent.tsx",
    "content": "import type { Element } from \"hast\";\nimport React from \"react\";\nimport { isTagElement, isTaskListItemElement } from \"@/types/markdown\";\n\n/**\n * Creates a conditional component that renders different components\n * based on AST node type detection\n *\n * @param CustomComponent - Custom component to render when condition matches\n * @param DefaultComponent - Default component/element to render otherwise\n * @param condition - Function to test AST node\n * @returns Conditional wrapper component\n */\nexport const createConditionalComponent = <P extends Record<string, unknown>>(\n  CustomComponent: React.ComponentType<P>,\n  DefaultComponent: React.ComponentType<P> | keyof JSX.IntrinsicElements,\n  condition: (node: Element) => boolean,\n) => {\n  return (props: P & { node?: Element }) => {\n    const { node, ...restProps } = props;\n\n    // Check AST node to determine which component to use\n    if (node && condition(node)) {\n      return <CustomComponent {...(restProps as P)} node={node} />;\n    }\n\n    // Render default component/element\n    if (typeof DefaultComponent === \"string\") {\n      return React.createElement(DefaultComponent, restProps);\n    }\n    return <DefaultComponent {...(restProps as P)} />;\n  };\n};\n\n// Re-export type guards for convenience\nexport { isTagElement as isTagNode, isTaskListItemElement as isTaskListItemNode };\n"
  },
  {
    "path": "web/src/components/MemoContent/MermaidBlock.tsx",
    "content": "import mermaid from \"mermaid\";\nimport { useEffect, useMemo, useState } from \"react\";\nimport { useAuth } from \"@/contexts/AuthContext\";\nimport { cn } from \"@/lib/utils\";\nimport { getThemeWithFallback, resolveTheme, setupSystemThemeListener } from \"@/utils/theme\";\nimport { extractCodeContent } from \"./utils\";\n\ninterface MermaidBlockProps {\n  children?: React.ReactNode;\n  className?: string;\n}\n\ntype MermaidTheme = \"default\" | \"dark\";\n\nconst toMermaidTheme = (appTheme: string): MermaidTheme => (appTheme === \"default-dark\" ? \"dark\" : \"default\");\n\nconst formatErrorMessage = (err: unknown): string => {\n  const msg = err instanceof Error ? err.message : \"Failed to render diagram\";\n  if (/no diagram type detected/i.test(msg)) {\n    return `${msg} — check that the diagram type is valid (e.g. sequenceDiagram, classDiagram, erDiagram)`;\n  }\n  return msg;\n};\n\nexport const MermaidBlock = ({ children, className }: MermaidBlockProps) => {\n  const { userGeneralSetting } = useAuth();\n  const [svg, setSvg] = useState<string>(\"\");\n  const [error, setError] = useState<string>(\"\");\n  const [systemThemeChange, setSystemThemeChange] = useState(0);\n\n  const codeContent = extractCodeContent(children);\n  const themePreference = getThemeWithFallback(userGeneralSetting?.theme);\n  const currentTheme = useMemo(() => resolveTheme(themePreference), [themePreference, systemThemeChange]);\n\n  // Re-resolve theme when OS preference changes (only relevant when using \"system\" theme)\n  useEffect(() => {\n    if (themePreference !== \"system\") return;\n    return setupSystemThemeListener(() => setSystemThemeChange((n) => n + 1));\n  }, [themePreference]);\n\n  // Initialize Mermaid when theme changes\n  useEffect(() => {\n    mermaid.initialize({\n      startOnLoad: false,\n      theme: toMermaidTheme(currentTheme),\n      securityLevel: \"strict\",\n      fontFamily: \"inherit\",\n      suppressErrorRendering: true,\n    });\n  }, [currentTheme]);\n\n  // Render diagram when content or theme changes\n  useEffect(() => {\n    if (!codeContent) return;\n\n    const id = `mermaid-${Math.random().toString(36).substring(7)}`;\n\n    mermaid\n      .render(id, codeContent)\n      .then(({ svg: renderedSvg }) => {\n        setSvg(renderedSvg);\n        setError(\"\");\n      })\n      .catch((err) => {\n        console.error(\"Failed to render mermaid diagram:\", err);\n        setSvg(\"\");\n        setError(formatErrorMessage(err));\n      });\n  }, [codeContent, currentTheme]);\n\n  if (error) {\n    return (\n      <div className=\"w-full\">\n        <div className=\"text-sm text-destructive mb-2 whitespace-normal break-words\">Mermaid Error: {error}</div>\n        <code className=\"block language-mermaid whitespace-pre text-sm\">{codeContent}</code>\n      </div>\n    );\n  }\n\n  if (!svg) return null;\n\n  return (\n    <div\n      className={cn(\"mermaid-diagram w-full flex justify-center items-center my-2 overflow-x-auto\", className)}\n      dangerouslySetInnerHTML={{ __html: svg }}\n    />\n  );\n};\n"
  },
  {
    "path": "web/src/components/MemoContent/Table.tsx",
    "content": "import { cn } from \"@/lib/utils\";\nimport type { ReactMarkdownProps } from \"./markdown/types\";\n\ninterface TableProps extends React.HTMLAttributes<HTMLTableElement>, ReactMarkdownProps {\n  children: React.ReactNode;\n}\n\nexport const Table = ({ children, className, node: _node, ...props }: TableProps) => {\n  return (\n    <div className=\"my-2 w-full overflow-x-auto rounded-lg border border-border bg-muted/20\">\n      <table className={cn(\"w-full border-collapse text-sm\", className)} {...props}>\n        {children}\n      </table>\n    </div>\n  );\n};\n\ninterface TableHeadProps extends React.HTMLAttributes<HTMLTableSectionElement>, ReactMarkdownProps {\n  children: React.ReactNode;\n}\n\nexport const TableHead = ({ children, className, node: _node, ...props }: TableHeadProps) => {\n  return (\n    <thead className={cn(\"border-b border-border bg-muted/30\", className)} {...props}>\n      {children}\n    </thead>\n  );\n};\n\ninterface TableBodyProps extends React.HTMLAttributes<HTMLTableSectionElement>, ReactMarkdownProps {\n  children: React.ReactNode;\n}\n\nexport const TableBody = ({ children, className, node: _node, ...props }: TableBodyProps) => {\n  return (\n    <tbody className={cn(\"divide-y divide-border\", className)} {...props}>\n      {children}\n    </tbody>\n  );\n};\n\ninterface TableRowProps extends React.HTMLAttributes<HTMLTableRowElement>, ReactMarkdownProps {\n  children: React.ReactNode;\n}\n\nexport const TableRow = ({ children, className, node: _node, ...props }: TableRowProps) => {\n  return (\n    <tr className={cn(\"transition-colors hover:bg-accent/20\", className)} {...props}>\n      {children}\n    </tr>\n  );\n};\n\ninterface TableHeaderCellProps extends React.ThHTMLAttributes<HTMLTableCellElement>, ReactMarkdownProps {\n  children: React.ReactNode;\n}\n\nexport const TableHeaderCell = ({ children, className, node: _node, ...props }: TableHeaderCellProps) => {\n  return (\n    <th className={cn(\"px-2 py-1 text-left align-middle text-sm font-medium text-muted-foreground\", className)} {...props}>\n      {children}\n    </th>\n  );\n};\n\ninterface TableCellProps extends React.TdHTMLAttributes<HTMLTableCellElement>, ReactMarkdownProps {\n  children: React.ReactNode;\n}\n\nexport const TableCell = ({ children, className, node: _node, ...props }: TableCellProps) => {\n  return (\n    <td className={cn(\"px-2 py-1 text-left align-middle text-sm\", className)} {...props}>\n      {children}\n    </td>\n  );\n};\n"
  },
  {
    "path": "web/src/components/MemoContent/Tag.tsx",
    "content": "import type { Element } from \"hast\";\nimport { useLocation } from \"react-router-dom\";\nimport { type MemoFilter, stringifyFilters, useMemoFilterContext } from \"@/contexts/MemoFilterContext\";\nimport useNavigateTo from \"@/hooks/useNavigateTo\";\nimport { cn } from \"@/lib/utils\";\nimport { Routes } from \"@/router\";\nimport { useMemoViewContext } from \"../MemoView/MemoViewContext\";\n\ninterface TagProps extends React.HTMLAttributes<HTMLSpanElement> {\n  node?: Element; // AST node from react-markdown\n  \"data-tag\"?: string;\n  children?: React.ReactNode;\n}\n\nexport const Tag: React.FC<TagProps> = ({ \"data-tag\": dataTag, children, className, ...props }) => {\n  const { parentPage } = useMemoViewContext();\n  const location = useLocation();\n  const navigateTo = useNavigateTo();\n  const { getFiltersByFactor, removeFilter, addFilter } = useMemoFilterContext();\n\n  const tag = dataTag || \"\";\n\n  const handleTagClick = (e: React.MouseEvent) => {\n    e.stopPropagation();\n\n    // If the tag is clicked in a memo detail page, we should navigate to the memo list page.\n    if (location.pathname.startsWith(\"/m\")) {\n      const pathname = parentPage || Routes.ROOT;\n      const searchParams = new URLSearchParams();\n\n      searchParams.set(\"filter\", stringifyFilters([{ factor: \"tagSearch\", value: tag }]));\n      navigateTo(`${pathname}?${searchParams.toString()}`);\n      return;\n    }\n\n    const isActive = getFiltersByFactor(\"tagSearch\").some((filter: MemoFilter) => filter.value === tag);\n    if (isActive) {\n      removeFilter((f: MemoFilter) => f.factor === \"tagSearch\" && f.value === tag);\n    } else {\n      // Remove all existing tag filters first, then add the new one\n      removeFilter((f: MemoFilter) => f.factor === \"tagSearch\");\n      addFilter({\n        factor: \"tagSearch\",\n        value: tag,\n      });\n    }\n  };\n\n  return (\n    <span\n      className={cn(\"inline-block w-auto text-primary cursor-pointer transition-colors hover:text-primary/80\", className)}\n      data-tag={tag}\n      {...props}\n      onClick={handleTagClick}\n    >\n      {children}\n    </span>\n  );\n};\n"
  },
  {
    "path": "web/src/components/MemoContent/TaskListItem.tsx",
    "content": "import { useRef } from \"react\";\nimport { Checkbox } from \"@/components/ui/checkbox\";\nimport { useUpdateMemo } from \"@/hooks/useMemoQueries\";\nimport { toggleTaskAtIndex } from \"@/utils/markdown-manipulation\";\nimport { useMemoViewContext, useMemoViewDerived } from \"../MemoView/MemoViewContext\";\nimport { TASK_LIST_ITEM_CLASS } from \"./constants\";\nimport type { ReactMarkdownProps } from \"./markdown/types\";\n\ninterface TaskListItemProps extends React.InputHTMLAttributes<HTMLInputElement>, ReactMarkdownProps {\n  checked?: boolean;\n}\n\nexport const TaskListItem: React.FC<TaskListItemProps> = ({ checked, node: _node, ...props }) => {\n  const { memo } = useMemoViewContext();\n  const { readonly } = useMemoViewDerived();\n  const checkboxRef = useRef<HTMLButtonElement>(null);\n  const { mutate: updateMemo } = useUpdateMemo();\n\n  const handleChange = async (newChecked: boolean) => {\n    // Don't update if readonly or no memo\n    if (readonly || !memo) {\n      return;\n    }\n\n    // Find the task index by walking up the DOM\n    const listItem = checkboxRef.current?.closest(\"li.task-list-item\");\n    if (!listItem) {\n      return;\n    }\n\n    // Get task index from data attribute, or calculate by counting\n    const taskIndexStr = listItem.getAttribute(\"data-task-index\");\n    let taskIndex = 0;\n\n    if (taskIndexStr !== null) {\n      taskIndex = parseInt(taskIndexStr);\n    } else {\n      // Fallback: Calculate index by counting all task list items in the entire memo\n      // We need to search from the root memo content container, not just the nearest list\n      // to ensure nested tasks are counted in document order\n      let searchRoot = listItem.closest(\"[data-memo-content]\");\n\n      // If memo content container not found, search from document body\n      if (!searchRoot) {\n        searchRoot = document.body;\n      }\n\n      const allTaskItems = searchRoot.querySelectorAll(`li.${TASK_LIST_ITEM_CLASS}`);\n      for (let i = 0; i < allTaskItems.length; i++) {\n        if (allTaskItems[i] === listItem) {\n          taskIndex = i;\n          break;\n        }\n      }\n    }\n\n    // Update memo content using the string manipulation utility\n    const newContent = toggleTaskAtIndex(memo.content, taskIndex, newChecked);\n    updateMemo({\n      update: {\n        name: memo.name,\n        content: newContent,\n      },\n      updateMask: [\"content\"],\n    });\n  };\n\n  // Override the disabled prop from remark-gfm (which defaults to true)\n  return <Checkbox ref={checkboxRef} checked={checked} disabled={readonly} onCheckedChange={handleChange} className={props.className} />;\n};\n"
  },
  {
    "path": "web/src/components/MemoContent/constants.ts",
    "content": "import { defaultSchema } from \"rehype-sanitize\";\n\n// Class names added by remark-gfm for task lists\nexport const TASK_LIST_CLASS = \"contains-task-list\";\nexport const TASK_LIST_ITEM_CLASS = \"task-list-item\";\n\n// Compact mode display settings\nexport const COMPACT_MODE_CONFIG = {\n  maxHeightVh: 60, // 60% of viewport height\n  gradientHeight: \"h-24\", // Tailwind class for gradient overlay\n} as const;\n\nexport const getMaxDisplayHeight = () => window.innerHeight * (COMPACT_MODE_CONFIG.maxHeightVh / 100);\n\nexport const COMPACT_STATES: Record<\"ALL\" | \"SNIPPET\", { textKey: string; next: \"ALL\" | \"SNIPPET\" }> = {\n  ALL: { textKey: \"memo.show-more\", next: \"SNIPPET\" },\n  SNIPPET: { textKey: \"memo.show-less\", next: \"ALL\" },\n};\n\n/**\n * Sanitization schema for markdown HTML content.\n * Extends the default schema to allow:\n * - KaTeX math rendering elements (MathML tags)\n * - KaTeX-specific attributes (className, style, aria-*, data-*)\n * - Safe HTML elements for rich content\n * - iframe embeds for trusted video providers (YouTube, Vimeo, etc.)\n *\n * This prevents XSS attacks while preserving math rendering functionality.\n */\nexport const SANITIZE_SCHEMA = {\n  ...defaultSchema,\n  attributes: {\n    ...defaultSchema.attributes,\n    div: [...(defaultSchema.attributes?.div || []), \"className\"],\n    img: [...(defaultSchema.attributes?.img || []), \"height\", \"width\"],\n    span: [...(defaultSchema.attributes?.span || []), \"className\", \"style\", [\"aria*\"], [\"data*\"]],\n    // iframe attributes for video embeds\n    iframe: [\"src\", \"width\", \"height\", \"frameborder\", \"allowfullscreen\", \"allow\", \"title\", \"referrerpolicy\", \"loading\"],\n    // MathML attributes for KaTeX rendering\n    annotation: [\"encoding\"],\n    math: [\"xmlns\"],\n    mi: [],\n    mn: [],\n    mo: [],\n    mrow: [],\n    mspace: [],\n    mstyle: [],\n    msup: [],\n    msub: [],\n    msubsup: [],\n    mfrac: [],\n    mtext: [],\n    semantics: [],\n  },\n  tagNames: [\n    ...(defaultSchema.tagNames || []),\n    // iframe for video embeds\n    \"iframe\",\n    // MathML elements for KaTeX math rendering\n    \"math\",\n    \"annotation\",\n    \"semantics\",\n    \"mi\",\n    \"mn\",\n    \"mo\",\n    \"mrow\",\n    \"mspace\",\n    \"mstyle\",\n    \"msup\",\n    \"msub\",\n    \"msubsup\",\n    \"mfrac\",\n    \"mtext\",\n  ],\n  protocols: {\n    ...defaultSchema.protocols,\n    // Allow HTTPS iframe embeds only for security\n    iframe: { src: [\"https\"] },\n  },\n};\n"
  },
  {
    "path": "web/src/components/MemoContent/hooks.ts",
    "content": "import { useCallback, useEffect, useRef, useState } from \"react\";\nimport { COMPACT_STATES, getMaxDisplayHeight } from \"./constants\";\nimport type { ContentCompactView } from \"./types\";\n\nexport const useCompactMode = (enabled: boolean) => {\n  const containerRef = useRef<HTMLDivElement>(null);\n  const [mode, setMode] = useState<ContentCompactView | undefined>(undefined);\n\n  useEffect(() => {\n    if (!enabled || !containerRef.current) return;\n    const maxHeight = getMaxDisplayHeight();\n    if (containerRef.current.getBoundingClientRect().height > maxHeight) {\n      setMode(\"ALL\");\n    }\n  }, [enabled]);\n\n  const toggle = useCallback(() => {\n    if (!mode) return;\n    setMode(COMPACT_STATES[mode].next);\n  }, [mode]);\n\n  return { containerRef, mode, toggle };\n};\n\nexport const useCompactLabel = (mode: ContentCompactView | undefined, t: (key: string) => string): string => {\n  if (!mode) return \"\";\n  return t(COMPACT_STATES[mode].textKey);\n};\n"
  },
  {
    "path": "web/src/components/MemoContent/index.tsx",
    "content": "import type { Element } from \"hast\";\nimport { ChevronDown, ChevronUp } from \"lucide-react\";\nimport { memo } from \"react\";\nimport ReactMarkdown from \"react-markdown\";\nimport rehypeKatex from \"rehype-katex\";\nimport rehypeRaw from \"rehype-raw\";\nimport rehypeSanitize from \"rehype-sanitize\";\nimport remarkBreaks from \"remark-breaks\";\nimport remarkGfm from \"remark-gfm\";\nimport remarkMath from \"remark-math\";\nimport { cn } from \"@/lib/utils\";\nimport { useTranslate } from \"@/utils/i18n\";\nimport { remarkDisableSetext } from \"@/utils/remark-plugins/remark-disable-setext\";\nimport { remarkPreserveType } from \"@/utils/remark-plugins/remark-preserve-type\";\nimport { remarkTag } from \"@/utils/remark-plugins/remark-tag\";\nimport { CodeBlock } from \"./CodeBlock\";\nimport { isTagNode, isTaskListItemNode } from \"./ConditionalComponent\";\nimport { COMPACT_MODE_CONFIG, SANITIZE_SCHEMA } from \"./constants\";\nimport { useCompactLabel, useCompactMode } from \"./hooks\";\nimport { Blockquote, Heading, HorizontalRule, Image, InlineCode, Link, List, ListItem, Paragraph } from \"./markdown\";\nimport { Table, TableBody, TableCell, TableHead, TableHeaderCell, TableRow } from \"./Table\";\nimport { Tag } from \"./Tag\";\nimport { TaskListItem } from \"./TaskListItem\";\nimport type { MemoContentProps } from \"./types\";\n\nconst MemoContent = (props: MemoContentProps) => {\n  const { className, contentClassName, content, onClick, onDoubleClick } = props;\n  const t = useTranslate();\n  const {\n    containerRef: memoContentContainerRef,\n    mode: showCompactMode,\n    toggle: toggleCompactMode,\n  } = useCompactMode(Boolean(props.compact));\n\n  const compactLabel = useCompactLabel(showCompactMode, t as (key: string) => string);\n\n  return (\n    <div className={`w-full flex flex-col justify-start items-start text-foreground ${className || \"\"}`}>\n      <div\n        ref={memoContentContainerRef}\n        data-memo-content\n        className={cn(\n          \"relative w-full max-w-full wrap-break-word text-base leading-6\",\n          \"[&>*:last-child]:mb-0\",\n          showCompactMode === \"ALL\" && \"overflow-hidden\",\n          contentClassName,\n        )}\n        style={showCompactMode === \"ALL\" ? { maxHeight: `${COMPACT_MODE_CONFIG.maxHeightVh}vh` } : undefined}\n        onMouseUp={onClick}\n        onDoubleClick={onDoubleClick}\n      >\n        <ReactMarkdown\n          remarkPlugins={[remarkDisableSetext, remarkMath, remarkGfm, remarkBreaks, remarkTag, remarkPreserveType]}\n          rehypePlugins={[rehypeRaw, [rehypeSanitize, SANITIZE_SCHEMA], [rehypeKatex, { throwOnError: false, strict: false }]]}\n          components={{\n            // Child components consume from MemoViewContext directly\n            input: ((inputProps: React.ComponentProps<\"input\"> & { node?: Element }) => {\n              if (inputProps.node && isTaskListItemNode(inputProps.node)) {\n                return <TaskListItem {...inputProps} />;\n              }\n              return <input {...inputProps} />;\n            }) as React.ComponentType<React.ComponentProps<\"input\">>,\n            span: ((spanProps: React.ComponentProps<\"span\"> & { node?: Element }) => {\n              const { node, ...rest } = spanProps;\n              if (node && isTagNode(node)) {\n                return <Tag {...spanProps} />;\n              }\n              return <span {...rest} />;\n            }) as React.ComponentType<React.ComponentProps<\"span\">>,\n            // Headings\n            h1: ({ children }) => <Heading level={1}>{children}</Heading>,\n            h2: ({ children }) => <Heading level={2}>{children}</Heading>,\n            h3: ({ children }) => <Heading level={3}>{children}</Heading>,\n            h4: ({ children }) => <Heading level={4}>{children}</Heading>,\n            h5: ({ children }) => <Heading level={5}>{children}</Heading>,\n            h6: ({ children }) => <Heading level={6}>{children}</Heading>,\n            // Block elements\n            p: ({ children }) => <Paragraph>{children}</Paragraph>,\n            blockquote: ({ children }) => <Blockquote>{children}</Blockquote>,\n            hr: () => <HorizontalRule />,\n            // Lists\n            ul: ({ children, ...props }) => <List {...props}>{children}</List>,\n            ol: ({ children, ...props }) => (\n              <List ordered {...props}>\n                {children}\n              </List>\n            ),\n            li: ({ children, ...props }) => <ListItem {...props}>{children}</ListItem>,\n            // Inline elements\n            a: ({ children, ...props }) => <Link {...props}>{children}</Link>,\n            code: ({ children }) => <InlineCode>{children}</InlineCode>,\n            img: ({ ...props }) => <Image {...props} />,\n            // Code blocks\n            pre: CodeBlock,\n            // Tables\n            table: ({ children }) => <Table>{children}</Table>,\n            thead: ({ children }) => <TableHead>{children}</TableHead>,\n            tbody: ({ children }) => <TableBody>{children}</TableBody>,\n            tr: ({ children }) => <TableRow>{children}</TableRow>,\n            th: ({ children, ...props }) => <TableHeaderCell {...props}>{children}</TableHeaderCell>,\n            td: ({ children, ...props }) => <TableCell {...props}>{children}</TableCell>,\n          }}\n        >\n          {content}\n        </ReactMarkdown>\n        {showCompactMode === \"ALL\" && (\n          <div\n            className={cn(\n              \"absolute inset-x-0 bottom-0 pointer-events-none\",\n              COMPACT_MODE_CONFIG.gradientHeight,\n              \"bg-linear-to-t from-background from-0% via-background/60 via-40% to-transparent to-100%\",\n            )}\n          />\n        )}\n      </div>\n      {showCompactMode !== undefined && (\n        <div className=\"relative w-full mt-2\">\n          <button\n            type=\"button\"\n            className=\"group inline-flex items-center gap-1 px-2 py-1 text-xs text-muted-foreground hover:text-foreground transition-colors\"\n            onClick={toggleCompactMode}\n          >\n            <span>{compactLabel}</span>\n            {showCompactMode === \"ALL\" ? <ChevronDown className=\"w-3 h-3\" /> : <ChevronUp className=\"w-3 h-3\" />}\n          </button>\n        </div>\n      )}\n    </div>\n  );\n};\n\nexport default memo(MemoContent);\n"
  },
  {
    "path": "web/src/components/MemoContent/markdown/Blockquote.tsx",
    "content": "import { cn } from \"@/lib/utils\";\nimport type { ReactMarkdownProps } from \"./types\";\n\ninterface BlockquoteProps extends React.BlockquoteHTMLAttributes<HTMLQuoteElement>, ReactMarkdownProps {\n  children: React.ReactNode;\n}\n\n/**\n * Blockquote component with left border accent\n */\nexport const Blockquote = ({ children, className, node: _node, ...props }: BlockquoteProps) => {\n  return (\n    <blockquote className={cn(\"my-0 mb-2 border-l-4 border-primary/30 pl-3 text-muted-foreground italic\", className)} {...props}>\n      {children}\n    </blockquote>\n  );\n};\n"
  },
  {
    "path": "web/src/components/MemoContent/markdown/Heading.tsx",
    "content": "import { cn } from \"@/lib/utils\";\nimport type { ReactMarkdownProps } from \"./types\";\n\ninterface HeadingProps extends React.HTMLAttributes<HTMLHeadingElement>, ReactMarkdownProps {\n  level: 1 | 2 | 3 | 4 | 5 | 6;\n  children: React.ReactNode;\n}\n\n/**\n * Heading component for h1-h6 elements\n * Renders semantic heading levels with consistent styling\n */\nexport const Heading = ({ level, children, className, node: _node, ...props }: HeadingProps) => {\n  const Component = `h${level}` as const;\n\n  const levelClasses = {\n    1: \"text-3xl font-bold border-b border-border pb-2\",\n    2: \"text-2xl font-semibold border-b border-border pb-1.5\",\n    3: \"text-xl font-semibold\",\n    4: \"text-lg font-semibold\",\n    5: \"text-base font-semibold\",\n    6: \"text-base font-medium text-muted-foreground\",\n  };\n\n  return (\n    <Component className={cn(\"mt-3 mb-2 leading-tight\", levelClasses[level], className)} {...props}>\n      {children}\n    </Component>\n  );\n};\n"
  },
  {
    "path": "web/src/components/MemoContent/markdown/HorizontalRule.tsx",
    "content": "import { cn } from \"@/lib/utils\";\nimport type { ReactMarkdownProps } from \"./types\";\n\ninterface HorizontalRuleProps extends React.HTMLAttributes<HTMLHRElement>, ReactMarkdownProps {}\n\n/**\n * Horizontal rule separator\n */\nexport const HorizontalRule = ({ className, node: _node, ...props }: HorizontalRuleProps) => {\n  return <hr className={cn(\"my-2 h-0 border-0 border-b border-border\", className)} {...props} />;\n};\n"
  },
  {
    "path": "web/src/components/MemoContent/markdown/Image.tsx",
    "content": "import { cn } from \"@/lib/utils\";\nimport type { ReactMarkdownProps } from \"./types\";\n\ninterface ImageProps extends React.ImgHTMLAttributes<HTMLImageElement>, ReactMarkdownProps {}\n\n/**\n * Image component for markdown images\n * Responsive with rounded corners\n */\nexport const Image = ({ className, alt, node: _node, height, width, style, ...props }: ImageProps) => {\n  return (\n    <img\n      className={cn(\"max-w-full my-2\", !height && \"h-auto\", className)}\n      alt={alt}\n      style={{ height: height ? `${height}px` : undefined, width: width ? `${width}px` : undefined, ...style }}\n      {...props}\n    />\n  );\n};\n"
  },
  {
    "path": "web/src/components/MemoContent/markdown/InlineCode.tsx",
    "content": "import { cn } from \"@/lib/utils\";\nimport type { ReactMarkdownProps } from \"./types\";\n\ninterface InlineCodeProps extends React.HTMLAttributes<HTMLElement>, ReactMarkdownProps {\n  children: React.ReactNode;\n}\n\n/**\n * Inline code component with background and monospace font\n */\nexport const InlineCode = ({ children, className, node: _node, ...props }: InlineCodeProps) => {\n  return (\n    <code className={cn(\"font-mono text-sm bg-muted px-1 py-0.5 rounded-md\", className)} {...props}>\n      {children}\n    </code>\n  );\n};\n"
  },
  {
    "path": "web/src/components/MemoContent/markdown/Link.tsx",
    "content": "import { cn } from \"@/lib/utils\";\nimport type { ReactMarkdownProps } from \"./types\";\n\ninterface LinkProps extends React.AnchorHTMLAttributes<HTMLAnchorElement>, ReactMarkdownProps {\n  children: React.ReactNode;\n}\n\n/**\n * Link component for external links\n * Opens in new tab with security attributes\n */\nexport const Link = ({ children, className, href, node: _node, ...props }: LinkProps) => {\n  return (\n    <a\n      href={href}\n      target=\"_blank\"\n      rel=\"noopener noreferrer\"\n      className={cn(\n        \"text-primary underline decoration-primary/50 underline-offset-2 transition-colors hover:decoration-primary\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n    </a>\n  );\n};\n"
  },
  {
    "path": "web/src/components/MemoContent/markdown/List.tsx",
    "content": "import { cn } from \"@/lib/utils\";\nimport { TASK_LIST_CLASS, TASK_LIST_ITEM_CLASS } from \"../constants\";\nimport type { ReactMarkdownProps } from \"./types\";\n\ninterface ListProps extends React.HTMLAttributes<HTMLUListElement | HTMLOListElement>, ReactMarkdownProps {\n  ordered?: boolean;\n  children: React.ReactNode;\n}\n\n/**\n * List component for both regular and task lists (GFM)\n * Detects task lists via the \"contains-task-list\" class added by remark-gfm\n */\nexport const List = ({ ordered, children, className, node: _node, ...domProps }: ListProps) => {\n  const Component = ordered ? \"ol\" : \"ul\";\n  const isTaskList = className?.includes(TASK_LIST_CLASS);\n\n  return (\n    <Component\n      className={cn(\n        \"my-0 mb-2 list-outside\",\n        isTaskList\n          ? // Task list: no bullets, nested lists get left margin for indentation\n            \"list-none [&_ul.contains-task-list]:ml-6\"\n          : // Regular list: standard padding and list style\n            cn(\"pl-6\", ordered ? \"list-decimal\" : \"list-disc\"),\n        className,\n      )}\n      {...domProps}\n    >\n      {children}\n    </Component>\n  );\n};\n\ninterface ListItemProps extends React.LiHTMLAttributes<HTMLLIElement>, ReactMarkdownProps {\n  children: React.ReactNode;\n}\n\n/**\n * List item component for both regular and task list items\n * Detects task items via the \"task-list-item\" class added by remark-gfm\n * Applies specialized styling for task checkboxes\n */\nexport const ListItem = ({ children, className, node: _node, ...domProps }: ListItemProps) => {\n  const isTaskListItem = className?.includes(TASK_LIST_ITEM_CLASS);\n\n  if (isTaskListItem) {\n    return (\n      <li\n        className={cn(\n          \"mt-0.5 leading-6 list-none\",\n          // Checkbox styling: margin and alignment\n          \"[&>button]:mr-2 [&>button]:align-middle\",\n          // Inline paragraph for task text\n          \"[&>p]:inline [&>p]:m-0\",\n          className,\n        )}\n        {...domProps}\n      >\n        {children}\n      </li>\n    );\n  }\n\n  return (\n    <li className={cn(\"mt-0.5 leading-6\", className)} {...domProps}>\n      {children}\n    </li>\n  );\n};\n"
  },
  {
    "path": "web/src/components/MemoContent/markdown/Paragraph.tsx",
    "content": "import { cn } from \"@/lib/utils\";\nimport type { ReactMarkdownProps } from \"./types\";\n\ninterface ParagraphProps extends React.HTMLAttributes<HTMLParagraphElement>, ReactMarkdownProps {\n  children: React.ReactNode;\n}\n\n/**\n * Paragraph component with compact spacing\n */\nexport const Paragraph = ({ children, className, node: _node, ...props }: ParagraphProps) => {\n  return (\n    <p className={cn(\"my-0 mb-2 leading-6\", className)} {...props}>\n      {children}\n    </p>\n  );\n};\n"
  },
  {
    "path": "web/src/components/MemoContent/markdown/README.md",
    "content": "# Markdown Components\n\nModern, type-safe React components for rendering markdown content via react-markdown.\n\n## Architecture\n\n### Component-Based Rendering\nFollowing patterns from popular AI chat apps (ChatGPT, Claude, Perplexity), we use React components instead of CSS selectors for markdown rendering. This provides:\n\n- **Type Safety**: Full TypeScript support with proper prop types\n- **Maintainability**: Components are easier to test, modify, and understand\n- **Performance**: No CSS specificity conflicts, cleaner DOM\n- **Modularity**: Each element is independently styled and documented\n\n### Type System\n\nAll components extend `ReactMarkdownProps` which includes the AST `node` prop passed by react-markdown. This is explicitly destructured as `node: _node` to:\n1. Filter it from DOM props (avoids `node=\"[object Object]\"` in HTML)\n2. Keep it available for advanced use cases (e.g., detecting task lists)\n3. Maintain type safety without `as any` casts\n\n### GFM Task Lists\n\nTask lists (from remark-gfm) are handled by:\n- **Detection**: `contains-task-list` and `task-list-item` classes from remark-gfm\n- **Styling**: Tailwind utilities with arbitrary variants for nested elements\n- **Checkboxes**: Custom `TaskListItem` component with Radix UI checkbox\n- **Interactivity**: Updates memo content via `toggleTaskAtIndex` utility\n\n### Component Patterns\n\nEach component follows this structure:\n```tsx\nimport { cn } from \"@/lib/utils\";\nimport type { ReactMarkdownProps } from \"./types\";\n\ninterface ComponentProps extends React.HTMLAttributes<HTMLElement>, ReactMarkdownProps {\n  children?: React.ReactNode;\n  // component-specific props\n}\n\n/**\n * JSDoc description\n */\nexport const Component = ({ children, className, node: _node, ...props }: ComponentProps) => {\n  return (\n    <element className={cn(\"base-classes\", className)} {...props}>\n      {children}\n    </element>\n  );\n};\n```\n\n## Components\n\n| Component | Element | Purpose |\n|-----------|---------|---------|\n| `Heading` | h1-h6 | Semantic headings with level-based styling |\n| `Paragraph` | p | Compact paragraphs with consistent spacing |\n| `Link` | a | External links with security attributes |\n| `List` | ul/ol | Regular and GFM task lists |\n| `ListItem` | li | List items with task checkbox support |\n| `Blockquote` | blockquote | Quotes with left border accent |\n| `InlineCode` | code | Inline code with background |\n| `Image` | img | Responsive images with rounded corners |\n| `HorizontalRule` | hr | Section separators |\n\n## Styling Approach\n\n- **Tailwind CSS**: All styling uses Tailwind utilities\n- **Design Tokens**: Colors use CSS variables (e.g., `--primary`, `--muted-foreground`)\n- **Responsive**: Max-width constraints, responsive images\n- **Accessibility**: Semantic HTML, proper ARIA attributes via Radix UI\n\n## Integration\n\nComponents are mapped to HTML elements in `MemoContent/index.tsx`:\n\n```tsx\n<ReactMarkdown\n  components={{\n    h1: ({ children }) => <Heading level={1}>{children}</Heading>,\n    p: ({ children, ...props }) => <Paragraph {...props}>{children}</Paragraph>,\n    // ... more mappings\n  }}\n>\n  {content}\n</ReactMarkdown>\n```\n\n## Future Enhancements\n\n- [ ] Syntax highlighting themes for code blocks\n- [ ] Table sorting/filtering interactions\n- [ ] Image lightbox/zoom functionality\n- [ ] Collapsible sections for long content\n- [ ] Copy button for code blocks\n"
  },
  {
    "path": "web/src/components/MemoContent/markdown/index.ts",
    "content": "export { Blockquote } from \"./Blockquote\";\nexport { Heading } from \"./Heading\";\nexport { HorizontalRule } from \"./HorizontalRule\";\nexport { Image } from \"./Image\";\nexport { InlineCode } from \"./InlineCode\";\nexport { Link } from \"./Link\";\nexport { List, ListItem } from \"./List\";\nexport { Paragraph } from \"./Paragraph\";\n"
  },
  {
    "path": "web/src/components/MemoContent/markdown/types.ts",
    "content": "import type { Element } from \"hast\";\n\n/**\n * Props passed by react-markdown to custom components\n * Includes the AST node for advanced use cases\n */\nexport interface ReactMarkdownProps {\n  node?: Element;\n}\n"
  },
  {
    "path": "web/src/components/MemoContent/types.ts",
    "content": "import type React from \"react\";\n\nexport interface MemoContentProps {\n  content: string;\n  compact?: boolean;\n  className?: string;\n  contentClassName?: string;\n  onClick?: (e: React.MouseEvent) => void;\n  onDoubleClick?: (e: React.MouseEvent) => void;\n}\n\nexport type ContentCompactView = \"ALL\" | \"SNIPPET\";\n"
  },
  {
    "path": "web/src/components/MemoContent/utils.ts",
    "content": "import type React from \"react\";\n\n/**\n * Extracts code content from a react-markdown code element.\n * Handles the nested structure where code is passed as children.\n *\n * @param children - The children prop from react-markdown (typically a code element)\n * @returns The extracted code content as a string with trailing newline removed\n */\nexport const extractCodeContent = (children: React.ReactNode): string => {\n  const codeElement = children as React.ReactElement;\n  return String(codeElement?.props?.children || \"\").replace(/\\n$/, \"\");\n};\n\n/**\n * Extracts the language identifier from a code block's className.\n * react-markdown uses the format \"language-xxx\" for code blocks.\n *\n * @param className - The className string from a code element\n * @returns The language identifier, or empty string if none found\n */\nexport const extractLanguage = (className: string): string => {\n  const match = /language-(\\w+)/.exec(className);\n  return match ? match[1] : \"\";\n};\n"
  },
  {
    "path": "web/src/components/MemoDetailSidebar/MemoDetailSidebar.tsx",
    "content": "import { create } from \"@bufbuild/protobuf\";\nimport { timestampDate } from \"@bufbuild/protobuf/wkt\";\nimport { isEqual } from \"lodash-es\";\nimport { CheckCircleIcon, Code2Icon, HashIcon, LinkIcon, Share2Icon } from \"lucide-react\";\nimport { useState } from \"react\";\nimport MemoSharePanel from \"@/components/MemoSharePanel\";\nimport { Button } from \"@/components/ui/button\";\nimport useCurrentUser from \"@/hooks/useCurrentUser\";\nimport { cn } from \"@/lib/utils\";\nimport { Memo, Memo_PropertySchema, MemoRelation_Type } from \"@/types/proto/api/v1/memo_service_pb\";\nimport { useTranslate } from \"@/utils/i18n\";\nimport { isSuperUser } from \"@/utils/user\";\nimport MemoRelationForceGraph from \"../MemoRelationForceGraph\";\n\ninterface Props {\n  memo: Memo;\n  className?: string;\n  parentPage?: string;\n}\n\nconst SectionLabel = ({ children }: { children: React.ReactNode }) => (\n  <p className=\"text-xs font-medium text-muted-foreground/50 uppercase tracking-wider\">{children}</p>\n);\n\nconst MemoDetailSidebar = ({ memo, className, parentPage }: Props) => {\n  const t = useTranslate();\n  const currentUser = useCurrentUser();\n  const [sharePanelOpen, setSharePanelOpen] = useState(false);\n  const property = create(Memo_PropertySchema, memo.property || {});\n  const hasSpecialProperty = property.hasLink || property.hasTaskList || property.hasCode;\n  const hasReferenceRelations = memo.relations.some((r) => r.type === MemoRelation_Type.REFERENCE);\n  const canManageShares = !memo.parent && (memo.creator === currentUser?.name || isSuperUser(currentUser));\n\n  return (\n    <aside className={cn(\"relative w-full h-auto max-h-screen overflow-auto flex flex-col gap-5\", className)}>\n      {canManageShares && (\n        <div className=\"w-full space-y-2\">\n          <SectionLabel>{t(\"memo.share.section-label\")}</SectionLabel>\n          <Button variant=\"outline\" className=\"w-full justify-start gap-2\" onClick={() => setSharePanelOpen(true)}>\n            <Share2Icon className=\"w-4 h-4\" />\n            {t(\"memo.share.open-panel\")}\n          </Button>\n        </div>\n      )}\n\n      {hasReferenceRelations && (\n        <div className=\"w-full space-y-2\">\n          <div className=\"flex items-center gap-1.5\">\n            <SectionLabel>{t(\"common.relations\")}</SectionLabel>\n            <span className=\"text-xs text-muted-foreground/30\">(Beta)</span>\n          </div>\n          <div className=\"relative w-full h-36 border border-border rounded-lg bg-muted overflow-hidden\">\n            <MemoRelationForceGraph className=\"w-full h-full\" memo={memo} parentPage={parentPage} />\n          </div>\n        </div>\n      )}\n\n      <div className=\"w-full space-y-1\">\n        <SectionLabel>{t(\"common.created-at\")}</SectionLabel>\n        <p className=\"text-sm text-foreground/70\">{memo.createTime ? timestampDate(memo.createTime).toLocaleString() : \"—\"}</p>\n      </div>\n\n      {!isEqual(memo.createTime, memo.updateTime) && (\n        <div className=\"w-full space-y-1\">\n          <SectionLabel>{t(\"common.last-updated-at\")}</SectionLabel>\n          <p className=\"text-sm text-foreground/70\">{memo.updateTime ? timestampDate(memo.updateTime).toLocaleString() : \"—\"}</p>\n        </div>\n      )}\n\n      {hasSpecialProperty && (\n        <div className=\"w-full space-y-2\">\n          <SectionLabel>{t(\"common.properties\")}</SectionLabel>\n          <div className=\"flex flex-wrap gap-1.5\">\n            {property.hasLink && (\n              <span className=\"inline-flex items-center gap-1.5 px-2 py-1 rounded-md border border-border/60 bg-muted/60 text-xs text-muted-foreground\">\n                <LinkIcon className=\"w-3.5 h-3.5\" />\n                {t(\"memo.links\")}\n              </span>\n            )}\n            {property.hasTaskList && (\n              <span className=\"inline-flex items-center gap-1.5 px-2 py-1 rounded-md border border-border/60 bg-muted/60 text-xs text-muted-foreground\">\n                <CheckCircleIcon className=\"w-3.5 h-3.5\" />\n                {t(\"memo.to-do\")}\n              </span>\n            )}\n            {property.hasCode && (\n              <span className=\"inline-flex items-center gap-1.5 px-2 py-1 rounded-md border border-border/60 bg-muted/60 text-xs text-muted-foreground\">\n                <Code2Icon className=\"w-3.5 h-3.5\" />\n                {t(\"memo.code\")}\n              </span>\n            )}\n          </div>\n        </div>\n      )}\n\n      {memo.tags.length > 0 && (\n        <div className=\"w-full space-y-2\">\n          <div className=\"flex items-center gap-1.5\">\n            <SectionLabel>{t(\"common.tags\")}</SectionLabel>\n            <span className=\"text-xs text-muted-foreground/30\">({memo.tags.length})</span>\n          </div>\n          <div className=\"flex flex-wrap gap-1.5\">\n            {memo.tags.map((tag) => (\n              <span\n                key={tag}\n                className=\"inline-flex items-center gap-1 px-1 rounded-md border border-border/60 bg-muted/60 text-sm text-muted-foreground hover:bg-muted hover:text-foreground/80 transition-colors cursor-pointer\"\n              >\n                <HashIcon className=\"w-3 h-3 opacity-50\" />\n                {tag}\n              </span>\n            ))}\n          </div>\n        </div>\n      )}\n\n      {sharePanelOpen && <MemoSharePanel memoName={memo.name} open={sharePanelOpen} onClose={() => setSharePanelOpen(false)} />}\n    </aside>\n  );\n};\n\nexport default MemoDetailSidebar;\n"
  },
  {
    "path": "web/src/components/MemoDetailSidebar/MemoDetailSidebarDrawer.tsx",
    "content": "import { GanttChartIcon } from \"lucide-react\";\nimport { useEffect, useState } from \"react\";\nimport { useLocation } from \"react-router-dom\";\nimport { Button } from \"@/components/ui/button\";\nimport { Sheet, SheetContent, SheetTrigger } from \"@/components/ui/sheet\";\nimport { Memo } from \"@/types/proto/api/v1/memo_service_pb\";\nimport MemoDetailSidebar from \"./MemoDetailSidebar\";\n\ninterface Props {\n  memo: Memo;\n  parentPage?: string;\n}\n\nconst MemoDetailSidebarDrawer = ({ memo, parentPage }: Props) => {\n  const location = useLocation();\n  const [open, setOpen] = useState(false);\n\n  useEffect(() => {\n    setOpen(false);\n  }, [location.pathname]);\n\n  return (\n    <Sheet open={open} onOpenChange={setOpen}>\n      <SheetTrigger asChild>\n        <Button variant=\"ghost\" size=\"sm\" className=\"px-2\">\n          <GanttChartIcon className=\"w-5 h-auto text-muted-foreground\" />\n        </Button>\n      </SheetTrigger>\n      <SheetContent side=\"right\" className=\"w-full sm:w-80 px-4 bg-background\">\n        <MemoDetailSidebar className=\"py-4\" memo={memo} parentPage={parentPage} />\n      </SheetContent>\n    </Sheet>\n  );\n};\n\nexport default MemoDetailSidebarDrawer;\n"
  },
  {
    "path": "web/src/components/MemoDetailSidebar/index.ts",
    "content": "import MemoDetailSidebar from \"./MemoDetailSidebar\";\nimport MemoDetailSidebarDrawer from \"./MemoDetailSidebarDrawer\";\n\nexport { MemoDetailSidebar, MemoDetailSidebarDrawer };\n"
  },
  {
    "path": "web/src/components/MemoDisplaySettingMenu.tsx",
    "content": "import { Settings2Icon } from \"lucide-react\";\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from \"@/components/ui/select\";\nimport { useView } from \"@/contexts/ViewContext\";\nimport { cn } from \"@/lib/utils\";\nimport { useTranslate } from \"@/utils/i18n\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"./ui/popover\";\n\ninterface Props {\n  className?: string;\n}\n\nfunction MemoDisplaySettingMenu({ className }: Props) {\n  const t = useTranslate();\n  const { orderByTimeAsc, toggleSortOrder } = useView();\n  const isApplying = orderByTimeAsc !== false;\n\n  return (\n    <Popover>\n      <PopoverTrigger className={cn(className, isApplying ? \"text-primary bg-primary/10 rounded\" : \"opacity-40\")}>\n        <Settings2Icon className=\"w-4 h-auto shrink-0\" />\n      </PopoverTrigger>\n      <PopoverContent align=\"end\" alignOffset={-12} sideOffset={14}>\n        <div className=\"flex flex-col gap-2 p-1\">\n          <div className=\"w-full flex flex-row justify-between items-center\">\n            <span className=\"text-sm shrink-0 mr-3 text-foreground\">{t(\"memo.direction\")}</span>\n            <Select\n              value={orderByTimeAsc.toString()}\n              onValueChange={(value) => {\n                if ((value === \"true\") !== orderByTimeAsc) {\n                  toggleSortOrder();\n                }\n              }}\n            >\n              <SelectTrigger size=\"sm\">\n                <SelectValue />\n              </SelectTrigger>\n              <SelectContent>\n                <SelectItem value=\"false\">{t(\"memo.direction-desc\")}</SelectItem>\n                <SelectItem value=\"true\">{t(\"memo.direction-asc\")}</SelectItem>\n              </SelectContent>\n            </Select>\n          </div>\n        </div>\n      </PopoverContent>\n    </Popover>\n  );\n}\n\nexport default MemoDisplaySettingMenu;\n"
  },
  {
    "path": "web/src/components/MemoEditor/Editor/SlashCommands.tsx",
    "content": "import type { SlashCommandsProps } from \"../types\";\nimport type { EditorRefActions } from \".\";\nimport { SuggestionsPopup } from \"./SuggestionsPopup\";\nimport { useSuggestions } from \"./useSuggestions\";\n\nconst SlashCommands = ({ editorRef, editorActions, commands }: SlashCommandsProps) => {\n  const handleCommandAutocomplete = (cmd: (typeof commands)[0], word: string, index: number, actions: EditorRefActions) => {\n    // Remove trigger char + word, then insert command output\n    actions.removeText(index, word.length);\n    actions.insertText(cmd.run());\n    // Position cursor relative to insertion point, if specified\n    if (cmd.cursorOffset) {\n      actions.setCursorPosition(index + cmd.cursorOffset);\n    }\n  };\n\n  const { position, suggestions, selectedIndex, isVisible, handleItemSelect } = useSuggestions({\n    editorRef,\n    editorActions,\n    triggerChar: \"/\",\n    items: commands,\n    filterItems: (items, query) => (!query ? items : items.filter((cmd) => cmd.name.toLowerCase().startsWith(query))),\n    onAutocomplete: handleCommandAutocomplete,\n  });\n\n  if (!isVisible || !position) return null;\n\n  return (\n    <SuggestionsPopup\n      position={position}\n      suggestions={suggestions}\n      selectedIndex={selectedIndex}\n      onItemSelect={handleItemSelect}\n      getItemKey={(cmd) => cmd.name}\n      renderItem={(cmd) => (\n        <span className=\"tracking-wide\">\n          <span className=\"text-muted-foreground\">/</span>\n          {cmd.name}\n        </span>\n      )}\n    />\n  );\n};\n\nexport default SlashCommands;\n"
  },
  {
    "path": "web/src/components/MemoEditor/Editor/SuggestionsPopup.tsx",
    "content": "import { ReactNode, useEffect, useRef } from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport { Position } from \"./useSuggestions\";\n\ninterface SuggestionsPopupProps<T> {\n  position: Position;\n  suggestions: T[];\n  selectedIndex: number;\n  onItemSelect: (item: T) => void;\n  renderItem: (item: T, isSelected: boolean) => ReactNode;\n  getItemKey: (item: T, index: number) => string;\n}\n\nconst POPUP_STYLES = {\n  container:\n    \"z-20 absolute p-1 mt-1 -ml-2 max-w-48 max-h-60 rounded border bg-popover text-popover-foreground shadow-lg font-mono flex flex-col overflow-y-auto overflow-x-hidden\",\n  item: \"rounded p-1 px-2 w-full text-sm cursor-pointer transition-colors select-none hover:bg-accent hover:text-accent-foreground\",\n};\n\nexport function SuggestionsPopup<T>({\n  position,\n  suggestions,\n  selectedIndex,\n  onItemSelect,\n  renderItem,\n  getItemKey,\n}: SuggestionsPopupProps<T>) {\n  const containerRef = useRef<HTMLDivElement>(null);\n  const selectedItemRef = useRef<HTMLDivElement>(null);\n\n  useEffect(() => {\n    selectedItemRef.current?.scrollIntoView({ block: \"nearest\", behavior: \"smooth\" });\n  }, [selectedIndex]);\n\n  return (\n    <div ref={containerRef} className={POPUP_STYLES.container} style={{ left: position.left, top: position.top + position.height }}>\n      {suggestions.map((item, i) => (\n        <div\n          key={getItemKey(item, i)}\n          ref={i === selectedIndex ? selectedItemRef : null}\n          onMouseDown={() => onItemSelect(item)}\n          className={cn(POPUP_STYLES.item, i === selectedIndex && \"bg-accent text-accent-foreground\")}\n        >\n          {renderItem(item, i === selectedIndex)}\n        </div>\n      ))}\n    </div>\n  );\n}\n"
  },
  {
    "path": "web/src/components/MemoEditor/Editor/TagSuggestions.tsx",
    "content": "import { useMemo } from \"react\";\nimport { matchPath } from \"react-router-dom\";\nimport OverflowTip from \"@/components/kit/OverflowTip\";\nimport { useTagCounts } from \"@/hooks/useUserQueries\";\nimport { Routes } from \"@/router\";\nimport type { TagSuggestionsProps } from \"../types\";\nimport { SuggestionsPopup } from \"./SuggestionsPopup\";\nimport { useSuggestions } from \"./useSuggestions\";\n\nexport default function TagSuggestions({ editorRef, editorActions }: TagSuggestionsProps) {\n  // On explore page, show all users' tags; otherwise show current user's tags\n  const isExplorePage = Boolean(matchPath(Routes.EXPLORE, window.location.pathname));\n  const { data: tagCount = {} } = useTagCounts(!isExplorePage);\n\n  const sortedTags = useMemo(() => {\n    return Object.entries(tagCount)\n      .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))\n      .map(([tag]) => tag);\n  }, [tagCount]);\n\n  const { position, suggestions, selectedIndex, isVisible, handleItemSelect } = useSuggestions({\n    editorRef,\n    editorActions,\n    triggerChar: \"#\",\n    items: sortedTags,\n    filterItems: (items, query) => (!query ? items : items.filter((tag) => tag.toLowerCase().includes(query))),\n    onAutocomplete: (tag, word, index, actions) => {\n      actions.removeText(index, word.length);\n      actions.insertText(`#${tag} `);\n    },\n  });\n\n  if (!isVisible || !position) return null;\n\n  return (\n    <SuggestionsPopup\n      position={position}\n      suggestions={suggestions}\n      selectedIndex={selectedIndex}\n      onItemSelect={handleItemSelect}\n      getItemKey={(tag) => tag}\n      renderItem={(tag) => (\n        <OverflowTip>\n          <span className=\"text-muted-foreground mr-1\">#</span>\n          {tag}\n        </OverflowTip>\n      )}\n    />\n  );\n}\n"
  },
  {
    "path": "web/src/components/MemoEditor/Editor/commands.ts",
    "content": "export interface Command {\n  name: string;\n  run: () => string;\n  cursorOffset?: number;\n}\n\nexport const editorCommands: Command[] = [\n  {\n    name: \"todo\",\n    run: () => \"- [ ] \",\n    cursorOffset: 6,\n  },\n  {\n    name: \"code\",\n    run: () => \"```\\n\\n```\",\n    cursorOffset: 4,\n  },\n  {\n    name: \"link\",\n    run: () => \"[text](url)\",\n    cursorOffset: 1,\n  },\n  {\n    name: \"table\",\n    run: () => \"| Header | Header |\\n| ------ | ------ |\\n| Cell   | Cell |\",\n    cursorOffset: 1,\n  },\n];\n"
  },
  {
    "path": "web/src/components/MemoEditor/Editor/index.tsx",
    "content": "import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef } from \"react\";\nimport getCaretCoordinates from \"textarea-caret\";\nimport { cn } from \"@/lib/utils\";\nimport { EDITOR_HEIGHT } from \"../constants\";\nimport type { EditorProps } from \"../types\";\nimport { editorCommands } from \"./commands\";\nimport SlashCommands from \"./SlashCommands\";\nimport TagSuggestions from \"./TagSuggestions\";\nimport { useListCompletion } from \"./useListCompletion\";\n\nexport interface EditorRefActions {\n  getEditor: () => HTMLTextAreaElement | null;\n  focus: () => void;\n  scrollToCursor: () => void;\n  insertText: (text: string, prefix?: string, suffix?: string) => void;\n  removeText: (start: number, length: number) => void;\n  setContent: (text: string) => void;\n  getContent: () => string;\n  getSelectedContent: () => string;\n  getCursorPosition: () => number;\n  setCursorPosition: (startPos: number, endPos?: number) => void;\n  getCursorLineNumber: () => number;\n  getLine: (lineNumber: number) => string;\n  setLine: (lineNumber: number, text: string) => void;\n}\n\nconst Editor = forwardRef(function Editor(props: EditorProps, ref: React.ForwardedRef<EditorRefActions>) {\n  const {\n    className,\n    initialContent,\n    placeholder,\n    onPaste,\n    onContentChange: handleContentChangeCallback,\n    isFocusMode,\n    isInIME = false,\n    onCompositionStart,\n    onCompositionEnd,\n  } = props;\n  const editorRef = useRef<HTMLTextAreaElement>(null);\n\n  const updateEditorHeight = useCallback(() => {\n    if (editorRef.current) {\n      editorRef.current.style.height = \"auto\";\n      editorRef.current.style.height = `${editorRef.current.scrollHeight ?? 0}px`;\n    }\n  }, []);\n\n  const updateContent = useCallback(() => {\n    if (editorRef.current) {\n      handleContentChangeCallback(editorRef.current.value);\n      updateEditorHeight();\n    }\n  }, [handleContentChangeCallback, updateEditorHeight]);\n\n  const scrollToCaret = useCallback((options: { force?: boolean } = {}) => {\n    const editor = editorRef.current;\n    if (!editor) return;\n\n    const { force = false } = options;\n    const caret = getCaretCoordinates(editor, editor.selectionEnd);\n\n    if (force) {\n      editor.scrollTop = Math.max(0, caret.top - editor.clientHeight / 2);\n      return;\n    }\n\n    const lineHeight = parseFloat(getComputedStyle(editor).lineHeight) || 24;\n    const viewportBottom = editor.scrollTop + editor.clientHeight;\n    // Scroll if cursor is near or beyond bottom edge (within 2 lines)\n    if (caret.top + lineHeight * 2 > viewportBottom) {\n      editor.scrollTop = Math.max(0, caret.top - editor.clientHeight / 2);\n    }\n  }, []);\n\n  useEffect(() => {\n    if (editorRef.current && initialContent) {\n      editorRef.current.value = initialContent;\n      handleContentChangeCallback(initialContent);\n      updateEditorHeight();\n    }\n    // Only run once on mount to set initial content\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, []);\n\n  // Update editor when content is externally changed (e.g., reset after save)\n  useEffect(() => {\n    if (editorRef.current && editorRef.current.value !== initialContent) {\n      editorRef.current.value = initialContent;\n      updateEditorHeight();\n    }\n  }, [initialContent, updateEditorHeight]);\n\n  const editorActions: EditorRefActions = useMemo(\n    () => ({\n      getEditor: () => editorRef.current,\n      focus: () => editorRef.current?.focus(),\n      scrollToCursor: () => {\n        scrollToCaret({ force: true });\n      },\n      insertText: (content = \"\", prefix = \"\", suffix = \"\") => {\n        const editor = editorRef.current;\n        if (!editor) return;\n\n        const cursorPos = editor.selectionStart;\n        const endPos = editor.selectionEnd;\n        const prev = editor.value;\n        const actual = content || prev.slice(cursorPos, endPos);\n        editor.value = prev.slice(0, cursorPos) + prefix + actual + suffix + prev.slice(endPos);\n\n        editor.focus();\n        editor.setSelectionRange(cursorPos + prefix.length + actual.length, cursorPos + prefix.length + actual.length);\n        updateContent();\n      },\n      removeText: (start: number, length: number) => {\n        const editor = editorRef.current;\n        if (!editor) return;\n\n        editor.value = editor.value.slice(0, start) + editor.value.slice(start + length);\n        editor.focus();\n        editor.setSelectionRange(start, start);\n        updateContent();\n      },\n      setContent: (text: string) => {\n        const editor = editorRef.current;\n        if (editor) {\n          editor.value = text;\n          updateContent();\n        }\n      },\n      getContent: () => editorRef.current?.value ?? \"\",\n      getCursorPosition: () => editorRef.current?.selectionStart ?? 0,\n      getSelectedContent: () => {\n        const editor = editorRef.current;\n        if (!editor) return \"\";\n        return editor.value.slice(editor.selectionStart, editor.selectionEnd);\n      },\n      setCursorPosition: (startPos: number, endPos?: number) => {\n        const editor = editorRef.current;\n        if (!editor) return;\n        // setSelectionRange requires valid arguments; default to startPos if endPos is undefined\n        const endPosition = endPos !== undefined && !Number.isNaN(endPos) ? endPos : startPos;\n        editor.setSelectionRange(startPos, endPosition);\n      },\n      getCursorLineNumber: () => {\n        const editor = editorRef.current;\n        if (!editor) return 0;\n        const lines = editor.value.slice(0, editor.selectionStart).split(\"\\n\");\n        return lines.length - 1;\n      },\n      getLine: (lineNumber: number) => editorRef.current?.value.split(\"\\n\")[lineNumber] ?? \"\",\n      setLine: (lineNumber: number, text: string) => {\n        const editor = editorRef.current;\n        if (!editor) return;\n        const lines = editor.value.split(\"\\n\");\n        lines[lineNumber] = text;\n        editor.value = lines.join(\"\\n\");\n        editor.focus();\n        updateContent();\n      },\n    }),\n    [updateContent, scrollToCaret],\n  );\n\n  useImperativeHandle(ref, () => editorActions, [editorActions]);\n\n  const handleEditorInput = useCallback(() => {\n    if (editorRef.current) {\n      handleContentChangeCallback(editorRef.current.value);\n      updateEditorHeight();\n\n      // Auto-scroll to keep cursor visible when typing\n      // See: https://github.com/usememos/memos/issues/5469\n      scrollToCaret();\n    }\n  }, [handleContentChangeCallback, updateEditorHeight, scrollToCaret]);\n\n  // Auto-complete markdown lists when pressing Enter\n  useListCompletion({\n    editorRef,\n    editorActions,\n    isInIME,\n  });\n\n  // Recalculate editor height when focus mode changes\n  useEffect(() => {\n    updateEditorHeight();\n  }, [isFocusMode, updateEditorHeight]);\n\n  return (\n    <div\n      className={cn(\n        \"flex flex-col justify-start items-start relative w-full bg-inherit\",\n        // Focus mode: flex-1 to grow and fill space; Normal: h-auto with max-height\n        isFocusMode ? \"flex-1\" : `h-auto ${EDITOR_HEIGHT.normal}`,\n        className,\n      )}\n    >\n      <textarea\n        className={cn(\n          \"w-full text-base resize-none overflow-x-hidden overflow-y-auto bg-transparent outline-none placeholder:opacity-70 whitespace-pre-wrap wrap-break-word\",\n          // Focus mode: flex-1 h-0 to grow within flex container; Normal: h-full to fill wrapper\n          isFocusMode ? \"flex-1 h-0\" : \"h-full\",\n        )}\n        rows={1}\n        placeholder={placeholder}\n        ref={editorRef}\n        onPaste={onPaste}\n        onInput={handleEditorInput}\n        onCompositionStart={onCompositionStart}\n        onCompositionEnd={onCompositionEnd}\n      ></textarea>\n      <TagSuggestions editorRef={editorRef} editorActions={ref} />\n      <SlashCommands editorRef={editorRef} editorActions={ref} commands={editorCommands} />\n    </div>\n  );\n});\n\nexport default Editor;\n"
  },
  {
    "path": "web/src/components/MemoEditor/Editor/shortcuts.ts",
    "content": "import type { EditorRefActions } from \"./index\";\n\nconst SHORTCUTS = {\n  BOLD: { key: \"b\", delimiter: \"**\" },\n  ITALIC: { key: \"i\", delimiter: \"*\" },\n  LINK: { key: \"k\" },\n} as const;\n\nconst URL_PLACEHOLDER = \"url\";\nconst URL_REGEX = /^https?:\\/\\/[^\\s]+$/;\nconst LINK_OFFSET = 3; // Length of \"]()\"\n\nexport function handleMarkdownShortcuts(event: React.KeyboardEvent, editor: EditorRefActions): void {\n  const key = event.key.toLowerCase();\n  if (key === SHORTCUTS.BOLD.key) {\n    event.preventDefault();\n    toggleTextStyle(editor, SHORTCUTS.BOLD.delimiter);\n  } else if (key === SHORTCUTS.ITALIC.key) {\n    event.preventDefault();\n    toggleTextStyle(editor, SHORTCUTS.ITALIC.delimiter);\n  } else if (key === SHORTCUTS.LINK.key) {\n    event.preventDefault();\n    insertHyperlink(editor);\n  }\n}\n\nexport function insertHyperlink(editor: EditorRefActions, url?: string): void {\n  const cursorPosition = editor.getCursorPosition();\n  const selectedContent = editor.getSelectedContent();\n  const isUrlSelected = !url && URL_REGEX.test(selectedContent.trim());\n\n  if (isUrlSelected) {\n    editor.insertText(`[](${selectedContent})`);\n    editor.setCursorPosition(cursorPosition + 1, cursorPosition + 1);\n    return;\n  }\n\n  const href = url ?? URL_PLACEHOLDER;\n  editor.insertText(`[${selectedContent}](${href})`);\n\n  if (href === URL_PLACEHOLDER) {\n    const urlStart = cursorPosition + selectedContent.length + LINK_OFFSET;\n    editor.setCursorPosition(urlStart, urlStart + href.length);\n  }\n}\n\nfunction toggleTextStyle(editor: EditorRefActions, delimiter: string): void {\n  const cursorPosition = editor.getCursorPosition();\n  const selectedContent = editor.getSelectedContent();\n  const isStyled = selectedContent.startsWith(delimiter) && selectedContent.endsWith(delimiter);\n\n  if (isStyled) {\n    const unstyled = selectedContent.slice(delimiter.length, -delimiter.length);\n    editor.insertText(unstyled);\n    editor.setCursorPosition(cursorPosition, cursorPosition + unstyled.length);\n  } else {\n    editor.insertText(`${delimiter}${selectedContent}${delimiter}`);\n    editor.setCursorPosition(cursorPosition + delimiter.length, cursorPosition + delimiter.length + selectedContent.length);\n  }\n}\n\nexport function hyperlinkHighlightedText(editor: EditorRefActions, url: string): void {\n  const selectedContent = editor.getSelectedContent();\n  const cursorPosition = editor.getCursorPosition();\n\n  editor.insertText(`[${selectedContent}](${url})`);\n\n  const newPosition = cursorPosition + selectedContent.length + url.length + 4;\n  editor.setCursorPosition(newPosition, newPosition);\n}\n"
  },
  {
    "path": "web/src/components/MemoEditor/Editor/useListCompletion.ts",
    "content": "import { useEffect, useRef } from \"react\";\nimport { detectLastListItem, generateListContinuation } from \"@/utils/markdown-list-detection\";\nimport { EditorRefActions } from \".\";\n\ninterface UseListCompletionOptions {\n  editorRef: React.RefObject<HTMLTextAreaElement>;\n  editorActions: EditorRefActions;\n  isInIME: boolean;\n}\n\n// Patterns to detect empty list items\nconst EMPTY_LIST_PATTERNS = [\n  /^(\\s*)([-*+])\\s*$/, // Empty unordered list\n  /^(\\s*)([-*+])\\s+\\[([ xX])\\]\\s*$/, // Empty task list\n  /^(\\s*)(\\d+)[.)]\\s*$/, // Empty ordered list\n];\n\nconst isEmptyListItem = (line: string) => EMPTY_LIST_PATTERNS.some((pattern) => pattern.test(line));\n\nexport function useListCompletion({ editorRef, editorActions, isInIME }: UseListCompletionOptions) {\n  const isInIMERef = useRef(isInIME);\n  isInIMERef.current = isInIME;\n\n  const editorActionsRef = useRef(editorActions);\n  editorActionsRef.current = editorActions;\n\n  // Track when composition ends to handle Safari race condition\n  // Safari fires keydown(Enter) immediately after compositionend, while Chrome doesn't\n  // See: https://github.com/usememos/memos/issues/5469\n  const lastCompositionEndRef = useRef(0);\n\n  useEffect(() => {\n    const editor = editorRef.current;\n    if (!editor) return;\n\n    const handleCompositionEnd = () => {\n      lastCompositionEndRef.current = Date.now();\n    };\n\n    const handleKeyDown = (event: KeyboardEvent) => {\n      if (event.key !== \"Enter\" || isInIMERef.current || event.shiftKey || event.ctrlKey || event.metaKey || event.altKey) {\n        return;\n      }\n\n      // Safari fix: Ignore Enter key within 100ms of composition end\n      // This prevents double-enter behavior when confirming IME input in lists\n      if (Date.now() - lastCompositionEndRef.current < 100) {\n        return;\n      }\n\n      const actions = editorActionsRef.current;\n      const cursorPosition = actions.getCursorPosition();\n      const contentBeforeCursor = actions.getContent().substring(0, cursorPosition);\n      const listInfo = detectLastListItem(contentBeforeCursor);\n\n      if (!listInfo.type) return;\n\n      event.preventDefault();\n\n      const lines = contentBeforeCursor.split(\"\\n\");\n      const currentLine = lines[lines.length - 1];\n\n      if (isEmptyListItem(currentLine)) {\n        const lineStartPos = cursorPosition - currentLine.length;\n        actions.removeText(lineStartPos, currentLine.length);\n      } else {\n        const continuation = generateListContinuation(listInfo);\n        actions.insertText(\"\\n\" + continuation);\n\n        // Auto-scroll to keep cursor visible after inserting list item\n        setTimeout(() => actions.scrollToCursor(), 0);\n      }\n    };\n\n    editor.addEventListener(\"compositionend\", handleCompositionEnd);\n    editor.addEventListener(\"keydown\", handleKeyDown);\n    return () => {\n      editor.removeEventListener(\"compositionend\", handleCompositionEnd);\n      editor.removeEventListener(\"keydown\", handleKeyDown);\n    };\n  }, []);\n}\n"
  },
  {
    "path": "web/src/components/MemoEditor/Editor/useSuggestions.ts",
    "content": "import { useEffect, useRef, useState } from \"react\";\nimport getCaretCoordinates from \"textarea-caret\";\nimport { EditorRefActions } from \".\";\n\nexport interface Position {\n  left: number;\n  top: number;\n  height: number;\n}\n\nexport interface UseSuggestionsOptions<T> {\n  editorRef: React.RefObject<HTMLTextAreaElement>;\n  editorActions: React.ForwardedRef<EditorRefActions>;\n  triggerChar: string;\n  items: T[];\n  filterItems: (items: T[], searchQuery: string) => T[];\n  onAutocomplete: (item: T, word: string, startIndex: number, actions: EditorRefActions) => void;\n}\n\nexport interface UseSuggestionsReturn<T> {\n  position: Position | null;\n  suggestions: T[];\n  selectedIndex: number;\n  isVisible: boolean;\n  handleItemSelect: (item: T) => void;\n}\n\nexport function useSuggestions<T>({\n  editorRef,\n  editorActions,\n  triggerChar,\n  items,\n  filterItems,\n  onAutocomplete,\n}: UseSuggestionsOptions<T>): UseSuggestionsReturn<T> {\n  const [position, setPosition] = useState<Position | null>(null);\n  const [selectedIndex, setSelectedIndex] = useState(0);\n  const isProcessingRef = useRef(false);\n\n  const selectedRef = useRef(selectedIndex);\n  selectedRef.current = selectedIndex;\n\n  const getCurrentWord = (): [word: string, startIndex: number] => {\n    const editor = editorRef.current;\n    if (!editor) return [\"\", 0];\n    const cursorPos = editor.selectionEnd;\n    const before = editor.value.slice(0, cursorPos).match(/\\S*$/) || { 0: \"\", index: cursorPos };\n    const after = editor.value.slice(cursorPos).match(/^\\S*/) || { 0: \"\" };\n    return [before[0] + after[0], before.index ?? cursorPos];\n  };\n\n  const hide = () => setPosition(null);\n\n  const suggestionsRef = useRef<T[]>([]);\n  suggestionsRef.current = (() => {\n    const [word] = getCurrentWord();\n    if (!word.startsWith(triggerChar)) return [];\n    const searchQuery = word.slice(triggerChar.length).toLowerCase();\n    return filterItems(items, searchQuery);\n  })();\n\n  const isVisibleRef = useRef(false);\n  isVisibleRef.current = !!(position && suggestionsRef.current.length > 0);\n\n  const handleAutocomplete = (item: T) => {\n    if (!editorActions || !(\"current\" in editorActions) || !editorActions.current) {\n      console.warn(\"useSuggestions: editorActions not available\");\n      return;\n    }\n    isProcessingRef.current = true;\n    const [word, index] = getCurrentWord();\n    onAutocomplete(item, word, index, editorActions.current);\n    hide();\n    // Re-enable input handling after all DOM operations complete\n    queueMicrotask(() => {\n      isProcessingRef.current = false;\n    });\n  };\n\n  const handleNavigation = (e: KeyboardEvent, selected: number, suggestionsCount: number) => {\n    if (e.code === \"ArrowDown\") {\n      setSelectedIndex((selected + 1) % suggestionsCount);\n      e.preventDefault();\n      e.stopPropagation();\n    } else if (e.code === \"ArrowUp\") {\n      setSelectedIndex((selected - 1 + suggestionsCount) % suggestionsCount);\n      e.preventDefault();\n      e.stopPropagation();\n    }\n  };\n\n  const handleKeyDown = (e: KeyboardEvent) => {\n    if (!isVisibleRef.current) return;\n\n    const suggestions = suggestionsRef.current;\n    const selected = selectedRef.current;\n\n    if ([\"Escape\", \"ArrowLeft\", \"ArrowRight\"].includes(e.code)) {\n      hide();\n      return;\n    }\n\n    if ([\"ArrowDown\", \"ArrowUp\"].includes(e.code)) {\n      handleNavigation(e, selected, suggestions.length);\n      return;\n    }\n\n    if ([\"Enter\", \"Tab\"].includes(e.code)) {\n      handleAutocomplete(suggestions[selected]);\n      e.preventDefault();\n      e.stopImmediatePropagation();\n    }\n  };\n\n  const handleInput = () => {\n    if (isProcessingRef.current) return;\n\n    const editor = editorRef.current;\n    if (!editor) return;\n\n    setSelectedIndex(0);\n    const [word, index] = getCurrentWord();\n    const currentChar = editor.value[editor.selectionEnd];\n    const isActive = word.startsWith(triggerChar) && currentChar !== triggerChar;\n\n    if (isActive) {\n      const coords = getCaretCoordinates(editor, index);\n      coords.top -= editor.scrollTop;\n      setPosition(coords);\n    } else {\n      hide();\n    }\n  };\n\n  useEffect(() => {\n    const editor = editorRef.current;\n    if (!editor) return;\n\n    const handlers = { click: hide, blur: hide, keydown: handleKeyDown, input: handleInput };\n    Object.entries(handlers).forEach(([event, handler]) => {\n      editor.addEventListener(event, handler as EventListener);\n    });\n\n    return () => {\n      Object.entries(handlers).forEach(([event, handler]) => {\n        editor.removeEventListener(event, handler as EventListener);\n      });\n    };\n  }, []);\n\n  return {\n    position,\n    suggestions: suggestionsRef.current,\n    selectedIndex,\n    isVisible: isVisibleRef.current,\n    handleItemSelect: handleAutocomplete,\n  };\n}\n"
  },
  {
    "path": "web/src/components/MemoEditor/README.md",
    "content": "# MemoEditor Architecture\n\n## Overview\n\nMemoEditor uses a three-layer architecture for better separation of concerns and testability.\n\n## Architecture\n\n```\n┌─────────────────────────────────────────┐\n│   Presentation Layer (Components)       │\n│   - EditorToolbar, EditorContent, etc.  │\n└─────────────────┬───────────────────────┘\n                  │\n┌─────────────────▼───────────────────────┐\n│   State Layer (Reducer + Context)       │\n│   - state/, useEditorContext()          │\n└─────────────────┬───────────────────────┘\n                  │\n┌─────────────────▼───────────────────────┐\n│   Service Layer (Business Logic)        │\n│   - services/ (pure functions)          │\n└─────────────────────────────────────────┘\n```\n\n## Directory Structure\n\n```\nMemoEditor/\n├── state/                  # State management (reducer, actions, context)\n├── services/              # Business logic (pure functions)\n├── components/            # UI components\n├── hooks/                 # React hooks (utilities)\n├── Editor/               # Core editor component\n├── Toolbar/              # Toolbar components\n├── constants.ts\n└── types/\n```\n\n## Key Concepts\n\n### State Management\n\nUses `useReducer` + Context for predictable state transitions. All state changes go through action creators.\n\n### Services\n\nPure TypeScript functions containing business logic. No React hooks, easy to test.\n\n### Components\n\nThin presentation components that dispatch actions and render UI.\n\n## Usage\n\n```typescript\nimport MemoEditor from \"@/components/MemoEditor\";\n\n<MemoEditor\n  memoName=\"memos/123\"\n  onConfirm={(name) => console.log('Saved:', name)}\n  onCancel={() => console.log('Cancelled')}\n/>\n```\n\n## Testing\n\nServices are pure functions - easy to unit test without React.\n\n```typescript\nconst state = mockEditorState();\nconst result = await memoService.save(state, { memoName: 'memos/123' });\n```\n"
  },
  {
    "path": "web/src/components/MemoEditor/Toolbar/InsertMenu.tsx",
    "content": "import { LatLng } from \"leaflet\";\nimport { uniqBy } from \"lodash-es\";\nimport { FileIcon, LinkIcon, LoaderIcon, type LucideIcon, MapPinIcon, Maximize2Icon, MoreHorizontalIcon, PlusIcon } from \"lucide-react\";\nimport { useCallback, useEffect, useMemo, useState } from \"react\";\nimport { useDebounce } from \"react-use\";\nimport { useReverseGeocoding } from \"@/components/map\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuSub,\n  DropdownMenuSubContent,\n  DropdownMenuSubTrigger,\n  DropdownMenuTrigger,\n  useDropdownMenuSubHoverDelay,\n} from \"@/components/ui/dropdown-menu\";\nimport type { MemoRelation } from \"@/types/proto/api/v1/memo_service_pb\";\nimport { useTranslate } from \"@/utils/i18n\";\nimport { LinkMemoDialog, LocationDialog } from \"../components\";\nimport { useFileUpload, useLinkMemo, useLocation } from \"../hooks\";\nimport { useEditorContext } from \"../state\";\nimport type { InsertMenuProps } from \"../types\";\nimport type { LocalFile } from \"../types/attachment\";\n\nconst InsertMenu = (props: InsertMenuProps) => {\n  const t = useTranslate();\n  const { state, actions, dispatch } = useEditorContext();\n  const { location: initialLocation, onLocationChange, onToggleFocusMode, isUploading: isUploadingProp } = props;\n\n  const [linkDialogOpen, setLinkDialogOpen] = useState(false);\n  const [locationDialogOpen, setLocationDialogOpen] = useState(false);\n  const [moreSubmenuOpen, setMoreSubmenuOpen] = useState(false);\n\n  const { handleTriggerEnter, handleTriggerLeave, handleContentEnter, handleContentLeave } = useDropdownMenuSubHoverDelay(\n    150,\n    setMoreSubmenuOpen,\n  );\n\n  const { fileInputRef, selectingFlag, handleFileInputChange, handleUploadClick } = useFileUpload((newFiles: LocalFile[]) => {\n    newFiles.forEach((file) => dispatch(actions.addLocalFile(file)));\n  });\n\n  const linkMemo = useLinkMemo({\n    isOpen: linkDialogOpen,\n    currentMemoName: props.memoName,\n    existingRelations: state.metadata.relations,\n    onAddRelation: (relation: MemoRelation) => {\n      dispatch(actions.setMetadata({ relations: uniqBy([...state.metadata.relations, relation], (r) => r.relatedMemo?.name) }));\n      setLinkDialogOpen(false);\n    },\n  });\n\n  const location = useLocation(props.location);\n\n  const [debouncedPosition, setDebouncedPosition] = useState<LatLng | undefined>(undefined);\n\n  useDebounce(\n    () => {\n      setDebouncedPosition(location.state.position);\n    },\n    1000,\n    [location.state.position],\n  );\n\n  const { data: displayName } = useReverseGeocoding(debouncedPosition?.lat, debouncedPosition?.lng);\n\n  useEffect(() => {\n    if (displayName) {\n      location.setPlaceholder(displayName);\n    }\n  }, [displayName]);\n\n  const isUploading = selectingFlag || isUploadingProp;\n\n  const handleOpenLinkDialog = useCallback(() => {\n    setLinkDialogOpen(true);\n  }, []);\n\n  const handleLocationClick = useCallback(() => {\n    setLocationDialogOpen(true);\n    if (!initialLocation && !location.locationInitialized) {\n      if (navigator.geolocation) {\n        navigator.geolocation.getCurrentPosition(\n          (position) => {\n            location.handlePositionChange(new LatLng(position.coords.latitude, position.coords.longitude));\n          },\n          (error) => {\n            console.error(\"Geolocation error:\", error);\n          },\n        );\n      }\n    }\n  }, [initialLocation, location]);\n\n  const handleLocationConfirm = useCallback(() => {\n    const newLocation = location.getLocation();\n    if (newLocation) {\n      onLocationChange(newLocation);\n      setLocationDialogOpen(false);\n    }\n  }, [location, onLocationChange]);\n\n  const handleLocationCancel = useCallback(() => {\n    location.reset();\n    setLocationDialogOpen(false);\n  }, [location]);\n\n  const handlePositionChange = useCallback(\n    (position: LatLng) => {\n      location.handlePositionChange(position);\n    },\n    [location],\n  );\n\n  const handleToggleFocusMode = useCallback(() => {\n    onToggleFocusMode?.();\n    setMoreSubmenuOpen(false);\n  }, [onToggleFocusMode]);\n\n  const menuItems = useMemo(\n    () =>\n      [\n        {\n          key: \"upload\",\n          label: t(\"common.upload\"),\n          icon: FileIcon,\n          onClick: handleUploadClick,\n        },\n        {\n          key: \"link\",\n          label: t(\"tooltip.link-memo\"),\n          icon: LinkIcon,\n          onClick: handleOpenLinkDialog,\n        },\n        {\n          key: \"location\",\n          label: t(\"tooltip.select-location\"),\n          icon: MapPinIcon,\n          onClick: handleLocationClick,\n        },\n      ] satisfies Array<{ key: string; label: string; icon: LucideIcon; onClick: () => void }>,\n    [handleLocationClick, handleOpenLinkDialog, handleUploadClick, t],\n  );\n\n  return (\n    <>\n      <DropdownMenu modal={false}>\n        <DropdownMenuTrigger asChild>\n          <Button variant=\"outline\" size=\"icon\" className=\"shadow-none\" disabled={isUploading}>\n            {isUploading ? <LoaderIcon className=\"size-4 animate-spin\" /> : <PlusIcon className=\"size-4\" />}\n          </Button>\n        </DropdownMenuTrigger>\n        <DropdownMenuContent align=\"start\">\n          {menuItems.map((item) => (\n            <DropdownMenuItem key={item.key} onClick={item.onClick}>\n              <item.icon className=\"w-4 h-4\" />\n              {item.label}\n            </DropdownMenuItem>\n          ))}\n          {/* View submenu with Focus Mode */}\n          <DropdownMenuSub open={moreSubmenuOpen} onOpenChange={setMoreSubmenuOpen}>\n            <DropdownMenuSubTrigger onPointerEnter={handleTriggerEnter} onPointerLeave={handleTriggerLeave}>\n              <MoreHorizontalIcon className=\"w-4 h-4\" />\n              {t(\"common.more\")}\n            </DropdownMenuSubTrigger>\n            <DropdownMenuSubContent onPointerEnter={handleContentEnter} onPointerLeave={handleContentLeave}>\n              <DropdownMenuItem onClick={handleToggleFocusMode}>\n                <Maximize2Icon className=\"w-4 h-4\" />\n                {t(\"editor.focus-mode\")}\n              </DropdownMenuItem>\n            </DropdownMenuSubContent>\n          </DropdownMenuSub>\n          <div className=\"px-2 py-1 text-xs text-muted-foreground opacity-80\">{t(\"editor.slash-commands\")}</div>\n        </DropdownMenuContent>\n      </DropdownMenu>\n\n      {/* Hidden file input */}\n      <input\n        className=\"hidden\"\n        ref={fileInputRef}\n        disabled={isUploading}\n        onChange={handleFileInputChange}\n        type=\"file\"\n        multiple={true}\n        accept=\"*\"\n      />\n\n      <LinkMemoDialog\n        open={linkDialogOpen}\n        onOpenChange={setLinkDialogOpen}\n        searchText={linkMemo.searchText}\n        onSearchChange={linkMemo.setSearchText}\n        filteredMemos={linkMemo.filteredMemos}\n        isFetching={linkMemo.isFetching}\n        onSelectMemo={linkMemo.addMemoRelation}\n        isAlreadyLinked={linkMemo.isAlreadyLinked}\n      />\n\n      <LocationDialog\n        open={locationDialogOpen}\n        onOpenChange={setLocationDialogOpen}\n        state={location.state}\n        locationInitialized={location.locationInitialized}\n        onPositionChange={handlePositionChange}\n        onUpdateCoordinate={location.updateCoordinate}\n        onPlaceholderChange={location.setPlaceholder}\n        onCancel={handleLocationCancel}\n        onConfirm={handleLocationConfirm}\n      />\n    </>\n  );\n};\n\nexport default InsertMenu;\n"
  },
  {
    "path": "web/src/components/MemoEditor/Toolbar/VisibilitySelector.tsx",
    "content": "import { CheckIcon, ChevronDownIcon } from \"lucide-react\";\nimport { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from \"@/components/ui/dropdown-menu\";\nimport VisibilityIcon from \"@/components/VisibilityIcon\";\nimport { Visibility } from \"@/types/proto/api/v1/memo_service_pb\";\nimport { useTranslate } from \"@/utils/i18n\";\nimport type { VisibilitySelectorProps } from \"../types\";\n\nconst VisibilitySelector = (props: VisibilitySelectorProps) => {\n  const { value, onChange } = props;\n  const t = useTranslate();\n\n  const visibilityOptions = [\n    { value: Visibility.PRIVATE, label: t(\"memo.visibility.private\") },\n    { value: Visibility.PROTECTED, label: t(\"memo.visibility.protected\") },\n    { value: Visibility.PUBLIC, label: t(\"memo.visibility.public\") },\n  ] as const;\n\n  const currentLabel = visibilityOptions.find((option) => option.value === value)?.label || \"\";\n\n  return (\n    <DropdownMenu onOpenChange={props.onOpenChange}>\n      <DropdownMenuTrigger asChild>\n        <button className=\"inline-flex items-center px-2 text-sm text-muted-foreground opacity-80 hover:opacity-100 transition-colors\">\n          <VisibilityIcon visibility={value} className=\"opacity-60 mr-1.5\" />\n          <span>{currentLabel}</span>\n          <ChevronDownIcon className=\"ml-0.5 w-4 h-4 opacity-60\" />\n        </button>\n      </DropdownMenuTrigger>\n      <DropdownMenuContent align=\"end\">\n        {visibilityOptions.map((option) => (\n          <DropdownMenuItem key={option.value} className=\"cursor-pointer gap-2\" onClick={() => onChange(option.value)}>\n            <VisibilityIcon visibility={option.value} />\n            <span className=\"flex-1\">{option.label}</span>\n            {value === option.value && <CheckIcon className=\"w-4 h-4 text-primary\" />}\n          </DropdownMenuItem>\n        ))}\n      </DropdownMenuContent>\n    </DropdownMenu>\n  );\n};\n\nexport default VisibilitySelector;\n"
  },
  {
    "path": "web/src/components/MemoEditor/Toolbar/index.ts",
    "content": "// Toolbar components for MemoEditor\nexport { default as InsertMenu } from \"./InsertMenu\";\nexport { default as VisibilitySelector } from \"./VisibilitySelector\";\n"
  },
  {
    "path": "web/src/components/MemoEditor/components/AttachmentList.tsx",
    "content": "import { ChevronDownIcon, ChevronUpIcon, FileIcon, PaperclipIcon, XIcon } from \"lucide-react\";\nimport type { FC } from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport type { Attachment } from \"@/types/proto/api/v1/attachment_service_pb\";\nimport { formatFileSize, getFileTypeLabel } from \"@/utils/format\";\nimport type { LocalFile } from \"../types/attachment\";\nimport { toAttachmentItems } from \"../types/attachment\";\n\ninterface AttachmentListProps {\n  attachments: Attachment[];\n  localFiles?: LocalFile[];\n  onAttachmentsChange?: (attachments: Attachment[]) => void;\n  onRemoveLocalFile?: (previewUrl: string) => void;\n}\n\nconst AttachmentItemCard: FC<{\n  item: ReturnType<typeof toAttachmentItems>[0];\n  onRemove?: () => void;\n  onMoveUp?: () => void;\n  onMoveDown?: () => void;\n  canMoveUp?: boolean;\n  canMoveDown?: boolean;\n}> = ({ item, onRemove, onMoveUp, onMoveDown, canMoveUp = true, canMoveDown = true }) => {\n  const { category, filename, thumbnailUrl, mimeType, size } = item;\n  const fileTypeLabel = getFileTypeLabel(mimeType);\n  const fileSizeLabel = size ? formatFileSize(size) : undefined;\n\n  return (\n    <div className=\"relative flex items-center gap-1.5 px-1.5 py-1 rounded border border-transparent hover:border-border hover:bg-accent/20 transition-all\">\n      <div className=\"shrink-0 w-6 h-6 rounded overflow-hidden bg-muted/40 flex items-center justify-center\">\n        {category === \"image\" && thumbnailUrl ? (\n          <img src={thumbnailUrl} alt=\"\" className=\"w-full h-full object-cover\" />\n        ) : (\n          <FileIcon className=\"w-3.5 h-3.5 text-muted-foreground\" />\n        )}\n      </div>\n\n      <div className=\"flex-1 min-w-0 flex flex-col sm:flex-row sm:items-baseline gap-0.5 sm:gap-1.5\">\n        <span className=\"text-xs truncate\" title={filename}>\n          {filename}\n        </span>\n\n        <div className=\"flex items-center gap-1 text-[11px] text-muted-foreground shrink-0\">\n          <span>{fileTypeLabel}</span>\n          {fileSizeLabel && (\n            <>\n              <span className=\"text-muted-foreground/50 hidden sm:inline\">•</span>\n              <span className=\"hidden sm:inline\">{fileSizeLabel}</span>\n            </>\n          )}\n        </div>\n      </div>\n\n      <div className=\"shrink-0 flex items-center gap-0.5\">\n        {onMoveUp && (\n          <button\n            type=\"button\"\n            onClick={onMoveUp}\n            disabled={!canMoveUp}\n            className={cn(\n              \"p-0.5 rounded hover:bg-accent active:bg-accent transition-colors touch-manipulation\",\n              !canMoveUp && \"opacity-20 cursor-not-allowed hover:bg-transparent\",\n            )}\n            title=\"Move up\"\n            aria-label=\"Move attachment up\"\n          >\n            <ChevronUpIcon className=\"w-3 h-3 text-muted-foreground\" />\n          </button>\n        )}\n\n        {onMoveDown && (\n          <button\n            type=\"button\"\n            onClick={onMoveDown}\n            disabled={!canMoveDown}\n            className={cn(\n              \"p-0.5 rounded hover:bg-accent active:bg-accent transition-colors touch-manipulation\",\n              !canMoveDown && \"opacity-20 cursor-not-allowed hover:bg-transparent\",\n            )}\n            title=\"Move down\"\n            aria-label=\"Move attachment down\"\n          >\n            <ChevronDownIcon className=\"w-3 h-3 text-muted-foreground\" />\n          </button>\n        )}\n\n        {onRemove && (\n          <button\n            type=\"button\"\n            onClick={onRemove}\n            className=\"p-0.5 rounded hover:bg-destructive/10 active:bg-destructive/10 transition-colors ml-0.5 touch-manipulation\"\n            title=\"Remove\"\n            aria-label=\"Remove attachment\"\n          >\n            <XIcon className=\"w-3 h-3 text-muted-foreground hover:text-destructive\" />\n          </button>\n        )}\n      </div>\n    </div>\n  );\n};\n\nconst AttachmentList: FC<AttachmentListProps> = ({ attachments, localFiles = [], onAttachmentsChange, onRemoveLocalFile }) => {\n  if (attachments.length === 0 && localFiles.length === 0) {\n    return null;\n  }\n\n  const items = toAttachmentItems(attachments, localFiles);\n\n  const handleMoveUp = (index: number) => {\n    if (index === 0 || !onAttachmentsChange) return;\n\n    const newAttachments = [...attachments];\n    [newAttachments[index - 1], newAttachments[index]] = [newAttachments[index], newAttachments[index - 1]];\n    onAttachmentsChange(newAttachments);\n  };\n\n  const handleMoveDown = (index: number) => {\n    if (index === attachments.length - 1 || !onAttachmentsChange) return;\n\n    const newAttachments = [...attachments];\n    [newAttachments[index], newAttachments[index + 1]] = [newAttachments[index + 1], newAttachments[index]];\n    onAttachmentsChange(newAttachments);\n  };\n\n  const handleRemoveAttachment = (name: string) => {\n    if (onAttachmentsChange) {\n      onAttachmentsChange(attachments.filter((attachment) => attachment.name !== name));\n    }\n  };\n\n  const handleRemoveItem = (item: (typeof items)[0]) => {\n    if (item.isLocal) {\n      onRemoveLocalFile?.(item.id);\n    } else {\n      handleRemoveAttachment(item.id);\n    }\n  };\n\n  return (\n    <div className=\"w-full rounded-lg border border-border bg-muted/20 overflow-hidden\">\n      <div className=\"flex items-center gap-1.5 px-2 py-1 border-b border-border bg-muted/30\">\n        <PaperclipIcon className=\"w-3.5 h-3.5 text-muted-foreground\" />\n        <span className=\"text-xs text-muted-foreground\">Attachments ({items.length})</span>\n      </div>\n\n      <div className=\"p-1 sm:p-1.5 flex flex-col gap-0.5\">\n        {items.map((item) => {\n          const isLocalFile = item.isLocal;\n          const attachmentIndex = isLocalFile ? -1 : attachments.findIndex((a) => a.name === item.id);\n\n          return (\n            <AttachmentItemCard\n              key={item.id}\n              item={item}\n              onRemove={() => handleRemoveItem(item)}\n              onMoveUp={!isLocalFile ? () => handleMoveUp(attachmentIndex) : undefined}\n              onMoveDown={!isLocalFile ? () => handleMoveDown(attachmentIndex) : undefined}\n              canMoveUp={!isLocalFile && attachmentIndex > 0}\n              canMoveDown={!isLocalFile && attachmentIndex < attachments.length - 1}\n            />\n          );\n        })}\n      </div>\n    </div>\n  );\n};\n\nexport default AttachmentList;\n"
  },
  {
    "path": "web/src/components/MemoEditor/components/EditorContent.tsx",
    "content": "import { forwardRef } from \"react\";\nimport Editor, { type EditorRefActions } from \"../Editor\";\nimport { useBlobUrls, useDragAndDrop } from \"../hooks\";\nimport { useEditorContext } from \"../state\";\nimport type { EditorContentProps } from \"../types\";\nimport type { LocalFile } from \"../types/attachment\";\n\nexport const EditorContent = forwardRef<EditorRefActions, EditorContentProps>(({ placeholder }, ref) => {\n  const { state, actions, dispatch } = useEditorContext();\n  const { createBlobUrl } = useBlobUrls();\n\n  const { dragHandlers } = useDragAndDrop((files: FileList) => {\n    const localFiles: LocalFile[] = Array.from(files).map((file) => ({\n      file,\n      previewUrl: createBlobUrl(file),\n    }));\n    localFiles.forEach((localFile) => dispatch(actions.addLocalFile(localFile)));\n  });\n\n  const handleCompositionStart = () => {\n    dispatch(actions.setComposing(true));\n  };\n\n  const handleCompositionEnd = () => {\n    dispatch(actions.setComposing(false));\n  };\n\n  const handleContentChange = (content: string) => {\n    dispatch(actions.updateContent(content));\n  };\n\n  const handlePaste = (event: React.ClipboardEvent<Element>) => {\n    const clipboard = event.clipboardData;\n    if (!clipboard) return;\n\n    const files: File[] = [];\n    if (clipboard.items && clipboard.items.length > 0) {\n      for (const item of Array.from(clipboard.items)) {\n        if (item.kind !== \"file\") continue;\n        const file = item.getAsFile();\n        if (file) files.push(file);\n      }\n    } else if (clipboard.files && clipboard.files.length > 0) {\n      files.push(...Array.from(clipboard.files));\n    }\n\n    if (files.length === 0) return;\n\n    const localFiles: LocalFile[] = files.map((file) => ({\n      file,\n      previewUrl: createBlobUrl(file),\n    }));\n    localFiles.forEach((localFile) => dispatch(actions.addLocalFile(localFile)));\n    event.preventDefault();\n  };\n\n  return (\n    <div className=\"w-full flex flex-col flex-1\" {...dragHandlers}>\n      <Editor\n        ref={ref}\n        className=\"memo-editor-content\"\n        initialContent={state.content}\n        placeholder={placeholder || \"\"}\n        isFocusMode={state.ui.isFocusMode}\n        isInIME={state.ui.isComposing}\n        onContentChange={handleContentChange}\n        onPaste={handlePaste}\n        onCompositionStart={handleCompositionStart}\n        onCompositionEnd={handleCompositionEnd}\n      />\n    </div>\n  );\n});\n\nEditorContent.displayName = \"EditorContent\";\n"
  },
  {
    "path": "web/src/components/MemoEditor/components/EditorMetadata.tsx",
    "content": "import type { FC } from \"react\";\nimport { useEditorContext } from \"../state\";\nimport type { EditorMetadataProps } from \"../types\";\nimport AttachmentList from \"./AttachmentList\";\nimport LocationDisplay from \"./LocationDisplay\";\nimport RelationList from \"./RelationList\";\n\nexport const EditorMetadata: FC<EditorMetadataProps> = ({ memoName }) => {\n  const { state, actions, dispatch } = useEditorContext();\n\n  return (\n    <div className=\"w-full flex flex-col gap-2\">\n      <AttachmentList\n        attachments={state.metadata.attachments}\n        localFiles={state.localFiles}\n        onAttachmentsChange={(attachments) => dispatch(actions.setMetadata({ attachments }))}\n        onRemoveLocalFile={(previewUrl) => dispatch(actions.removeLocalFile(previewUrl))}\n      />\n\n      <RelationList\n        relations={state.metadata.relations}\n        onRelationsChange={(relations) => dispatch(actions.setMetadata({ relations }))}\n        memoName={memoName}\n      />\n\n      {state.metadata.location && (\n        <LocationDisplay location={state.metadata.location} onRemove={() => dispatch(actions.setMetadata({ location: undefined }))} />\n      )}\n    </div>\n  );\n};\n"
  },
  {
    "path": "web/src/components/MemoEditor/components/EditorToolbar.tsx",
    "content": "import type { FC } from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport { useTranslate } from \"@/utils/i18n\";\nimport { validationService } from \"../services\";\nimport { useEditorContext } from \"../state\";\nimport InsertMenu from \"../Toolbar/InsertMenu\";\nimport VisibilitySelector from \"../Toolbar/VisibilitySelector\";\nimport type { EditorToolbarProps } from \"../types\";\n\nexport const EditorToolbar: FC<EditorToolbarProps> = ({ onSave, onCancel, memoName }) => {\n  const t = useTranslate();\n  const { state, actions, dispatch } = useEditorContext();\n  const { valid } = validationService.canSave(state);\n\n  const isSaving = state.ui.isLoading.saving;\n\n  const handleLocationChange = (location: typeof state.metadata.location) => {\n    dispatch(actions.setMetadata({ location }));\n  };\n\n  const handleToggleFocusMode = () => {\n    dispatch(actions.toggleFocusMode());\n  };\n\n  const handleVisibilityChange = (visibility: typeof state.metadata.visibility) => {\n    dispatch(actions.setMetadata({ visibility }));\n  };\n\n  return (\n    <div className=\"w-full flex flex-row justify-between items-center mb-2\">\n      <div className=\"flex flex-row justify-start items-center\">\n        <InsertMenu\n          isUploading={state.ui.isLoading.uploading}\n          location={state.metadata.location}\n          onLocationChange={handleLocationChange}\n          onToggleFocusMode={handleToggleFocusMode}\n          memoName={memoName}\n        />\n      </div>\n\n      <div className=\"flex flex-row justify-end items-center gap-2\">\n        <VisibilitySelector value={state.metadata.visibility} onChange={handleVisibilityChange} />\n\n        {onCancel && (\n          <Button variant=\"ghost\" onClick={onCancel} disabled={isSaving}>\n            {t(\"common.cancel\")}\n          </Button>\n        )}\n\n        <Button onClick={onSave} disabled={!valid || isSaving}>\n          {isSaving ? t(\"editor.saving\") : t(\"editor.save\")}\n        </Button>\n      </div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "web/src/components/MemoEditor/components/FocusModeOverlay.tsx",
    "content": "import { Minimize2Icon } from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { FOCUS_MODE_STYLES } from \"../constants\";\nimport type { FocusModeExitButtonProps, FocusModeOverlayProps } from \"../types\";\n\nexport function FocusModeOverlay({ isActive, onToggle }: FocusModeOverlayProps) {\n  if (!isActive) return null;\n\n  return <button type=\"button\" className={FOCUS_MODE_STYLES.backdrop} onClick={onToggle} aria-label=\"Exit focus mode\" />;\n}\n\nexport function FocusModeExitButton({ isActive, onToggle, title }: FocusModeExitButtonProps) {\n  if (!isActive) return null;\n\n  return (\n    <Button variant=\"ghost\" size=\"icon\" className={FOCUS_MODE_STYLES.exitButton} onClick={onToggle} title={title}>\n      <Minimize2Icon className=\"w-4 h-4\" />\n    </Button>\n  );\n}\n"
  },
  {
    "path": "web/src/components/MemoEditor/components/LinkMemoDialog.tsx",
    "content": "import { timestampDate } from \"@bufbuild/protobuf/wkt\";\nimport { LinkIcon } from \"lucide-react\";\nimport { MemoPreview } from \"@/components/MemoPreview\";\nimport { Dialog, DialogClose, DialogContent, DialogDescription, DialogTitle } from \"@/components/ui/dialog\";\nimport { Input } from \"@/components/ui/input\";\nimport { VisuallyHidden } from \"@/components/ui/visually-hidden\";\nimport { extractMemoIdFromName } from \"@/helpers/resource-names\";\nimport { cn } from \"@/lib/utils\";\nimport { useTranslate } from \"@/utils/i18n\";\nimport type { LinkMemoDialogProps } from \"../types\";\n\nexport const LinkMemoDialog = ({\n  open,\n  onOpenChange,\n  searchText,\n  onSearchChange,\n  filteredMemos,\n  isFetching,\n  onSelectMemo,\n  isAlreadyLinked,\n}: LinkMemoDialogProps) => {\n  const t = useTranslate();\n\n  return (\n    <Dialog open={open} onOpenChange={onOpenChange}>\n      <DialogContent className=\"max-w-[min(28rem,calc(100vw-2rem))] p-0!\" showCloseButton={false}>\n        <VisuallyHidden>\n          <DialogClose />\n        </VisuallyHidden>\n        <VisuallyHidden>\n          <DialogTitle>{t(\"tooltip.link-memo\")}</DialogTitle>\n        </VisuallyHidden>\n        <VisuallyHidden>\n          <DialogDescription>Search and select a memo to link</DialogDescription>\n        </VisuallyHidden>\n        <div className=\"flex flex-col\">\n          <div className=\"p-3\">\n            <Input\n              placeholder={t(\"reference.search-placeholder\")}\n              value={searchText}\n              onChange={(e) => onSearchChange(e.target.value)}\n              className=\"!text-sm h-9\"\n              autoFocus\n            />\n          </div>\n          <div className=\"border-t border-border\" />\n          <div className=\"max-h-[320px] overflow-y-auto\">\n            {filteredMemos.length === 0 ? (\n              <div className=\"py-8 text-center text-sm text-muted-foreground\">\n                {isFetching ? \"Loading...\" : t(\"reference.no-memos-found\")}\n              </div>\n            ) : (\n              filteredMemos.map((memo) => {\n                const alreadyLinked = isAlreadyLinked(memo.name);\n                return (\n                  <div\n                    key={memo.name}\n                    className={cn(\n                      \"flex cursor-pointer items-start border-b border-border last:border-b-0 px-3 py-2.5 hover:bg-accent/50 transition-colors\",\n                      alreadyLinked && \"opacity-40 cursor-default\",\n                    )}\n                    onClick={() => !alreadyLinked && onSelectMemo(memo)}\n                  >\n                    <div className=\"w-full flex flex-col gap-1\">\n                      <div className=\"flex items-center gap-1.5 text-sm text-muted-foreground select-none\">\n                        {alreadyLinked && <LinkIcon className=\"w-3 h-3 shrink-0\" />}\n                        <span className=\"text-xs font-mono px-1 py-0.5 rounded border border-border bg-muted/40 shrink-0\">\n                          {extractMemoIdFromName(memo.name).slice(0, 6)}\n                        </span>\n                        <span>{memo.displayTime && timestampDate(memo.displayTime).toLocaleString()}</span>\n                      </div>\n                      <MemoPreview content={memo.content} attachments={memo.attachments} />\n                    </div>\n                  </div>\n                );\n              })\n            )}\n          </div>\n        </div>\n      </DialogContent>\n    </Dialog>\n  );\n};\n"
  },
  {
    "path": "web/src/components/MemoEditor/components/LocationDialog.tsx",
    "content": "import { LocationPicker } from \"@/components/map\";\nimport { Button } from \"@/components/ui/button\";\nimport { Dialog, DialogClose, DialogContent, DialogDescription, DialogTitle } from \"@/components/ui/dialog\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport { Textarea } from \"@/components/ui/textarea\";\nimport { VisuallyHidden } from \"@/components/ui/visually-hidden\";\nimport { useTranslate } from \"@/utils/i18n\";\nimport type { LocationDialogProps } from \"../types\";\n\nexport const LocationDialog = ({\n  open,\n  onOpenChange,\n  state,\n  locationInitialized: _locationInitialized,\n  onPositionChange,\n  onUpdateCoordinate,\n  onPlaceholderChange,\n  onCancel,\n  onConfirm,\n}: LocationDialogProps) => {\n  const t = useTranslate();\n  const { placeholder, position, latInput, lngInput } = state;\n\n  return (\n    <Dialog open={open} onOpenChange={onOpenChange}>\n      <DialogContent className=\"max-w-[min(28rem,calc(100vw-2rem))] p-0!\">\n        <VisuallyHidden>\n          <DialogClose />\n        </VisuallyHidden>\n        <VisuallyHidden>\n          <DialogTitle>{t(\"tooltip.select-location\")}</DialogTitle>\n        </VisuallyHidden>\n        <VisuallyHidden>\n          <DialogDescription>Select a location on the map or enter coordinates manually</DialogDescription>\n        </VisuallyHidden>\n        <div className=\"flex flex-col\">\n          <div className=\"w-full h-64 overflow-hidden rounded-t-md bg-muted/30\">\n            <LocationPicker latlng={position} onChange={onPositionChange} />\n          </div>\n          <div className=\"w-full flex flex-col p-3 gap-3\">\n            <div className=\"grid grid-cols-2 gap-3\">\n              <div className=\"grid gap-1\">\n                <Label htmlFor=\"memo-location-lat\" className=\"text-xs uppercase tracking-wide text-muted-foreground\">\n                  Lat\n                </Label>\n                <Input\n                  id=\"memo-location-lat\"\n                  placeholder=\"Lat\"\n                  type=\"number\"\n                  step=\"any\"\n                  min=\"-90\"\n                  max=\"90\"\n                  value={latInput}\n                  onChange={(e) => onUpdateCoordinate(\"lat\", e.target.value)}\n                  className=\"h-9\"\n                />\n              </div>\n              <div className=\"grid gap-1\">\n                <Label htmlFor=\"memo-location-lng\" className=\"text-xs uppercase tracking-wide text-muted-foreground\">\n                  Lng\n                </Label>\n                <Input\n                  id=\"memo-location-lng\"\n                  placeholder=\"Lng\"\n                  type=\"number\"\n                  step=\"any\"\n                  min=\"-180\"\n                  max=\"180\"\n                  value={lngInput}\n                  onChange={(e) => onUpdateCoordinate(\"lng\", e.target.value)}\n                  className=\"h-9\"\n                />\n              </div>\n            </div>\n            <div className=\"grid gap-1\">\n              <Label htmlFor=\"memo-location-placeholder\" className=\"text-xs uppercase tracking-wide text-muted-foreground\">\n                {t(\"tooltip.select-location\")}\n              </Label>\n              <Textarea\n                id=\"memo-location-placeholder\"\n                placeholder=\"Choose a position first.\"\n                value={placeholder}\n                disabled={!position}\n                onChange={(e) => onPlaceholderChange(e.target.value)}\n                className=\"min-h-16\"\n              />\n            </div>\n            <div className=\"w-full flex items-center justify-end gap-2\">\n              <Button variant=\"ghost\" onClick={onCancel}>\n                {t(\"common.close\")}\n              </Button>\n              <Button onClick={onConfirm} disabled={!position || placeholder.trim().length === 0}>\n                {t(\"common.confirm\")}\n              </Button>\n            </div>\n          </div>\n        </div>\n      </DialogContent>\n    </Dialog>\n  );\n};\n"
  },
  {
    "path": "web/src/components/MemoEditor/components/LocationDisplay.tsx",
    "content": "import { MapPinIcon, XIcon } from \"lucide-react\";\nimport type { FC } from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport type { Location } from \"@/types/proto/api/v1/memo_service_pb\";\n\ninterface LocationDisplayProps {\n  location: Location;\n  onRemove?: () => void;\n  className?: string;\n}\n\nconst LocationDisplay: FC<LocationDisplayProps> = ({ location, onRemove, className }) => {\n  const displayText = location.placeholder || `${location.latitude.toFixed(6)}, ${location.longitude.toFixed(6)}`;\n\n  return (\n    <div\n      className={cn(\n        \"relative flex items-center gap-1.5 px-1.5 py-1 rounded border border-border bg-muted/20 hover:bg-accent/20 transition-all w-full\",\n        className,\n      )}\n    >\n      <MapPinIcon className=\"w-3.5 h-3.5 shrink-0 text-muted-foreground\" />\n\n      <div className=\"flex items-center gap-1.5 min-w-0 flex-1\">\n        <span className=\"text-xs truncate\" title={displayText}>\n          {displayText}\n        </span>\n        <span className=\"text-[11px] text-muted-foreground shrink-0 hidden sm:inline\">\n          {location.latitude.toFixed(4)}°, {location.longitude.toFixed(4)}°\n        </span>\n      </div>\n\n      {onRemove && (\n        <button\n          type=\"button\"\n          onClick={onRemove}\n          className=\"p-0.5 rounded hover:bg-destructive/10 active:bg-destructive/10 transition-colors touch-manipulation shrink-0 ml-auto\"\n          title=\"Remove\"\n          aria-label=\"Remove location\"\n        >\n          <XIcon className=\"w-3 h-3 text-muted-foreground hover:text-destructive\" />\n        </button>\n      )}\n    </div>\n  );\n};\n\nexport default LocationDisplay;\n"
  },
  {
    "path": "web/src/components/MemoEditor/components/RelationList.tsx",
    "content": "import { create } from \"@bufbuild/protobuf\";\nimport { LinkIcon, XIcon } from \"lucide-react\";\nimport type { FC } from \"react\";\nimport { useEffect, useState } from \"react\";\nimport RelationCard from \"@/components/MemoView/components/metadata/RelationCard\";\nimport { memoServiceClient } from \"@/connect\";\nimport type { MemoRelation } from \"@/types/proto/api/v1/memo_service_pb\";\nimport { MemoRelation_Memo, MemoRelation_MemoSchema, MemoRelation_Type } from \"@/types/proto/api/v1/memo_service_pb\";\n\ninterface RelationListProps {\n  relations: MemoRelation[];\n  onRelationsChange?: (relations: MemoRelation[]) => void;\n  parentPage?: string;\n  memoName?: string;\n}\n\nconst RelationItemCard: FC<{\n  memo: MemoRelation[\"relatedMemo\"];\n  onRemove?: () => void;\n  parentPage?: string;\n}> = ({ memo, onRemove, parentPage }) => {\n  return (\n    <div className=\"group relative flex items-center justify-between w-full rounded hover:bg-accent/20 transition-colors\">\n      <RelationCard memo={memo!} parentPage={parentPage} className=\"flex-1 hover:bg-transparent\" />\n\n      {onRemove && (\n        <button\n          type=\"button\"\n          onClick={onRemove}\n          className=\"p-1 mr-0.5 rounded opacity-0 group-hover:opacity-100 hover:bg-destructive/10 active:bg-destructive/10 transition-all touch-manipulation\"\n          title=\"Remove\"\n          aria-label=\"Remove relation\"\n        >\n          <XIcon className=\"w-3 h-3 text-muted-foreground hover:text-destructive\" />\n        </button>\n      )}\n    </div>\n  );\n};\n\nconst RelationList: FC<RelationListProps> = ({ relations, onRelationsChange, parentPage, memoName }) => {\n  const referenceRelations = relations.filter(\n    (r) => r.type === MemoRelation_Type.REFERENCE && (!memoName || !r.memo?.name || r.memo.name === memoName),\n  );\n  const [fetchedMemos, setFetchedMemos] = useState<Record<string, MemoRelation_Memo>>({});\n\n  useEffect(() => {\n    (async () => {\n      const missingSnippetRelations = referenceRelations.filter((relation) => !relation.relatedMemo?.snippet && relation.relatedMemo?.name);\n      if (missingSnippetRelations.length > 0) {\n        const requests = missingSnippetRelations.map(async (relation) => {\n          const memo = await memoServiceClient.getMemo({ name: relation.relatedMemo!.name });\n          return create(MemoRelation_MemoSchema, { name: memo.name, snippet: memo.snippet });\n        });\n        const list = await Promise.all(requests);\n        setFetchedMemos((prev) => {\n          const next = { ...prev };\n          for (const memo of list) {\n            next[memo.name] = memo;\n          }\n          return next;\n        });\n      }\n    })();\n  }, [referenceRelations]);\n\n  const handleDeleteRelation = (memoName: string) => {\n    if (onRelationsChange) {\n      onRelationsChange(relations.filter((relation) => relation.relatedMemo?.name !== memoName));\n    }\n  };\n\n  if (referenceRelations.length === 0) {\n    return null;\n  }\n\n  return (\n    <div className=\"w-full rounded-lg border border-border bg-muted/20 overflow-hidden\">\n      <div className=\"flex items-center gap-1.5 px-2 py-1 border-b border-border bg-muted/30\">\n        <LinkIcon className=\"w-3.5 h-3.5 text-muted-foreground\" />\n        <span className=\"text-xs text-muted-foreground\">Relations ({referenceRelations.length})</span>\n      </div>\n\n      <div className=\"p-1 sm:p-1.5 flex flex-col gap-0.5\">\n        {referenceRelations.map((relation) => {\n          const relatedMemo = relation.relatedMemo!;\n          const memo = relatedMemo.snippet ? relatedMemo : fetchedMemos[relatedMemo.name] || relatedMemo;\n          return <RelationItemCard key={memo.name} memo={memo} onRemove={() => handleDeleteRelation(memo.name)} parentPage={parentPage} />;\n        })}\n      </div>\n    </div>\n  );\n};\n\nexport default RelationList;\n"
  },
  {
    "path": "web/src/components/MemoEditor/components/TimestampPopover.tsx",
    "content": "import { type FC, useRef, useState } from \"react\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"@/components/ui/popover\";\nimport { useTranslate } from \"@/utils/i18n\";\nimport { useEditorContext } from \"../state\";\n\nconst DATETIME_FORMAT = \"YYYY-MM-DD HH:mm:ss\";\n\nfunction formatDate(date: Date): string {\n  const pad = (n: number) => String(n).padStart(2, \"0\");\n  return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;\n}\n\nfunction parseDate(value: string): Date | undefined {\n  const match = value.match(/^(\\d{4})-(\\d{2})-(\\d{2}) (\\d{2}):(\\d{2}):(\\d{2})$/);\n  if (!match) return undefined;\n  const date = new Date(Number(match[1]), Number(match[2]) - 1, Number(match[3]), Number(match[4]), Number(match[5]), Number(match[6]));\n  return Number.isNaN(date.getTime()) ? undefined : date;\n}\n\nconst TimestampInput: FC<{\n  label: string;\n  date: Date | undefined;\n  onChange: (date: Date) => void;\n}> = ({ label, date, onChange }) => {\n  const initialValue = useRef(date ? formatDate(date) : \"\");\n  const [value, setValue] = useState(initialValue.current);\n  const [invalid, setInvalid] = useState(false);\n\n  const handleBlur = () => {\n    const parsed = parseDate(value);\n    if (parsed) {\n      setInvalid(false);\n      onChange(parsed);\n    } else {\n      setInvalid(true);\n    }\n  };\n\n  return (\n    <div className=\"space-y-1\">\n      <label className=\"text-xs font-medium text-muted-foreground\">\n        {label}\n        {value !== initialValue.current && <span className=\"text-primary ml-0.5\">*</span>}\n      </label>\n      <input\n        type=\"text\"\n        className=\"block w-full rounded-md border border-border bg-background px-2 py-1 text-sm font-mono data-[invalid=true]:border-destructive\"\n        data-invalid={invalid}\n        placeholder={DATETIME_FORMAT}\n        value={value}\n        onChange={(e) => setValue(e.target.value)}\n        onBlur={handleBlur}\n      />\n    </div>\n  );\n};\n\nexport const TimestampPopover: FC = () => {\n  const t = useTranslate();\n  const { state, actions, dispatch } = useEditorContext();\n  const { createTime, updateTime } = state.timestamps;\n\n  if (!createTime) return null;\n\n  return (\n    <Popover>\n      <PopoverTrigger asChild>\n        <button\n          type=\"button\"\n          className=\"w-auto text-sm text-muted-foreground text-left hover:text-foreground transition-colors cursor-pointer\"\n        >\n          {formatDate(createTime)}\n        </button>\n      </PopoverTrigger>\n      <PopoverContent align=\"start\" className=\"w-auto p-2 pt-1 space-y-1\">\n        <TimestampInput\n          label={t(\"common.created-at\")}\n          date={createTime}\n          onChange={(d) => dispatch(actions.setTimestamps({ createTime: d }))}\n        />\n        <TimestampInput\n          label={t(\"common.last-updated-at\")}\n          date={updateTime}\n          onChange={(d) => dispatch(actions.setTimestamps({ updateTime: d }))}\n        />\n      </PopoverContent>\n    </Popover>\n  );\n};\n"
  },
  {
    "path": "web/src/components/MemoEditor/components/index.ts",
    "content": "// UI components for MemoEditor\n\nexport { default as AttachmentList } from \"./AttachmentList\";\nexport * from \"./EditorContent\";\nexport * from \"./EditorMetadata\";\nexport * from \"./EditorToolbar\";\nexport { FocusModeExitButton, FocusModeOverlay } from \"./FocusModeOverlay\";\nexport { LinkMemoDialog } from \"./LinkMemoDialog\";\nexport { LocationDialog } from \"./LocationDialog\";\nexport { default as LocationDisplay } from \"./LocationDisplay\";\nexport { default as RelationList } from \"./RelationList\";\nexport { TimestampPopover } from \"./TimestampPopover\";\n"
  },
  {
    "path": "web/src/components/MemoEditor/constants.ts",
    "content": "export const LOCALSTORAGE_DEBOUNCE_DELAY = 500;\n\nexport const FOCUS_MODE_STYLES = {\n  backdrop: \"fixed inset-0 bg-black/20 backdrop-blur-sm z-40\",\n  container: {\n    base: \"fixed z-50 w-auto max-w-5xl mx-auto shadow-2xl border-border h-auto overflow-y-auto\",\n    spacing: \"top-2 left-2 right-2 bottom-2 sm:top-4 sm:left-4 sm:right-4 sm:bottom-4 md:top-8 md:left-8 md:right-8 md:bottom-8\",\n  },\n  transition: \"transition-all duration-300 ease-in-out\",\n  exitButton: \"absolute top-2 right-2 z-10 opacity-60 hover:opacity-100\",\n} as const;\n\nexport const EDITOR_HEIGHT = {\n  // Max height for normal mode - focus mode uses flex-1 to grow dynamically\n  normal: \"max-h-[50vh]\",\n} as const;\n"
  },
  {
    "path": "web/src/components/MemoEditor/hooks/index.ts",
    "content": "// Custom hooks for MemoEditor (internal use only)\nexport { useAutoSave } from \"./useAutoSave\";\nexport { useBlobUrls } from \"./useBlobUrls\";\nexport { useDragAndDrop } from \"./useDragAndDrop\";\nexport { useFileUpload } from \"./useFileUpload\";\nexport { useFocusMode } from \"./useFocusMode\";\nexport { useKeyboard } from \"./useKeyboard\";\nexport { useLinkMemo } from \"./useLinkMemo\";\nexport { useLocation } from \"./useLocation\";\nexport { useMemoInit } from \"./useMemoInit\";\n"
  },
  {
    "path": "web/src/components/MemoEditor/hooks/useAutoSave.ts",
    "content": "import { useEffect } from \"react\";\nimport { cacheService } from \"../services\";\n\nexport const useAutoSave = (content: string, username: string, cacheKey: string | undefined) => {\n  useEffect(() => {\n    const key = cacheService.key(username, cacheKey);\n    cacheService.save(key, content);\n  }, [content, username, cacheKey]);\n};\n"
  },
  {
    "path": "web/src/components/MemoEditor/hooks/useBlobUrls.ts",
    "content": "import { useEffect, useRef } from \"react\";\n\nexport function useBlobUrls() {\n  const urlsRef = useRef<Set<string>>(new Set());\n\n  useEffect(\n    () => () => {\n      for (const url of urlsRef.current) {\n        URL.revokeObjectURL(url);\n      }\n    },\n    [],\n  );\n\n  return {\n    createBlobUrl: (blob: Blob | File): string => {\n      const url = URL.createObjectURL(blob);\n      urlsRef.current.add(url);\n      return url;\n    },\n    revokeBlobUrl: (url: string) => {\n      if (urlsRef.current.has(url)) {\n        URL.revokeObjectURL(url);\n        urlsRef.current.delete(url);\n      }\n    },\n  };\n}\n"
  },
  {
    "path": "web/src/components/MemoEditor/hooks/useDragAndDrop.ts",
    "content": "export function useDragAndDrop(onDrop: (files: FileList) => void) {\n  return {\n    dragHandlers: {\n      onDragOver: (e: React.DragEvent) => {\n        if (e.dataTransfer?.types.includes(\"Files\")) {\n          e.preventDefault();\n          e.dataTransfer.dropEffect = \"copy\";\n        }\n      },\n      onDragLeave: (e: React.DragEvent) => {\n        e.preventDefault();\n      },\n      onDrop: (e: React.DragEvent) => {\n        if (e.dataTransfer?.files.length) {\n          e.preventDefault();\n          onDrop(e.dataTransfer.files);\n        }\n      },\n    },\n  };\n}\n"
  },
  {
    "path": "web/src/components/MemoEditor/hooks/useFileUpload.ts",
    "content": "import { useRef } from \"react\";\nimport type { LocalFile } from \"../types/attachment\";\n\nexport const useFileUpload = (onFilesSelected: (localFiles: LocalFile[]) => void) => {\n  const fileInputRef = useRef<HTMLInputElement>(null);\n  const selectingFlagRef = useRef(false);\n\n  const handleFileInputChange = (event?: React.ChangeEvent<HTMLInputElement>) => {\n    const files = Array.from(fileInputRef.current?.files || event?.target.files || []);\n    if (files.length === 0 || selectingFlagRef.current) {\n      return;\n    }\n    selectingFlagRef.current = true;\n    const localFiles: LocalFile[] = files.map((file) => ({\n      file,\n      previewUrl: URL.createObjectURL(file),\n    }));\n    onFilesSelected(localFiles);\n    selectingFlagRef.current = false;\n    // Optionally clear input value to allow re-selecting the same file\n    if (fileInputRef.current) fileInputRef.current.value = \"\";\n  };\n\n  const handleUploadClick = () => {\n    fileInputRef.current?.click();\n  };\n\n  return {\n    fileInputRef,\n    selectingFlag: selectingFlagRef.current,\n    handleFileInputChange,\n    handleUploadClick,\n  };\n};\n"
  },
  {
    "path": "web/src/components/MemoEditor/hooks/useFocusMode.ts",
    "content": "import { useEffect } from \"react\";\n\nexport function useFocusMode(isFocusMode: boolean): void {\n  useEffect(() => {\n    document.body.style.overflow = isFocusMode ? \"hidden\" : \"\";\n    return () => {\n      document.body.style.overflow = \"\";\n    };\n  }, [isFocusMode]);\n}\n"
  },
  {
    "path": "web/src/components/MemoEditor/hooks/useKeyboard.ts",
    "content": "import { useEffect } from \"react\";\nimport type { EditorRefActions } from \"../Editor\";\n\ninterface UseKeyboardOptions {\n  onSave: () => void;\n}\n\nexport const useKeyboard = (editorRef: React.RefObject<EditorRefActions | null>, options: UseKeyboardOptions) => {\n  useEffect(() => {\n    const handleKeyDown = (event: KeyboardEvent) => {\n      if (!(event.metaKey || event.ctrlKey) || event.key !== \"Enter\") {\n        return;\n      }\n\n      const editor = editorRef.current?.getEditor();\n      if (!editor) {\n        return;\n      }\n\n      const activeElement = document.activeElement;\n      const target = event.target;\n      if (activeElement !== editor && target !== editor) {\n        return;\n      }\n\n      event.preventDefault();\n      options.onSave();\n    };\n\n    window.addEventListener(\"keydown\", handleKeyDown);\n    return () => window.removeEventListener(\"keydown\", handleKeyDown);\n  }, [editorRef, options]);\n};\n"
  },
  {
    "path": "web/src/components/MemoEditor/hooks/useLinkMemo.ts",
    "content": "import { create } from \"@bufbuild/protobuf\";\nimport { useEffect, useMemo, useState } from \"react\";\nimport useDebounce from \"react-use/lib/useDebounce\";\nimport { memoServiceClient } from \"@/connect\";\nimport { DEFAULT_LIST_MEMOS_PAGE_SIZE } from \"@/helpers/consts\";\nimport { extractUserIdFromName } from \"@/helpers/resource-names\";\nimport useCurrentUser from \"@/hooks/useCurrentUser\";\nimport {\n  type Memo,\n  type MemoRelation,\n  MemoRelation_MemoSchema,\n  MemoRelation_Type,\n  MemoRelationSchema,\n} from \"@/types/proto/api/v1/memo_service_pb\";\n\ninterface UseLinkMemoParams {\n  isOpen: boolean;\n  currentMemoName?: string;\n  existingRelations: MemoRelation[];\n  onAddRelation: (relation: MemoRelation) => void;\n}\n\nexport const useLinkMemo = ({ isOpen, currentMemoName, existingRelations, onAddRelation }: UseLinkMemoParams) => {\n  const user = useCurrentUser();\n  const [searchText, setSearchText] = useState(\"\");\n  const [isFetching, setIsFetching] = useState(true);\n  const [fetchedMemos, setFetchedMemos] = useState<Memo[]>([]);\n\n  const filteredMemos = fetchedMemos.filter((memo) => memo.name !== currentMemoName);\n\n  const linkedMemoNames = useMemo(() => new Set(existingRelations.map((r) => r.relatedMemo?.name)), [existingRelations]);\n\n  const isAlreadyLinked = (memoName: string): boolean => linkedMemoNames.has(memoName);\n\n  useEffect(() => {\n    if (isOpen) {\n      setSearchText(\"\");\n    }\n  }, [isOpen]);\n\n  useDebounce(\n    async () => {\n      if (!isOpen) return;\n\n      setIsFetching(true);\n      try {\n        const conditions = [`creator_id == ${extractUserIdFromName(user?.name ?? \"\")}`];\n        if (searchText) {\n          conditions.push(`content.contains(\"${searchText}\")`);\n        }\n        const { memos } = await memoServiceClient.listMemos({\n          pageSize: DEFAULT_LIST_MEMOS_PAGE_SIZE,\n          filter: conditions.join(\" && \"),\n        });\n        setFetchedMemos(memos);\n      } catch (error) {\n        console.error(error);\n      } finally {\n        setIsFetching(false);\n      }\n    },\n    300,\n    [isOpen, searchText],\n  );\n\n  const addMemoRelation = (memo: Memo) => {\n    const relation = create(MemoRelationSchema, {\n      type: MemoRelation_Type.REFERENCE,\n      relatedMemo: create(MemoRelation_MemoSchema, {\n        name: memo.name,\n        snippet: memo.snippet,\n      }),\n    });\n    onAddRelation(relation);\n  };\n\n  return {\n    searchText,\n    setSearchText,\n    isFetching,\n    filteredMemos,\n    addMemoRelation,\n    isAlreadyLinked,\n  };\n};\n"
  },
  {
    "path": "web/src/components/MemoEditor/hooks/useLocation.ts",
    "content": "import { create } from \"@bufbuild/protobuf\";\nimport { LatLng } from \"leaflet\";\nimport { useState } from \"react\";\nimport { Location, LocationSchema } from \"@/types/proto/api/v1/memo_service_pb\";\nimport { LocationState } from \"../types/insert-menu\";\n\nexport const useLocation = (initialLocation?: Location) => {\n  const [locationInitialized, setLocationInitialized] = useState(false);\n  const [state, setState] = useState<LocationState>({\n    placeholder: initialLocation?.placeholder || \"\",\n    position: initialLocation ? new LatLng(initialLocation.latitude, initialLocation.longitude) : undefined,\n    latInput: initialLocation ? String(initialLocation.latitude) : \"\",\n    lngInput: initialLocation ? String(initialLocation.longitude) : \"\",\n  });\n\n  const updatePosition = (position?: LatLng) => {\n    setState((prev) => ({\n      ...prev,\n      position,\n      latInput: position ? String(position.lat) : \"\",\n      lngInput: position ? String(position.lng) : \"\",\n    }));\n  };\n\n  const handlePositionChange = (position: LatLng) => {\n    if (!locationInitialized) setLocationInitialized(true);\n    updatePosition(position);\n  };\n\n  const updateCoordinate = (type: \"lat\" | \"lng\", value: string) => {\n    setState((prev) => ({ ...prev, [type === \"lat\" ? \"latInput\" : \"lngInput\"]: value }));\n    const num = parseFloat(value);\n    const isValid = type === \"lat\" ? !isNaN(num) && num >= -90 && num <= 90 : !isNaN(num) && num >= -180 && num <= 180;\n    if (isValid && state.position) {\n      updatePosition(type === \"lat\" ? new LatLng(num, state.position.lng) : new LatLng(state.position.lat, num));\n    }\n  };\n\n  const setPlaceholder = (placeholder: string) => {\n    setState((prev) => ({ ...prev, placeholder }));\n  };\n\n  const reset = () => {\n    setState({\n      placeholder: \"\",\n      position: undefined,\n      latInput: \"\",\n      lngInput: \"\",\n    });\n    setLocationInitialized(false);\n  };\n\n  const getLocation = (): Location | undefined => {\n    if (!state.position || !state.placeholder.trim()) {\n      return undefined;\n    }\n    return create(LocationSchema, {\n      latitude: state.position.lat,\n      longitude: state.position.lng,\n      placeholder: state.placeholder,\n    });\n  };\n\n  return {\n    state,\n    locationInitialized,\n    handlePositionChange,\n    updateCoordinate,\n    setPlaceholder,\n    reset,\n    getLocation,\n  };\n};\n"
  },
  {
    "path": "web/src/components/MemoEditor/hooks/useMemoInit.ts",
    "content": "import { useEffect, useRef } from \"react\";\nimport type { Memo, Visibility } from \"@/types/proto/api/v1/memo_service_pb\";\nimport type { EditorRefActions } from \"../Editor\";\nimport { cacheService, memoService } from \"../services\";\nimport { useEditorContext } from \"../state\";\n\ninterface UseMemoInitOptions {\n  editorRef: React.RefObject<EditorRefActions | null>;\n  memo?: Memo;\n  cacheKey?: string;\n  username: string;\n  autoFocus?: boolean;\n  defaultVisibility?: Visibility;\n}\n\nexport const useMemoInit = ({ editorRef, memo, cacheKey, username, autoFocus, defaultVisibility }: UseMemoInitOptions) => {\n  const { actions, dispatch } = useEditorContext();\n  const initializedRef = useRef(false);\n\n  useEffect(() => {\n    if (initializedRef.current) return;\n    initializedRef.current = true;\n\n    if (memo) {\n      dispatch(actions.initMemo(memoService.fromMemo(memo)));\n    } else {\n      const cachedContent = cacheService.load(cacheService.key(username, cacheKey));\n      if (cachedContent) {\n        dispatch(actions.updateContent(cachedContent));\n      }\n      if (defaultVisibility !== undefined) {\n        dispatch(actions.setMetadata({ visibility: defaultVisibility }));\n      }\n    }\n\n    if (autoFocus) {\n      setTimeout(() => editorRef.current?.focus(), 100);\n    }\n  }, [memo, cacheKey, username, autoFocus, defaultVisibility, actions, dispatch, editorRef]);\n};\n"
  },
  {
    "path": "web/src/components/MemoEditor/index.tsx",
    "content": "import { useQueryClient } from \"@tanstack/react-query\";\nimport { useRef } from \"react\";\nimport { toast } from \"react-hot-toast\";\nimport { useAuth } from \"@/contexts/AuthContext\";\nimport useCurrentUser from \"@/hooks/useCurrentUser\";\nimport { memoKeys } from \"@/hooks/useMemoQueries\";\nimport { userKeys } from \"@/hooks/useUserQueries\";\nimport { handleError } from \"@/lib/error\";\nimport { cn } from \"@/lib/utils\";\nimport { useTranslate } from \"@/utils/i18n\";\nimport { convertVisibilityFromString } from \"@/utils/memo\";\nimport { EditorContent, EditorMetadata, EditorToolbar, FocusModeExitButton, FocusModeOverlay, TimestampPopover } from \"./components\";\nimport { FOCUS_MODE_STYLES } from \"./constants\";\nimport type { EditorRefActions } from \"./Editor\";\nimport { useAutoSave, useFocusMode, useKeyboard, useMemoInit } from \"./hooks\";\nimport { cacheService, errorService, memoService, validationService } from \"./services\";\nimport { EditorProvider, useEditorContext } from \"./state\";\nimport type { MemoEditorProps } from \"./types\";\n\nconst MemoEditor = (props: MemoEditorProps) => (\n  <EditorProvider>\n    <MemoEditorImpl {...props} />\n  </EditorProvider>\n);\n\nconst MemoEditorImpl: React.FC<MemoEditorProps> = ({\n  className,\n  cacheKey,\n  memo,\n  parentMemoName,\n  autoFocus,\n  placeholder,\n  onConfirm,\n  onCancel,\n}) => {\n  const t = useTranslate();\n  const queryClient = useQueryClient();\n  const currentUser = useCurrentUser();\n  const editorRef = useRef<EditorRefActions>(null);\n  const { state, actions, dispatch } = useEditorContext();\n  const { userGeneralSetting } = useAuth();\n\n  const memoName = memo?.name;\n\n  // Get default visibility from user settings\n  const defaultVisibility = userGeneralSetting?.memoVisibility ? convertVisibilityFromString(userGeneralSetting.memoVisibility) : undefined;\n\n  useMemoInit({ editorRef, memo, cacheKey, username: currentUser?.name ?? \"\", autoFocus, defaultVisibility });\n\n  // Auto-save content to localStorage\n  useAutoSave(state.content, currentUser?.name ?? \"\", cacheKey);\n\n  // Focus mode management with body scroll lock\n  useFocusMode(state.ui.isFocusMode);\n\n  const handleToggleFocusMode = () => {\n    dispatch(actions.toggleFocusMode());\n  };\n\n  useKeyboard(editorRef, { onSave: handleSave });\n\n  async function handleSave() {\n    // Validate before saving\n    const { valid, reason } = validationService.canSave(state);\n    if (!valid) {\n      toast.error(reason || \"Cannot save\");\n      return;\n    }\n\n    dispatch(actions.setLoading(\"saving\", true));\n\n    try {\n      const result = await memoService.save(state, { memoName, parentMemoName });\n\n      if (!result.hasChanges) {\n        toast.error(t(\"editor.no-changes-detected\"));\n        onCancel?.();\n        return;\n      }\n\n      // Clear localStorage cache on successful save\n      cacheService.clear(cacheService.key(currentUser?.name ?? \"\", cacheKey));\n\n      // Invalidate React Query cache to refresh memo lists across the app\n      const invalidationPromises = [\n        queryClient.invalidateQueries({ queryKey: memoKeys.lists() }),\n        queryClient.invalidateQueries({ queryKey: userKeys.stats() }),\n      ];\n\n      // Ensure memo detail pages don't keep stale cached content after edits.\n      if (memoName) {\n        invalidationPromises.push(queryClient.invalidateQueries({ queryKey: memoKeys.detail(memoName) }));\n      }\n\n      // If this was a comment, also invalidate the comments query for the parent memo\n      if (parentMemoName) {\n        invalidationPromises.push(queryClient.invalidateQueries({ queryKey: memoKeys.comments(parentMemoName) }));\n      }\n\n      await Promise.all(invalidationPromises);\n\n      // Reset editor state to initial values\n      dispatch(actions.reset());\n      if (!memoName && defaultVisibility) {\n        dispatch(actions.setMetadata({ visibility: defaultVisibility }));\n      }\n\n      // Notify parent component of successful save\n      onConfirm?.(result.memoName);\n    } catch (error) {\n      handleError(error, toast.error, {\n        context: \"Failed to save memo\",\n        fallbackMessage: errorService.getErrorMessage(error),\n      });\n    } finally {\n      dispatch(actions.setLoading(\"saving\", false));\n    }\n  }\n\n  return (\n    <>\n      <FocusModeOverlay isActive={state.ui.isFocusMode} onToggle={handleToggleFocusMode} />\n\n      {/*\n        Layout structure:\n        - Uses justify-between to push content to top and bottom\n        - In focus mode: becomes fixed with specific spacing, editor grows to fill space\n        - In normal mode: stays relative with max-height constraint\n      */}\n      <div\n        className={cn(\n          \"group relative w-full flex flex-col justify-between items-start bg-card px-4 pt-3 pb-1 rounded-lg border border-border gap-2\",\n          FOCUS_MODE_STYLES.transition,\n          state.ui.isFocusMode && cn(FOCUS_MODE_STYLES.container.base, FOCUS_MODE_STYLES.container.spacing),\n          className,\n        )}\n      >\n        {/* Exit button is absolutely positioned in top-right corner when active */}\n        <FocusModeExitButton isActive={state.ui.isFocusMode} onToggle={handleToggleFocusMode} title={t(\"editor.exit-focus-mode\")} />\n\n        {memoName && (\n          <div className=\"w-full -mb-1\">\n            <TimestampPopover />\n          </div>\n        )}\n\n        {/* Editor content grows to fill available space in focus mode */}\n        <EditorContent ref={editorRef} placeholder={placeholder} autoFocus={autoFocus} />\n\n        {/* Metadata and toolbar grouped together at bottom */}\n        <div className=\"w-full flex flex-col gap-2\">\n          <EditorMetadata memoName={memoName} />\n          <EditorToolbar onSave={handleSave} onCancel={onCancel} memoName={memoName} />\n        </div>\n      </div>\n    </>\n  );\n};\n\nexport default MemoEditor;\n"
  },
  {
    "path": "web/src/components/MemoEditor/services/cacheService.ts",
    "content": "import { debounce } from \"lodash-es\";\n\nexport const CACHE_DEBOUNCE_DELAY = 500;\n\nexport const cacheService = {\n  key: (username: string, cacheKey?: string): string => {\n    return `${username}-${cacheKey || \"\"}`;\n  },\n\n  save: debounce((key: string, content: string) => {\n    if (content.trim()) {\n      localStorage.setItem(key, content);\n    } else {\n      localStorage.removeItem(key);\n    }\n  }, CACHE_DEBOUNCE_DELAY),\n\n  load(key: string): string {\n    return localStorage.getItem(key) || \"\";\n  },\n\n  clear(key: string): void {\n    localStorage.removeItem(key);\n  },\n};\n"
  },
  {
    "path": "web/src/components/MemoEditor/services/errorService.ts",
    "content": "export const errorService = {\n  getErrorMessage(error: unknown): string {\n    // Handle ConnectError or errors with details property\n    if (error && typeof error === \"object\" && \"details\" in error) {\n      return (error as { details?: string }).details || \"An error occurred\";\n    }\n\n    if (error instanceof Error) {\n      return error.message;\n    }\n\n    return \"An unknown error occurred\";\n  },\n};\n"
  },
  {
    "path": "web/src/components/MemoEditor/services/index.ts",
    "content": "export * from \"./cacheService\";\nexport * from \"./errorService\";\nexport * from \"./memoService\";\nexport * from \"./uploadService\";\nexport * from \"./validationService\";\n"
  },
  {
    "path": "web/src/components/MemoEditor/services/memoService.ts",
    "content": "import { create } from \"@bufbuild/protobuf\";\nimport { FieldMaskSchema, timestampDate, timestampFromDate } from \"@bufbuild/protobuf/wkt\";\nimport { isEqual } from \"lodash-es\";\nimport { memoServiceClient } from \"@/connect\";\nimport type { Attachment } from \"@/types/proto/api/v1/attachment_service_pb\";\nimport { AttachmentSchema } from \"@/types/proto/api/v1/attachment_service_pb\";\nimport type { Memo } from \"@/types/proto/api/v1/memo_service_pb\";\nimport { MemoSchema } from \"@/types/proto/api/v1/memo_service_pb\";\nimport type { EditorState } from \"../state\";\nimport { uploadService } from \"./uploadService\";\n\n/**\n * Converts attachments to reference format for API requests.\n * The backend only needs the attachment name to link it to a memo.\n */\nfunction toAttachmentReferences(attachments: Attachment[]): Attachment[] {\n  return attachments.map((a) => create(AttachmentSchema, { name: a.name }));\n}\n\nfunction buildUpdateMask(\n  prevMemo: Memo,\n  state: EditorState,\n  allAttachments: typeof state.metadata.attachments,\n): { mask: Set<string>; patch: Partial<Memo> } {\n  const mask = new Set<string>();\n  const patch: Partial<Memo> = {\n    name: prevMemo.name,\n    content: state.content,\n  };\n\n  if (!isEqual(state.content, prevMemo.content)) {\n    mask.add(\"content\");\n    patch.content = state.content;\n  }\n  if (!isEqual(state.metadata.visibility, prevMemo.visibility)) {\n    mask.add(\"visibility\");\n    patch.visibility = state.metadata.visibility;\n  }\n  if (!isEqual(allAttachments, prevMemo.attachments)) {\n    mask.add(\"attachments\");\n    patch.attachments = toAttachmentReferences(allAttachments);\n  }\n  if (!isEqual(state.metadata.relations, prevMemo.relations)) {\n    mask.add(\"relations\");\n    patch.relations = state.metadata.relations;\n  }\n  if (!isEqual(state.metadata.location, prevMemo.location)) {\n    mask.add(\"location\");\n    patch.location = state.metadata.location;\n  }\n\n  // Auto-update timestamp if content changed\n  if ([\"content\", \"attachments\", \"relations\", \"location\"].some((key) => mask.has(key))) {\n    mask.add(\"update_time\");\n  }\n\n  // Handle custom timestamps\n  if (state.timestamps.createTime) {\n    const prevCreateTime = prevMemo.createTime ? timestampDate(prevMemo.createTime) : undefined;\n    if (!isEqual(state.timestamps.createTime, prevCreateTime)) {\n      mask.add(\"create_time\");\n      patch.createTime = timestampFromDate(state.timestamps.createTime);\n    }\n  }\n  if (state.timestamps.updateTime) {\n    const prevUpdateTime = prevMemo.updateTime ? timestampDate(prevMemo.updateTime) : undefined;\n    if (!isEqual(state.timestamps.updateTime, prevUpdateTime)) {\n      mask.add(\"update_time\");\n      patch.updateTime = timestampFromDate(state.timestamps.updateTime);\n    }\n  }\n\n  return { mask, patch };\n}\n\nexport const memoService = {\n  async save(\n    state: EditorState,\n    options: {\n      memoName?: string;\n      parentMemoName?: string;\n    },\n  ): Promise<{ memoName: string; hasChanges: boolean }> {\n    // 1. Upload local files first\n    const newAttachments = await uploadService.uploadFiles(state.localFiles);\n    const allAttachments = [...state.metadata.attachments, ...newAttachments];\n\n    // 2. Update existing memo\n    if (options.memoName) {\n      const prevMemo = await memoServiceClient.getMemo({ name: options.memoName });\n      const { mask, patch } = buildUpdateMask(prevMemo, state, allAttachments);\n\n      if (mask.size === 0) {\n        return { memoName: prevMemo.name, hasChanges: false };\n      }\n\n      const memo = await memoServiceClient.updateMemo({\n        memo: create(MemoSchema, patch as Record<string, unknown>),\n        updateMask: create(FieldMaskSchema, { paths: Array.from(mask) }),\n      });\n      return { memoName: memo.name, hasChanges: true };\n    }\n\n    // 3. Create new memo or comment\n    const memoData = create(MemoSchema, {\n      content: state.content,\n      visibility: state.metadata.visibility,\n      attachments: toAttachmentReferences(allAttachments),\n      relations: state.metadata.relations,\n      location: state.metadata.location,\n      createTime: state.timestamps.createTime ? timestampFromDate(state.timestamps.createTime) : undefined,\n      updateTime: state.timestamps.updateTime ? timestampFromDate(state.timestamps.updateTime) : undefined,\n    });\n\n    const memo = options.parentMemoName\n      ? await memoServiceClient.createMemoComment({\n          name: options.parentMemoName,\n          comment: memoData,\n        })\n      : await memoServiceClient.createMemo({ memo: memoData });\n\n    return { memoName: memo.name, hasChanges: true };\n  },\n\n  /** Build editor state from an already-loaded Memo entity (no network request). */\n  fromMemo(memo: Memo): EditorState {\n    return {\n      content: memo.content,\n      metadata: {\n        visibility: memo.visibility,\n        attachments: memo.attachments,\n        relations: memo.relations,\n        location: memo.location,\n      },\n      ui: {\n        isFocusMode: false,\n        isLoading: { saving: false, uploading: false, loading: false },\n        isDragging: false,\n        isComposing: false,\n      },\n      timestamps: {\n        createTime: memo.createTime ? timestampDate(memo.createTime) : undefined,\n        updateTime: memo.updateTime ? timestampDate(memo.updateTime) : undefined,\n      },\n      localFiles: [],\n    };\n  },\n};\n"
  },
  {
    "path": "web/src/components/MemoEditor/services/uploadService.ts",
    "content": "import { create } from \"@bufbuild/protobuf\";\nimport { attachmentServiceClient } from \"@/connect\";\nimport type { Attachment } from \"@/types/proto/api/v1/attachment_service_pb\";\nimport { AttachmentSchema } from \"@/types/proto/api/v1/attachment_service_pb\";\nimport type { LocalFile } from \"../types/attachment\";\n\nexport const uploadService = {\n  async uploadFiles(localFiles: LocalFile[]): Promise<Attachment[]> {\n    if (localFiles.length === 0) return [];\n\n    const attachments: Attachment[] = [];\n\n    for (const { file } of localFiles) {\n      const buffer = new Uint8Array(await file.arrayBuffer());\n      const attachment = await attachmentServiceClient.createAttachment({\n        attachment: create(AttachmentSchema, {\n          filename: file.name,\n          size: BigInt(file.size),\n          type: file.type,\n          content: buffer,\n        }),\n      });\n      attachments.push(attachment);\n    }\n\n    return attachments;\n  },\n};\n"
  },
  {
    "path": "web/src/components/MemoEditor/services/validationService.ts",
    "content": "import type { EditorState } from \"../state\";\n\nexport interface ValidationResult {\n  valid: boolean;\n  reason?: string;\n}\n\nexport const validationService = {\n  canSave(state: EditorState): ValidationResult {\n    // Cannot save while loading initial content\n    if (state.ui.isLoading.loading) {\n      return { valid: false, reason: \"Loading memo content\" };\n    }\n\n    // Must have content, attachment, or local file\n    if (!state.content.trim() && state.metadata.attachments.length === 0 && state.localFiles.length === 0) {\n      return { valid: false, reason: \"Content, attachment, or file required\" };\n    }\n\n    // Cannot save while uploading\n    if (state.ui.isLoading.uploading) {\n      return { valid: false, reason: \"Wait for upload to complete\" };\n    }\n\n    // Cannot save while already saving\n    if (state.ui.isLoading.saving) {\n      return { valid: false, reason: \"Save in progress\" };\n    }\n\n    return { valid: true };\n  },\n};\n"
  },
  {
    "path": "web/src/components/MemoEditor/state/actions.ts",
    "content": "import type { Attachment } from \"@/types/proto/api/v1/attachment_service_pb\";\nimport type { MemoRelation } from \"@/types/proto/api/v1/memo_service_pb\";\nimport type { LocalFile } from \"../types/attachment\";\nimport type { EditorAction, EditorState, LoadingKey } from \"./types\";\n\nexport const editorActions = {\n  initMemo: (payload: { content: string; metadata: EditorState[\"metadata\"]; timestamps: EditorState[\"timestamps\"] }): EditorAction => ({\n    type: \"INIT_MEMO\",\n    payload,\n  }),\n\n  updateContent: (content: string): EditorAction => ({\n    type: \"UPDATE_CONTENT\",\n    payload: content,\n  }),\n\n  setMetadata: (metadata: Partial<EditorState[\"metadata\"]>): EditorAction => ({\n    type: \"SET_METADATA\",\n    payload: metadata,\n  }),\n\n  addAttachment: (attachment: Attachment): EditorAction => ({\n    type: \"ADD_ATTACHMENT\",\n    payload: attachment,\n  }),\n\n  removeAttachment: (name: string): EditorAction => ({\n    type: \"REMOVE_ATTACHMENT\",\n    payload: name,\n  }),\n\n  addRelation: (relation: MemoRelation): EditorAction => ({\n    type: \"ADD_RELATION\",\n    payload: relation,\n  }),\n\n  removeRelation: (name: string): EditorAction => ({\n    type: \"REMOVE_RELATION\",\n    payload: name,\n  }),\n\n  addLocalFile: (file: LocalFile): EditorAction => ({\n    type: \"ADD_LOCAL_FILE\",\n    payload: file,\n  }),\n\n  removeLocalFile: (previewUrl: string): EditorAction => ({\n    type: \"REMOVE_LOCAL_FILE\",\n    payload: previewUrl,\n  }),\n\n  clearLocalFiles: (): EditorAction => ({\n    type: \"CLEAR_LOCAL_FILES\",\n  }),\n\n  toggleFocusMode: (): EditorAction => ({\n    type: \"TOGGLE_FOCUS_MODE\",\n  }),\n\n  setLoading: (key: LoadingKey, value: boolean): EditorAction => ({\n    type: \"SET_LOADING\",\n    payload: { key, value },\n  }),\n\n  setDragging: (value: boolean): EditorAction => ({\n    type: \"SET_DRAGGING\",\n    payload: value,\n  }),\n\n  setComposing: (value: boolean): EditorAction => ({\n    type: \"SET_COMPOSING\",\n    payload: value,\n  }),\n\n  setTimestamps: (timestamps: Partial<EditorState[\"timestamps\"]>): EditorAction => ({\n    type: \"SET_TIMESTAMPS\",\n    payload: timestamps,\n  }),\n\n  reset: (): EditorAction => ({\n    type: \"RESET\",\n  }),\n};\n"
  },
  {
    "path": "web/src/components/MemoEditor/state/context.tsx",
    "content": "import { createContext, type Dispatch, type FC, type PropsWithChildren, useContext, useMemo, useReducer } from \"react\";\nimport { editorActions } from \"./actions\";\nimport { editorReducer } from \"./reducer\";\nimport type { EditorAction, EditorState } from \"./types\";\nimport { initialState } from \"./types\";\n\ninterface EditorContextValue {\n  state: EditorState;\n  dispatch: Dispatch<EditorAction>;\n  actions: typeof editorActions;\n}\n\nconst EditorContext = createContext<EditorContextValue | null>(null);\n\nexport const useEditorContext = () => {\n  const context = useContext(EditorContext);\n  if (!context) {\n    throw new Error(\"useEditorContext must be used within EditorProvider\");\n  }\n  return context;\n};\n\ninterface EditorProviderProps extends PropsWithChildren {\n  initialEditorState?: EditorState;\n}\n\nexport const EditorProvider: FC<EditorProviderProps> = ({ children, initialEditorState }) => {\n  const [state, dispatch] = useReducer(editorReducer, initialEditorState || initialState);\n\n  const value = useMemo<EditorContextValue>(\n    () => ({\n      state,\n      dispatch,\n      actions: editorActions,\n    }),\n    [state],\n  );\n\n  return <EditorContext.Provider value={value}>{children}</EditorContext.Provider>;\n};\n"
  },
  {
    "path": "web/src/components/MemoEditor/state/index.ts",
    "content": "export * from \"./actions\";\nexport * from \"./context\";\nexport * from \"./reducer\";\nexport * from \"./types\";\n"
  },
  {
    "path": "web/src/components/MemoEditor/state/reducer.ts",
    "content": "import type { EditorAction, EditorState } from \"./types\";\nimport { initialState } from \"./types\";\n\nexport function editorReducer(state: EditorState, action: EditorAction): EditorState {\n  switch (action.type) {\n    case \"INIT_MEMO\":\n      return {\n        ...state,\n        content: action.payload.content,\n        metadata: action.payload.metadata,\n        timestamps: action.payload.timestamps,\n      };\n\n    case \"UPDATE_CONTENT\":\n      return {\n        ...state,\n        content: action.payload,\n      };\n\n    case \"SET_METADATA\":\n      return {\n        ...state,\n        metadata: {\n          ...state.metadata,\n          ...action.payload,\n        },\n      };\n\n    case \"ADD_ATTACHMENT\":\n      return {\n        ...state,\n        metadata: {\n          ...state.metadata,\n          attachments: [...state.metadata.attachments, action.payload],\n        },\n      };\n\n    case \"REMOVE_ATTACHMENT\":\n      return {\n        ...state,\n        metadata: {\n          ...state.metadata,\n          attachments: state.metadata.attachments.filter((a) => a.name !== action.payload),\n        },\n      };\n\n    case \"ADD_RELATION\":\n      return {\n        ...state,\n        metadata: {\n          ...state.metadata,\n          relations: [...state.metadata.relations, action.payload],\n        },\n      };\n\n    case \"REMOVE_RELATION\":\n      return {\n        ...state,\n        metadata: {\n          ...state.metadata,\n          relations: state.metadata.relations.filter((r) => r.relatedMemo?.name !== action.payload),\n        },\n      };\n\n    case \"ADD_LOCAL_FILE\":\n      return {\n        ...state,\n        localFiles: [...state.localFiles, action.payload],\n      };\n\n    case \"REMOVE_LOCAL_FILE\":\n      return {\n        ...state,\n        localFiles: state.localFiles.filter((f) => f.previewUrl !== action.payload),\n      };\n\n    case \"CLEAR_LOCAL_FILES\":\n      return {\n        ...state,\n        localFiles: [],\n      };\n\n    case \"TOGGLE_FOCUS_MODE\":\n      return {\n        ...state,\n        ui: {\n          ...state.ui,\n          isFocusMode: !state.ui.isFocusMode,\n        },\n      };\n\n    case \"SET_LOADING\":\n      return {\n        ...state,\n        ui: {\n          ...state.ui,\n          isLoading: {\n            ...state.ui.isLoading,\n            [action.payload.key]: action.payload.value,\n          },\n        },\n      };\n\n    case \"SET_DRAGGING\":\n      return {\n        ...state,\n        ui: {\n          ...state.ui,\n          isDragging: action.payload,\n        },\n      };\n\n    case \"SET_COMPOSING\":\n      return {\n        ...state,\n        ui: {\n          ...state.ui,\n          isComposing: action.payload,\n        },\n      };\n\n    case \"SET_TIMESTAMPS\":\n      return {\n        ...state,\n        timestamps: {\n          ...state.timestamps,\n          ...action.payload,\n        },\n      };\n\n    case \"RESET\":\n      return {\n        ...initialState,\n      };\n\n    default:\n      return state;\n  }\n}\n"
  },
  {
    "path": "web/src/components/MemoEditor/state/types.ts",
    "content": "import type { Attachment } from \"@/types/proto/api/v1/attachment_service_pb\";\nimport type { Location, MemoRelation } from \"@/types/proto/api/v1/memo_service_pb\";\nimport { Visibility } from \"@/types/proto/api/v1/memo_service_pb\";\nimport type { LocalFile } from \"../types/attachment\";\n\nexport type LoadingKey = \"saving\" | \"uploading\" | \"loading\";\n\nexport interface EditorState {\n  content: string;\n  metadata: {\n    visibility: Visibility;\n    attachments: Attachment[];\n    relations: MemoRelation[];\n    location?: Location;\n  };\n  ui: {\n    isFocusMode: boolean;\n    isLoading: {\n      saving: boolean;\n      uploading: boolean;\n      loading: boolean;\n    };\n    isDragging: boolean;\n    isComposing: boolean;\n  };\n  timestamps: {\n    createTime?: Date;\n    updateTime?: Date;\n  };\n  localFiles: LocalFile[];\n}\n\nexport type EditorAction =\n  | { type: \"INIT_MEMO\"; payload: { content: string; metadata: EditorState[\"metadata\"]; timestamps: EditorState[\"timestamps\"] } }\n  | { type: \"UPDATE_CONTENT\"; payload: string }\n  | { type: \"SET_METADATA\"; payload: Partial<EditorState[\"metadata\"]> }\n  | { type: \"ADD_ATTACHMENT\"; payload: Attachment }\n  | { type: \"REMOVE_ATTACHMENT\"; payload: string }\n  | { type: \"ADD_RELATION\"; payload: MemoRelation }\n  | { type: \"REMOVE_RELATION\"; payload: string }\n  | { type: \"ADD_LOCAL_FILE\"; payload: LocalFile }\n  | { type: \"REMOVE_LOCAL_FILE\"; payload: string }\n  | { type: \"CLEAR_LOCAL_FILES\" }\n  | { type: \"TOGGLE_FOCUS_MODE\" }\n  | { type: \"SET_LOADING\"; payload: { key: LoadingKey; value: boolean } }\n  | { type: \"SET_DRAGGING\"; payload: boolean }\n  | { type: \"SET_COMPOSING\"; payload: boolean }\n  | { type: \"SET_TIMESTAMPS\"; payload: Partial<EditorState[\"timestamps\"]> }\n  | { type: \"RESET\" };\n\nexport const initialState: EditorState = {\n  content: \"\",\n  metadata: {\n    visibility: Visibility.PRIVATE,\n    attachments: [],\n    relations: [],\n    location: undefined,\n  },\n  ui: {\n    isFocusMode: false,\n    isLoading: {\n      saving: false,\n      uploading: false,\n      loading: false,\n    },\n    isDragging: false,\n    isComposing: false,\n  },\n  timestamps: {\n    createTime: undefined,\n    updateTime: undefined,\n  },\n  localFiles: [],\n};\n"
  },
  {
    "path": "web/src/components/MemoEditor/types/attachment.ts",
    "content": "import type { Attachment } from \"@/types/proto/api/v1/attachment_service_pb\";\nimport { getAttachmentThumbnailUrl, getAttachmentType, getAttachmentUrl } from \"@/utils/attachment\";\n\nexport type FileCategory = \"image\" | \"video\" | \"document\";\n\n// Unified view model for rendering attachments and local files\nexport interface AttachmentItem {\n  readonly id: string;\n  readonly filename: string;\n  readonly category: FileCategory;\n  readonly mimeType: string;\n  readonly thumbnailUrl: string;\n  readonly sourceUrl: string;\n  readonly size?: number;\n  readonly isLocal: boolean;\n}\n\n// For MemoEditor: local files being uploaded\nexport interface LocalFile {\n  readonly file: File;\n  readonly previewUrl: string;\n}\n\nfunction categorizeFile(mimeType: string): FileCategory {\n  if (mimeType.startsWith(\"image/\")) return \"image\";\n  if (mimeType.startsWith(\"video/\")) return \"video\";\n  return \"document\";\n}\n\nexport function attachmentToItem(attachment: Attachment): AttachmentItem {\n  const attachmentType = getAttachmentType(attachment);\n  const sourceUrl = getAttachmentUrl(attachment);\n\n  return {\n    id: attachment.name,\n    filename: attachment.filename,\n    category: categorizeFile(attachment.type),\n    mimeType: attachment.type,\n    thumbnailUrl: attachmentType === \"image/*\" ? getAttachmentThumbnailUrl(attachment) : sourceUrl,\n    sourceUrl,\n    size: Number(attachment.size),\n    isLocal: false,\n  };\n}\n\nexport function fileToItem(file: File, blobUrl: string): AttachmentItem {\n  return {\n    id: blobUrl,\n    filename: file.name,\n    category: categorizeFile(file.type),\n    mimeType: file.type,\n    thumbnailUrl: blobUrl,\n    sourceUrl: blobUrl,\n    size: file.size,\n    isLocal: true,\n  };\n}\n\nexport function toAttachmentItems(attachments: Attachment[], localFiles: LocalFile[] = []): AttachmentItem[] {\n  return [...attachments.map(attachmentToItem), ...localFiles.map(({ file, previewUrl }) => fileToItem(file, previewUrl))];\n}\n\nexport function filterByCategory(items: AttachmentItem[], categories: FileCategory[]): AttachmentItem[] {\n  const categorySet = new Set(categories);\n  return items.filter((item) => categorySet.has(item.category));\n}\n\nexport function separateMediaAndDocs(items: AttachmentItem[]): { media: AttachmentItem[]; docs: AttachmentItem[] } {\n  const media: AttachmentItem[] = [];\n  const docs: AttachmentItem[] = [];\n\n  for (const item of items) {\n    if (item.category === \"image\" || item.category === \"video\") {\n      media.push(item);\n    } else {\n      docs.push(item);\n    }\n  }\n\n  return { media, docs };\n}\n"
  },
  {
    "path": "web/src/components/MemoEditor/types/components.ts",
    "content": "import type { LatLng } from \"leaflet\";\nimport type { Location, Memo, Visibility } from \"@/types/proto/api/v1/memo_service_pb\";\nimport type { EditorRefActions } from \"../Editor\";\nimport type { Command } from \"../Editor/commands\";\nimport type { LocationState } from \"./insert-menu\";\n\nexport interface MemoEditorProps {\n  className?: string;\n  cacheKey?: string;\n  placeholder?: string;\n  /** Existing memo to edit. When provided, the editor initializes from it without fetching. */\n  memo?: Memo;\n  parentMemoName?: string;\n  autoFocus?: boolean;\n  onConfirm?: (memoName: string) => void;\n  onCancel?: () => void;\n}\n\nexport interface EditorContentProps {\n  placeholder?: string;\n  autoFocus?: boolean;\n}\n\nexport interface EditorToolbarProps {\n  onSave: () => void;\n  onCancel?: () => void;\n  memoName?: string;\n}\n\nexport interface EditorMetadataProps {\n  memoName?: string;\n}\n\nexport interface FocusModeOverlayProps {\n  isActive: boolean;\n  onToggle: () => void;\n}\n\nexport interface FocusModeExitButtonProps {\n  isActive: boolean;\n  onToggle: () => void;\n  title: string;\n}\n\nexport interface LinkMemoDialogProps {\n  open: boolean;\n  onOpenChange: (open: boolean) => void;\n  searchText: string;\n  onSearchChange: (text: string) => void;\n  filteredMemos: Memo[];\n  isFetching: boolean;\n  onSelectMemo: (memo: Memo) => void;\n  isAlreadyLinked: (memoName: string) => boolean;\n}\n\nexport interface LocationDialogProps {\n  open: boolean;\n  onOpenChange: (open: boolean) => void;\n  state: LocationState;\n  locationInitialized: boolean;\n  onPositionChange: (position: LatLng) => void;\n  onUpdateCoordinate: (type: \"lat\" | \"lng\", value: string) => void;\n  onPlaceholderChange: (placeholder: string) => void;\n  onCancel: () => void;\n  onConfirm: () => void;\n}\n\nexport interface InsertMenuProps {\n  isUploading?: boolean;\n  location?: Location;\n  onLocationChange: (location?: Location) => void;\n  onToggleFocusMode?: () => void;\n  memoName?: string;\n}\n\nexport interface TagSuggestionsProps {\n  editorRef: React.RefObject<HTMLTextAreaElement>;\n  editorActions: React.ForwardedRef<EditorRefActions>;\n}\n\nexport interface SlashCommandsProps {\n  editorRef: React.RefObject<HTMLTextAreaElement>;\n  editorActions: React.ForwardedRef<EditorRefActions>;\n  commands: Command[];\n}\n\nexport interface EditorProps {\n  className: string;\n  initialContent: string;\n  placeholder: string;\n  onContentChange: (content: string) => void;\n  onPaste: (event: React.ClipboardEvent) => void;\n  isFocusMode?: boolean;\n  isInIME?: boolean;\n  onCompositionStart?: () => void;\n  onCompositionEnd?: () => void;\n}\n\nexport interface VisibilitySelectorProps {\n  value: Visibility;\n  onChange: (visibility: Visibility) => void;\n  onOpenChange?: (open: boolean) => void;\n}\n"
  },
  {
    "path": "web/src/components/MemoEditor/types/context.ts",
    "content": "import { createContext } from \"react\";\nimport type { Attachment } from \"@/types/proto/api/v1/attachment_service_pb\";\nimport type { MemoRelation } from \"@/types/proto/api/v1/memo_service_pb\";\nimport type { LocalFile } from \"./attachment\";\n\nexport interface MemoEditorContextValue {\n  attachmentList: Attachment[];\n  relationList: MemoRelation[];\n  setAttachmentList: (attachmentList: Attachment[]) => void;\n  setRelationList: (relationList: MemoRelation[]) => void;\n  memoName?: string;\n  addLocalFiles?: (files: LocalFile[]) => void;\n  removeLocalFile?: (previewUrl: string) => void;\n  localFiles?: LocalFile[];\n}\n\nconst defaultContextValue: MemoEditorContextValue = {\n  attachmentList: [],\n  relationList: [],\n  setAttachmentList: () => {},\n  setRelationList: () => {},\n  addLocalFiles: () => {},\n  removeLocalFile: () => {},\n  localFiles: [],\n};\n\nexport const MemoEditorContext = createContext<MemoEditorContextValue>(defaultContextValue);\n"
  },
  {
    "path": "web/src/components/MemoEditor/types/index.ts",
    "content": "// MemoEditor type exports\n\nexport type {\n  EditorContentProps,\n  EditorMetadataProps,\n  EditorProps,\n  EditorToolbarProps,\n  FocusModeExitButtonProps,\n  FocusModeOverlayProps,\n  InsertMenuProps,\n  LinkMemoDialogProps,\n  LocationDialogProps,\n  MemoEditorProps,\n  SlashCommandsProps,\n  TagSuggestionsProps,\n  VisibilitySelectorProps,\n} from \"./components\";\nexport { MemoEditorContext, type MemoEditorContextValue } from \"./context\";\nexport type { LocationState } from \"./insert-menu\";\n"
  },
  {
    "path": "web/src/components/MemoEditor/types/insert-menu.ts",
    "content": "import { LatLng } from \"leaflet\";\n\nexport interface LocationState {\n  placeholder: string;\n  position?: LatLng;\n  latInput: string;\n  lngInput: string;\n}\n"
  },
  {
    "path": "web/src/components/MemoExplorer/MemoExplorer.tsx",
    "content": "import SearchBar from \"@/components/SearchBar\";\nimport useCurrentUser from \"@/hooks/useCurrentUser\";\nimport { cn } from \"@/lib/utils\";\nimport type { StatisticsData } from \"@/types/statistics\";\nimport StatisticsView from \"../StatisticsView\";\nimport ShortcutsSection from \"./ShortcutsSection\";\nimport TagsSection from \"./TagsSection\";\n\nexport type MemoExplorerContext = \"home\" | \"explore\" | \"archived\" | \"profile\";\n\nexport interface MemoExplorerFeatures {\n  search?: boolean;\n  statistics?: boolean;\n  shortcuts?: boolean;\n  tags?: boolean;\n}\n\ninterface Props {\n  className?: string;\n  context?: MemoExplorerContext;\n  features?: MemoExplorerFeatures;\n  statisticsData: StatisticsData;\n  tagCount: Record<string, number>;\n}\n\nconst getDefaultFeatures = (context: MemoExplorerContext): MemoExplorerFeatures => {\n  switch (context) {\n    case \"explore\":\n      return {\n        search: true,\n        statistics: true,\n        shortcuts: false, // Global explore doesn't use shortcuts\n        tags: true,\n      };\n    case \"archived\":\n      return {\n        search: true,\n        statistics: true,\n        shortcuts: false, // Archived doesn't typically use shortcuts\n        tags: true,\n      };\n    case \"profile\":\n      return {\n        search: true,\n        statistics: true,\n        shortcuts: false, // Profile view doesn't use shortcuts\n        tags: true,\n      };\n    case \"home\":\n    default:\n      return {\n        search: true,\n        statistics: true,\n        shortcuts: true,\n        tags: true,\n      };\n  }\n};\n\nconst MemoExplorer = (props: Props) => {\n  const { className, context = \"home\", features: featureOverrides = {}, statisticsData, tagCount } = props;\n  const currentUser = useCurrentUser();\n\n  // Merge default features with overrides\n  const features = {\n    ...getDefaultFeatures(context),\n    ...featureOverrides,\n  };\n\n  return (\n    <aside\n      className={cn(\n        \"relative w-full h-full overflow-auto flex flex-col justify-start items-start bg-background text-sidebar-foreground\",\n        className,\n      )}\n    >\n      {features.search && <SearchBar />}\n      <div className=\"mt-1 px-1 w-full\">\n        {features.statistics && <StatisticsView statisticsData={statisticsData} />}\n        {features.shortcuts && currentUser && <ShortcutsSection />}\n        {features.tags && <TagsSection readonly={context === \"explore\"} tagCount={tagCount} />}\n      </div>\n    </aside>\n  );\n};\n\nexport default MemoExplorer;\n"
  },
  {
    "path": "web/src/components/MemoExplorer/MemoExplorerDrawer.tsx",
    "content": "import { MenuIcon } from \"lucide-react\";\nimport { useEffect, useState } from \"react\";\nimport { useLocation } from \"react-router-dom\";\nimport { Button } from \"@/components/ui/button\";\nimport { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from \"@/components/ui/sheet\";\nimport type { StatisticsData } from \"@/types/statistics\";\nimport MemoExplorer, { MemoExplorerContext, MemoExplorerFeatures } from \"./MemoExplorer\";\n\ninterface Props {\n  context?: MemoExplorerContext;\n  features?: MemoExplorerFeatures;\n  statisticsData: StatisticsData;\n  tagCount: Record<string, number>;\n}\n\nconst MemoExplorerDrawer = (props: Props) => {\n  const { context, features, statisticsData, tagCount } = props;\n  const location = useLocation();\n  const [open, setOpen] = useState(false);\n\n  useEffect(() => {\n    setOpen(false);\n  }, [location.pathname]);\n\n  return (\n    <Sheet open={open} onOpenChange={setOpen}>\n      <SheetTrigger asChild>\n        <Button variant=\"ghost\">\n          <MenuIcon className=\"size-5 text-foreground\" />\n        </Button>\n      </SheetTrigger>\n      <SheetContent side=\"right\" className=\"w-80 max-w-full bg-background\">\n        <SheetHeader>\n          <SheetTitle />\n        </SheetHeader>\n        <MemoExplorer className=\"px-4\" context={context} features={features} statisticsData={statisticsData} tagCount={tagCount} />\n      </SheetContent>\n    </Sheet>\n  );\n};\n\nexport default MemoExplorerDrawer;\n"
  },
  {
    "path": "web/src/components/MemoExplorer/ShortcutsSection.tsx",
    "content": "import { Edit3Icon, MoreVerticalIcon, PlusIcon, TrashIcon } from \"lucide-react\";\nimport { useEffect, useState } from \"react\";\nimport toast from \"react-hot-toast\";\nimport ConfirmDialog from \"@/components/ConfirmDialog\";\nimport { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from \"@/components/ui/tooltip\";\nimport { shortcutServiceClient } from \"@/connect\";\nimport { useAuth } from \"@/contexts/AuthContext\";\nimport { useMemoFilterContext } from \"@/contexts/MemoFilterContext\";\nimport { cn } from \"@/lib/utils\";\nimport { Shortcut } from \"@/types/proto/api/v1/shortcut_service_pb\";\nimport { useTranslate } from \"@/utils/i18n\";\nimport CreateShortcutDialog from \"../CreateShortcutDialog\";\nimport { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from \"../ui/dropdown-menu\";\n\nconst emojiRegex = /^(\\p{Emoji_Presentation}|\\p{Emoji}\\uFE0F)$/u;\n\n// Helper function to extract shortcut ID from resource name\n// Format: users/{user}/shortcuts/{shortcut}\nconst getShortcutId = (name: string): string => {\n  const parts = name.split(\"/\");\n  return parts.length === 4 ? parts[3] : \"\";\n};\n\nfunction ShortcutsSection() {\n  const t = useTranslate();\n  const { shortcuts, refetchSettings } = useAuth();\n  const { shortcut: selectedShortcut, setShortcut } = useMemoFilterContext();\n  const [isCreateShortcutDialogOpen, setIsCreateShortcutDialogOpen] = useState(false);\n  const [deleteTarget, setDeleteTarget] = useState<Shortcut | undefined>();\n  const [editingShortcut, setEditingShortcut] = useState<Shortcut | undefined>();\n\n  useEffect(() => {\n    refetchSettings();\n  }, [refetchSettings]);\n\n  const handleDeleteShortcut = async (shortcut: Shortcut) => {\n    setDeleteTarget(shortcut);\n  };\n\n  const confirmDeleteShortcut = async () => {\n    if (!deleteTarget) return;\n    await shortcutServiceClient.deleteShortcut({ name: deleteTarget.name });\n    await refetchSettings();\n    toast.success(t(\"setting.shortcut.delete-success\", { title: deleteTarget.title }));\n    setDeleteTarget(undefined);\n  };\n\n  const handleCreateShortcut = () => {\n    setEditingShortcut(undefined);\n    setIsCreateShortcutDialogOpen(true);\n  };\n\n  const handleEditShortcut = (shortcut: Shortcut) => {\n    setEditingShortcut(shortcut);\n    setIsCreateShortcutDialogOpen(true);\n  };\n\n  const handleShortcutDialogSuccess = () => {\n    setIsCreateShortcutDialogOpen(false);\n    setEditingShortcut(undefined);\n  };\n\n  return (\n    <div className=\"w-full flex flex-col justify-start items-start mt-3 px-1 h-auto shrink-0 flex-nowrap\">\n      <div className=\"flex flex-row justify-between items-center w-full gap-1 mb-1 text-sm leading-6 text-muted-foreground select-none\">\n        <span>{t(\"common.shortcuts\")}</span>\n        <TooltipProvider>\n          <Tooltip>\n            <TooltipTrigger asChild>\n              <PlusIcon className=\"w-4 h-auto cursor-pointer\" onClick={handleCreateShortcut} />\n            </TooltipTrigger>\n            <TooltipContent>\n              <p>{t(\"common.create\")}</p>\n            </TooltipContent>\n          </Tooltip>\n        </TooltipProvider>\n      </div>\n      <div className=\"w-full flex flex-row justify-start items-center relative flex-wrap gap-x-2 gap-y-1\">\n        {shortcuts.map((shortcut) => {\n          const shortcutId = getShortcutId(shortcut.name);\n          const maybeEmoji = shortcut.title.split(\" \")[0];\n          const emoji = emojiRegex.test(maybeEmoji) ? maybeEmoji : undefined;\n          const title = emoji ? shortcut.title.replace(emoji, \"\") : shortcut.title;\n          const selected = selectedShortcut === shortcutId;\n          return (\n            <div\n              key={shortcutId}\n              className=\"shrink-0 w-full text-sm rounded-md leading-6 flex flex-row justify-between items-center select-none gap-2 text-muted-foreground\"\n            >\n              <span\n                className={cn(\"truncate cursor-pointer text-muted-foreground\", selected && \"text-primary font-medium\")}\n                onClick={() => (selected ? setShortcut(undefined) : setShortcut(shortcutId))}\n              >\n                {emoji && <span className=\"text-base mr-1\">{emoji}</span>}\n                {title.trim()}\n              </span>\n              <DropdownMenu>\n                <DropdownMenuTrigger asChild>\n                  <MoreVerticalIcon className=\"w-4 h-auto shrink-0 text-muted-foreground cursor-pointer hover:text-foreground\" />\n                </DropdownMenuTrigger>\n                <DropdownMenuContent align=\"end\" alignOffset={-12}>\n                  <DropdownMenuItem onClick={() => handleEditShortcut(shortcut)}>\n                    <Edit3Icon className=\"w-4 h-auto\" />\n                    {t(\"common.edit\")}\n                  </DropdownMenuItem>\n                  <DropdownMenuItem onClick={() => handleDeleteShortcut(shortcut)}>\n                    <TrashIcon className=\"w-4 h-auto\" />\n                    {t(\"common.delete\")}\n                  </DropdownMenuItem>\n                </DropdownMenuContent>\n              </DropdownMenu>\n            </div>\n          );\n        })}\n      </div>\n      <CreateShortcutDialog\n        open={isCreateShortcutDialogOpen}\n        onOpenChange={setIsCreateShortcutDialogOpen}\n        shortcut={editingShortcut}\n        onSuccess={handleShortcutDialogSuccess}\n      />\n      <ConfirmDialog\n        open={!!deleteTarget}\n        onOpenChange={(open) => !open && setDeleteTarget(undefined)}\n        title={t(\"setting.shortcut.delete-confirm\", { title: deleteTarget?.title ?? \"\" })}\n        confirmLabel={t(\"common.delete\")}\n        cancelLabel={t(\"common.cancel\")}\n        onConfirm={confirmDeleteShortcut}\n        confirmVariant=\"destructive\"\n      />\n    </div>\n  );\n}\n\nexport default ShortcutsSection;\n"
  },
  {
    "path": "web/src/components/MemoExplorer/TagsSection.tsx",
    "content": "import { HashIcon, MoreVerticalIcon, TagsIcon } from \"lucide-react\";\nimport useLocalStorage from \"react-use/lib/useLocalStorage\";\nimport { Switch } from \"@/components/ui/switch\";\nimport { type MemoFilter, useMemoFilterContext } from \"@/contexts/MemoFilterContext\";\nimport { cn } from \"@/lib/utils\";\nimport { useTranslate } from \"@/utils/i18n\";\nimport TagTree from \"../TagTree\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"../ui/popover\";\n\ninterface Props {\n  readonly?: boolean;\n  tagCount: Record<string, number>;\n}\n\nconst TagsSection = (props: Props) => {\n  const t = useTranslate();\n  const { getFiltersByFactor, addFilter, removeFilter } = useMemoFilterContext();\n  const [treeMode, setTreeMode] = useLocalStorage<boolean>(\"tag-view-as-tree\", false);\n  const [treeAutoExpand, setTreeAutoExpand] = useLocalStorage<boolean>(\"tag-tree-auto-expand\", false);\n\n  const tags = Object.entries(props.tagCount)\n    .sort((a, b) => a[0].localeCompare(b[0]))\n    .sort((a, b) => b[1] - a[1]);\n\n  const handleTagClick = (tag: string) => {\n    const isActive = getFiltersByFactor(\"tagSearch\").some((filter: MemoFilter) => filter.value === tag);\n    if (isActive) {\n      removeFilter((f: MemoFilter) => f.factor === \"tagSearch\" && f.value === tag);\n    } else {\n      // Remove all existing tag filters first, then add the new one\n      removeFilter((f: MemoFilter) => f.factor === \"tagSearch\");\n      addFilter({\n        factor: \"tagSearch\",\n        value: tag,\n      });\n    }\n  };\n\n  return (\n    <div className=\"w-full flex flex-col justify-start items-start mt-3 px-1 h-auto shrink-0 flex-nowrap\">\n      <div className=\"flex flex-row justify-between items-center w-full gap-1 mb-1 text-sm leading-6 text-muted-foreground select-none\">\n        <span>{t(\"common.tags\")}</span>\n        {tags.length > 0 && (\n          <Popover>\n            <PopoverTrigger>\n              <MoreVerticalIcon className=\"w-4 h-auto shrink-0 text-muted-foreground cursor-pointer hover:text-foreground\" />\n            </PopoverTrigger>\n            <PopoverContent align=\"end\" alignOffset={-12}>\n              <div className=\"w-auto flex flex-row justify-between items-center gap-2 p-1\">\n                <span className=\"text-sm shrink-0\">{t(\"common.tree-mode\")}</span>\n                <Switch checked={treeMode} onCheckedChange={(checked) => setTreeMode(checked)} />\n              </div>\n              <div className=\"w-auto flex flex-row justify-between items-center gap-2 p-1\">\n                <span className=\"text-sm shrink-0\">{t(\"common.auto-expand\")}</span>\n                <Switch disabled={!treeMode} checked={treeAutoExpand} onCheckedChange={(checked) => setTreeAutoExpand(checked)} />\n              </div>\n            </PopoverContent>\n          </Popover>\n        )}\n      </div>\n      {tags.length > 0 ? (\n        treeMode ? (\n          <TagTree tagAmounts={tags} expandSubTags={!!treeAutoExpand} />\n        ) : (\n          <div className=\"w-full flex flex-row justify-start items-center relative flex-wrap gap-x-2 gap-y-1.5\">\n            {tags.map(([tag, amount]) => {\n              const isActive = getFiltersByFactor(\"tagSearch\").some((filter: MemoFilter) => filter.value === tag);\n              return (\n                <div\n                  key={tag}\n                  className={cn(\n                    \"shrink-0 w-auto max-w-full text-sm rounded-md leading-6 flex flex-row justify-start items-center select-none cursor-pointer transition-colors\",\n                    \"hover:opacity-80\",\n                    isActive ? \"text-primary\" : \"text-muted-foreground\",\n                  )}\n                  onClick={() => handleTagClick(tag)}\n                >\n                  <HashIcon className=\"w-4 h-auto shrink-0\" />\n                  <div className=\"inline-flex flex-nowrap ml-0.5 gap-0.5 max-w-[calc(100%-16px)]\">\n                    <span className={cn(\"truncate\", isActive ? \"font-medium\" : \"\")}>{tag}</span>\n                    {amount > 1 && <span className=\"opacity-60 shrink-0\">({amount})</span>}\n                  </div>\n                </div>\n              );\n            })}\n          </div>\n        )\n      ) : (\n        !props.readonly && (\n          <div className=\"p-2 border border-dashed rounded-md flex flex-row justify-start items-start gap-2 text-muted-foreground\">\n            <TagsIcon className=\"w-5 h-5 shrink-0\" />\n            <p className=\"text-sm leading-snug italic\">{t(\"tag.create-tags-guide\")}</p>\n          </div>\n        )\n      )}\n    </div>\n  );\n};\n\nexport default TagsSection;\n"
  },
  {
    "path": "web/src/components/MemoExplorer/index.ts",
    "content": "import MemoExplorer from \"./MemoExplorer\";\nimport MemoExplorerDrawer from \"./MemoExplorerDrawer\";\n\nexport type { MemoExplorerContext, MemoExplorerFeatures } from \"./MemoExplorer\";\nexport { MemoExplorer, MemoExplorerDrawer };\n"
  },
  {
    "path": "web/src/components/MemoFilters.tsx",
    "content": "import { isEqual } from \"lodash-es\";\nimport {\n  BookmarkIcon,\n  CalendarIcon,\n  CheckCircleIcon,\n  CodeIcon,\n  EyeIcon,\n  HashIcon,\n  LinkIcon,\n  LucideIcon,\n  SearchIcon,\n  XIcon,\n} from \"lucide-react\";\nimport { FilterFactor, getMemoFilterKey, MemoFilter, useMemoFilterContext } from \"@/contexts/MemoFilterContext\";\nimport { useTranslate } from \"@/utils/i18n\";\n\ninterface FilterConfig {\n  icon: LucideIcon;\n  getLabel: (value: string, t: ReturnType<typeof useTranslate>) => string;\n}\n\nconst FILTER_CONFIGS: Record<FilterFactor, FilterConfig> = {\n  tagSearch: {\n    icon: HashIcon,\n    getLabel: (value) => value,\n  },\n  visibility: {\n    icon: EyeIcon,\n    getLabel: (value) => value,\n  },\n  contentSearch: {\n    icon: SearchIcon,\n    getLabel: (value) => value,\n  },\n  displayTime: {\n    icon: CalendarIcon,\n    getLabel: (value) => value,\n  },\n  pinned: {\n    icon: BookmarkIcon,\n    getLabel: (value) => value,\n  },\n  \"property.hasLink\": {\n    icon: LinkIcon,\n    getLabel: (_, t) => t(\"memo.filters.has-link\"),\n  },\n  \"property.hasTaskList\": {\n    icon: CheckCircleIcon,\n    getLabel: (_, t) => t(\"memo.filters.has-task-list\"),\n  },\n  \"property.hasCode\": {\n    icon: CodeIcon,\n    getLabel: (_, t) => t(\"memo.filters.has-code\"),\n  },\n};\n\nconst MemoFilters = () => {\n  const t = useTranslate();\n  const { filters, removeFilter } = useMemoFilterContext();\n\n  const handleRemoveFilter = (filter: MemoFilter) => {\n    removeFilter((f: MemoFilter) => isEqual(f, filter));\n  };\n\n  const getFilterDisplayText = (filter: MemoFilter): string => {\n    const config = FILTER_CONFIGS[filter.factor];\n    if (!config) {\n      return filter.value || filter.factor;\n    }\n    return config.getLabel(filter.value, t);\n  };\n\n  if (filters.length === 0) {\n    return null;\n  }\n\n  return (\n    <div className=\"w-full mb-2 flex flex-row justify-start items-center flex-wrap gap-2\">\n      {filters.map((filter) => {\n        const config = FILTER_CONFIGS[filter.factor];\n        const Icon = config?.icon;\n\n        return (\n          <div\n            key={getMemoFilterKey(filter)}\n            className=\"group inline-flex items-center gap-1.5 h-7 px-2.5 bg-accent/50 hover:bg-accent border border-border/50 rounded-full text-sm transition-all duration-200 hover:shadow-sm\"\n          >\n            {Icon && <Icon className=\"w-3.5 h-3.5 text-muted-foreground shrink-0\" />}\n            <span className=\"text-foreground/80 font-medium max-w-32 truncate\">{getFilterDisplayText(filter)}</span>\n            <button\n              onClick={() => handleRemoveFilter(filter)}\n              className=\"ml-0.5 -mr-1 p-0.5 text-muted-foreground/60 hover:text-destructive hover:bg-destructive/10 rounded-full transition-colors\"\n              aria-label=\"Remove filter\"\n            >\n              <XIcon className=\"w-3 h-3\" />\n            </button>\n          </div>\n        );\n      })}\n    </div>\n  );\n};\n\nMemoFilters.displayName = \"MemoFilters\";\n\nexport default MemoFilters;\n"
  },
  {
    "path": "web/src/components/MemoPreview/MemoPreview.tsx",
    "content": "import { create } from \"@bufbuild/protobuf\";\nimport { FileIcon } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\nimport type { Attachment } from \"@/types/proto/api/v1/attachment_service_pb\";\nimport { MemoSchema } from \"@/types/proto/api/v1/memo_service_pb\";\nimport { getAttachmentType, getAttachmentUrl } from \"@/utils/attachment\";\nimport MemoContent from \"../MemoContent\";\nimport { MemoViewContext, type MemoViewContextValue } from \"../MemoView/MemoViewContext\";\n\ninterface MemoPreviewProps {\n  content: string;\n  attachments: Attachment[];\n  compact?: boolean;\n  className?: string;\n}\n\nconst STUB_CONTEXT: MemoViewContextValue = {\n  memo: create(MemoSchema),\n  creator: undefined,\n  currentUser: undefined,\n  parentPage: \"/\",\n  isArchived: false,\n  readonly: true,\n  showNSFWContent: false,\n  nsfw: false,\n};\n\nconst AttachmentThumbnails = ({ attachments }: { attachments: Attachment[] }) => {\n  const images: Attachment[] = [];\n  const others: Attachment[] = [];\n  for (const a of attachments) {\n    if (getAttachmentType(a) === \"image/*\") images.push(a);\n    else others.push(a);\n  }\n\n  return (\n    <div className=\"flex items-center gap-1.5 flex-wrap\">\n      {images.map((a) => (\n        <img\n          key={a.name}\n          src={getAttachmentUrl(a)}\n          alt={a.filename}\n          className=\"w-10 h-10 rounded border border-border object-cover bg-muted/40\"\n          loading=\"lazy\"\n        />\n      ))}\n      {others.map((a) => (\n        <div key={a.name} className=\"flex items-center gap-1 text-[10px] text-muted-foreground\">\n          <FileIcon className=\"w-3 h-3 shrink-0\" />\n          <span className=\"truncate max-w-[80px]\">{a.filename}</span>\n        </div>\n      ))}\n    </div>\n  );\n};\n\nconst MemoPreview = ({ content, attachments, compact = true, className }: MemoPreviewProps) => {\n  const hasContent = content.trim().length > 0;\n  const hasAttachments = attachments.length > 0;\n\n  if (!hasContent && !hasAttachments) {\n    return null;\n  }\n\n  return (\n    <MemoViewContext.Provider value={STUB_CONTEXT}>\n      <div className={cn(\"flex flex-col gap-1 pointer-events-none\", className)}>\n        {hasContent && <MemoContent content={content} compact={compact} />}\n        {hasAttachments && <AttachmentThumbnails attachments={attachments} />}\n      </div>\n    </MemoViewContext.Provider>\n  );\n};\n\nexport default MemoPreview;\n"
  },
  {
    "path": "web/src/components/MemoPreview/index.ts",
    "content": "export { default as MemoPreview } from \"./MemoPreview\";\n"
  },
  {
    "path": "web/src/components/MemoReactionListView/MemoReactionListView.tsx",
    "content": "import { memo } from \"react\";\nimport useCurrentUser from \"@/hooks/useCurrentUser\";\nimport { State } from \"@/types/proto/api/v1/common_pb\";\nimport type { Memo, Reaction } from \"@/types/proto/api/v1/memo_service_pb\";\nimport { useReactionGroups } from \"./hooks\";\nimport ReactionSelector from \"./ReactionSelector\";\nimport ReactionView from \"./ReactionView\";\n\ninterface Props {\n  memo: Memo;\n  reactions: Reaction[];\n}\n\nconst MemoReactionListView = (props: Props) => {\n  const { memo: memoData, reactions } = props;\n  const currentUser = useCurrentUser();\n  const reactionGroup = useReactionGroups(reactions);\n  const readonly = memoData.state === State.ARCHIVED;\n\n  if (reactions.length === 0) {\n    return null;\n  }\n\n  return (\n    <div className=\"w-full flex flex-row justify-start items-start flex-wrap gap-1 select-none\">\n      {Array.from(reactionGroup).map(([reactionType, users]) => (\n        <ReactionView key={`${reactionType.toString()} ${users.length}`} memo={memoData} reactionType={reactionType} users={users} />\n      ))}\n      {!readonly && currentUser && <ReactionSelector memo={memoData} />}\n    </div>\n  );\n};\n\nexport default memo(MemoReactionListView);\n"
  },
  {
    "path": "web/src/components/MemoReactionListView/ReactionSelector.tsx",
    "content": "import { SmilePlusIcon } from \"lucide-react\";\nimport { useState } from \"react\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"@/components/ui/popover\";\nimport { useInstance } from \"@/contexts/InstanceContext\";\nimport { cn } from \"@/lib/utils\";\nimport type { Memo } from \"@/types/proto/api/v1/memo_service_pb\";\nimport { useReactionActions } from \"./hooks\";\n\ninterface Props {\n  memo: Memo;\n  className?: string;\n  onOpenChange?: (open: boolean) => void;\n}\n\nconst ReactionSelector = (props: Props) => {\n  const { memo, className, onOpenChange } = props;\n  const [open, setOpen] = useState(false);\n  const { memoRelatedSetting } = useInstance();\n\n  const handleOpenChange = (newOpen: boolean) => {\n    setOpen(newOpen);\n    onOpenChange?.(newOpen);\n  };\n\n  const { hasReacted, handleReactionClick } = useReactionActions({\n    memo,\n    onComplete: () => handleOpenChange(false),\n  });\n\n  return (\n    <Popover open={open} onOpenChange={handleOpenChange}>\n      <PopoverTrigger asChild>\n        <span\n          className={cn(\n            \"h-7 w-7 flex justify-center items-center rounded-full border cursor-pointer transition-all hover:opacity-80\",\n            className,\n          )}\n        >\n          <SmilePlusIcon className=\"w-4 h-4 mx-auto text-muted-foreground\" />\n        </span>\n      </PopoverTrigger>\n      <PopoverContent align=\"center\" className=\"max-w-[90vw] sm:max-w-md\">\n        <div className=\"grid grid-cols-4 sm:grid-cols-6 md:grid-cols-8 gap-1 max-h-64 overflow-y-auto\">\n          {memoRelatedSetting.reactions.map((reactionType) => (\n            <button\n              type=\"button\"\n              key={reactionType}\n              className={cn(\n                \"inline-flex w-auto text-base cursor-pointer rounded px-1 text-muted-foreground hover:opacity-80 transition-colors\",\n                hasReacted(reactionType) && \"bg-secondary text-secondary-foreground\",\n              )}\n              onClick={() => handleReactionClick(reactionType)}\n            >\n              {reactionType}\n            </button>\n          ))}\n        </div>\n      </PopoverContent>\n    </Popover>\n  );\n};\n\nexport default ReactionSelector;\n"
  },
  {
    "path": "web/src/components/MemoReactionListView/ReactionView.tsx",
    "content": "import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from \"@/components/ui/tooltip\";\nimport useCurrentUser from \"@/hooks/useCurrentUser\";\nimport { cn } from \"@/lib/utils\";\nimport { State } from \"@/types/proto/api/v1/common_pb\";\nimport type { Memo } from \"@/types/proto/api/v1/memo_service_pb\";\nimport type { User } from \"@/types/proto/api/v1/user_service_pb\";\nimport { formatReactionTooltip, useReactionActions } from \"./hooks\";\n\ninterface Props {\n  memo: Memo;\n  reactionType: string;\n  users: User[];\n}\n\nconst ReactionView = (props: Props) => {\n  const { memo, reactionType, users } = props;\n  const currentUser = useCurrentUser();\n  const hasReaction = users.some((user) => currentUser && user.username === currentUser.username);\n  const readonly = memo.state === State.ARCHIVED;\n\n  const { handleReactionClick } = useReactionActions({ memo });\n\n  const handleClick = () => {\n    if (!currentUser || readonly) return;\n    handleReactionClick(reactionType);\n  };\n\n  const isClickable = currentUser && !readonly;\n\n  return (\n    <TooltipProvider>\n      <Tooltip>\n        <TooltipTrigger asChild>\n          <button\n            type=\"button\"\n            className={cn(\n              \"h-7 border px-2 py-0.5 rounded-full flex flex-row justify-center items-center gap-1\",\n              \"text-sm text-muted-foreground\",\n              isClickable && \"cursor-pointer\",\n              !isClickable && \"cursor-default\",\n              hasReaction && \"bg-accent border-border\",\n            )}\n            onClick={handleClick}\n            disabled={!isClickable}\n          >\n            <span>{reactionType}</span>\n            <span className=\"opacity-60\">{users.length}</span>\n          </button>\n        </TooltipTrigger>\n        <TooltipContent>\n          <p>{formatReactionTooltip(users, reactionType)}</p>\n        </TooltipContent>\n      </Tooltip>\n    </TooltipProvider>\n  );\n};\n\nexport default ReactionView;\n"
  },
  {
    "path": "web/src/components/MemoReactionListView/hooks.ts",
    "content": "import { useQueryClient } from \"@tanstack/react-query\";\nimport { useMemo } from \"react\";\nimport { memoServiceClient } from \"@/connect\";\nimport useCurrentUser from \"@/hooks/useCurrentUser\";\nimport { memoKeys } from \"@/hooks/useMemoQueries\";\nimport { useUsersByNames } from \"@/hooks/useUserQueries\";\nimport type { Memo, Reaction } from \"@/types/proto/api/v1/memo_service_pb\";\nimport type { User } from \"@/types/proto/api/v1/user_service_pb\";\n\nexport type ReactionGroup = Map<string, User[]>;\n\nexport const useReactionGroups = (reactions: Reaction[]): ReactionGroup => {\n  const creatorNames = useMemo(() => reactions.map((r) => r.creator), [reactions]);\n  const { data: userMap } = useUsersByNames(creatorNames);\n\n  return useMemo(() => {\n    const reactionGroup = new Map<string, User[]>();\n    for (const reaction of reactions) {\n      const user = userMap?.get(reaction.creator);\n      if (!user) continue;\n\n      const users = reactionGroup.get(reaction.reactionType) || [];\n      users.push(user);\n      reactionGroup.set(reaction.reactionType, users);\n    }\n    return reactionGroup;\n  }, [reactions, userMap]);\n};\n\ninterface UseReactionActionsOptions {\n  memo: Memo;\n  onComplete?: () => void;\n}\n\nexport const useReactionActions = ({ memo, onComplete }: UseReactionActionsOptions) => {\n  const currentUser = useCurrentUser();\n  const queryClient = useQueryClient();\n\n  const hasReacted = (reactionType: string) => {\n    return memo.reactions.some((r) => r.reactionType === reactionType && r.creator === currentUser?.name);\n  };\n\n  const handleReactionClick = async (reactionType: string) => {\n    if (!currentUser) return;\n\n    try {\n      if (hasReacted(reactionType)) {\n        const reactions = memo.reactions.filter(\n          (reaction) => reaction.reactionType === reactionType && reaction.creator === currentUser.name,\n        );\n        for (const reaction of reactions) {\n          await memoServiceClient.deleteMemoReaction({ name: reaction.name });\n        }\n      } else {\n        await memoServiceClient.upsertMemoReaction({\n          name: memo.name,\n          reaction: { contentId: memo.name, reactionType },\n        });\n      }\n      // Refetch the memo to get updated reactions and invalidate cache\n      const updatedMemo = await memoServiceClient.getMemo({ name: memo.name });\n      queryClient.setQueryData(memoKeys.detail(memo.name), updatedMemo);\n      queryClient.invalidateQueries({ queryKey: memoKeys.lists() });\n      // If this memo is a comment, refresh the parent's comments list so the comment's reactions update in the UI\n      if (memo.parent) {\n        queryClient.invalidateQueries({ queryKey: memoKeys.comments(memo.parent) });\n      }\n    } catch {\n      // skip error\n    }\n    onComplete?.();\n  };\n\n  return { hasReacted, handleReactionClick };\n};\n\nexport const formatReactionTooltip = (users: User[], reactionType: string): string => {\n  if (users.length === 0) return \"\";\n  const formatUserName = (user: User) => user.displayName || user.username;\n  if (users.length < 5) {\n    return `${users.map(formatUserName).join(\", \")} reacted with ${reactionType.toLowerCase()}`;\n  }\n  return `${users.slice(0, 4).map(formatUserName).join(\", \")} and ${users.length - 4} more reacted with ${reactionType.toLowerCase()}`;\n};\n"
  },
  {
    "path": "web/src/components/MemoReactionListView/index.ts",
    "content": "export { default, default as MemoReactionListView } from \"./MemoReactionListView\";\nexport { default as ReactionSelector } from \"./ReactionSelector\";\nexport { default as ReactionView } from \"./ReactionView\";\n"
  },
  {
    "path": "web/src/components/MemoRelationForceGraph/MemoRelationForceGraph.tsx",
    "content": "import { useEffect, useRef, useState } from \"react\";\nimport ForceGraph2D, { ForceGraphMethods, LinkObject, NodeObject } from \"react-force-graph-2d\";\nimport { extractMemoIdFromName } from \"@/helpers/resource-names\";\nimport useNavigateTo from \"@/hooks/useNavigateTo\";\nimport { cn } from \"@/lib/utils\";\nimport { Memo, MemoRelation_Type } from \"@/types/proto/api/v1/memo_service_pb\";\nimport { LinkType, NodeType } from \"./types\";\nimport { convertMemoRelationsToGraphData } from \"./utils\";\n\ninterface Props {\n  memo: Memo;\n  className?: string;\n  parentPage?: string;\n}\n\nconst MAIN_NODE_COLOR = \"#14b8a6\";\nconst DEFAULT_NODE_COLOR = \"#a1a1aa\";\n\nconst MemoRelationForceGraph = ({ className, memo, parentPage }: Props) => {\n  const navigateTo = useNavigateTo();\n  const [mode] = useState<\"light\">(\"light\");\n  const containerRef = useRef<HTMLDivElement>(null);\n  const graphRef = useRef<ForceGraphMethods<NodeObject<NodeType>, LinkObject<NodeType, LinkType>> | undefined>(undefined);\n  const [graphSize, setGraphSize] = useState({ width: 0, height: 0 });\n\n  useEffect(() => {\n    if (!containerRef.current) return;\n    setGraphSize(containerRef.current.getBoundingClientRect());\n  }, []);\n\n  const onNodeClick = (node: NodeObject<NodeType>) => {\n    if (node.memo.name === memo.name) return;\n    navigateTo(`/${memo.name}`, {\n      state: {\n        from: parentPage,\n      },\n    });\n  };\n\n  return (\n    <div ref={containerRef} className={cn(\"opacity-80\", className)}>\n      <ForceGraph2D\n        ref={graphRef}\n        width={graphSize.width}\n        height={graphSize.height}\n        enableZoomInteraction\n        cooldownTicks={0}\n        nodeColor={(node) => (node.id === memo.name ? MAIN_NODE_COLOR : DEFAULT_NODE_COLOR)}\n        nodeRelSize={3}\n        nodeLabel={(node) => extractMemoIdFromName(node.memo.name).slice(0, 6).toLowerCase()}\n        linkColor={() => (mode === \"light\" ? \"#e4e4e7\" : \"#3f3f46\")}\n        graphData={convertMemoRelationsToGraphData(memo.relations.filter((r) => r.type === MemoRelation_Type.REFERENCE))}\n        onNodeClick={onNodeClick}\n        linkDirectionalArrowLength={3}\n        linkDirectionalArrowRelPos={1}\n        linkCurvature={0.25}\n      />\n    </div>\n  );\n};\n\nexport default MemoRelationForceGraph;\n"
  },
  {
    "path": "web/src/components/MemoRelationForceGraph/index.ts",
    "content": "import MemoRelationForceGraph from \"./MemoRelationForceGraph\";\n\nexport * from \"./utils\";\n\nexport default MemoRelationForceGraph;\n"
  },
  {
    "path": "web/src/components/MemoRelationForceGraph/types.ts",
    "content": "import { MemoRelation_Memo } from \"@/types/proto/api/v1/memo_service_pb\";\n\nexport interface NodeType {\n  memo: MemoRelation_Memo;\n}\n\n// eslint-disable-next-line @typescript-eslint/no-empty-object-type\nexport interface LinkType {\n  // ...add more additional properties relevant to the link here.\n}\n"
  },
  {
    "path": "web/src/components/MemoRelationForceGraph/utils.ts",
    "content": "import { GraphData, LinkObject, NodeObject } from \"react-force-graph-2d\";\nimport { MemoRelation, MemoRelation_Memo } from \"@/types/proto/api/v1/memo_service_pb\";\nimport { LinkType, NodeType } from \"./types\";\n\nexport const convertMemoRelationsToGraphData = (memoRelations: MemoRelation[]): GraphData<NodeType, LinkType> => {\n  const nodesMap = new Map<string, NodeObject<NodeType>>();\n  const links: LinkObject<NodeType, LinkType>[] = [];\n\n  // Iterate through memoRelations to populate nodes and links.\n  memoRelations.forEach((relation) => {\n    const memo = relation.memo as MemoRelation_Memo;\n    const relatedMemo = relation.relatedMemo as MemoRelation_Memo;\n\n    // Add memo node if not already present.\n    if (!nodesMap.has(memo.name)) {\n      nodesMap.set(memo.name, { id: memo.name, memo });\n    }\n\n    // Add related_memo node if not already present.\n    if (!nodesMap.has(relatedMemo.name)) {\n      nodesMap.set(relatedMemo.name, { id: relatedMemo.name, memo: relatedMemo });\n    }\n\n    // Create link between memo and relatedMemo.\n    links.push({\n      source: memo.name,\n      target: relatedMemo.name,\n      type: relation.type, // Include the type of relation as a property of the link.\n    });\n  });\n\n  return {\n    nodes: Array.from(nodesMap.values()),\n    links,\n  };\n};\n"
  },
  {
    "path": "web/src/components/MemoResource.tsx",
    "content": ""
  },
  {
    "path": "web/src/components/MemoSharePanel.tsx",
    "content": "import { timestampDate } from \"@bufbuild/protobuf/wkt\";\nimport { ConnectError } from \"@connectrpc/connect\";\nimport { CheckIcon, CopyIcon, LinkIcon, Loader2Icon, Trash2Icon } from \"lucide-react\";\nimport { useState } from \"react\";\nimport { toast } from \"react-hot-toast\";\nimport { Button } from \"@/components/ui/button\";\nimport { Dialog, DialogContent, DialogHeader, DialogTitle } from \"@/components/ui/dialog\";\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from \"@/components/ui/select\";\nimport { getShareUrl, useCreateMemoShare, useDeleteMemoShare, useMemoShares } from \"@/hooks/useMemoShareQueries\";\nimport type { MemoShare } from \"@/types/proto/api/v1/memo_service_pb\";\nimport { useTranslate } from \"@/utils/i18n\";\n\ntype ExpiryOption = \"never\" | \"1d\" | \"7d\" | \"30d\";\n\nfunction getExpireDate(option: ExpiryOption): Date | undefined {\n  if (option === \"never\") return undefined;\n  const d = new Date();\n  if (option === \"1d\") d.setDate(d.getDate() + 1);\n  else if (option === \"7d\") d.setDate(d.getDate() + 7);\n  else if (option === \"30d\") d.setDate(d.getDate() + 30);\n  return d;\n}\n\nfunction formatExpiry(share: MemoShare, t: ReturnType<typeof useTranslate>): string {\n  if (!share.expireTime) return t(\"memo.share.never-expires\");\n  const d = timestampDate(share.expireTime);\n  return t(\"memo.share.expires-on\", { date: d.toLocaleDateString() });\n}\n\ninterface ShareLinkRowProps {\n  share: MemoShare;\n  memoName: string;\n}\n\nfunction ShareLinkRow({ share, memoName }: ShareLinkRowProps) {\n  const t = useTranslate();\n  const [copied, setCopied] = useState(false);\n  const deleteShare = useDeleteMemoShare();\n  const url = getShareUrl(share);\n\n  const handleCopy = async () => {\n    await navigator.clipboard.writeText(url);\n    setCopied(true);\n    setTimeout(() => setCopied(false), 2000);\n  };\n\n  const handleRevoke = async () => {\n    try {\n      await deleteShare.mutateAsync({ name: share.name, memoName });\n      toast.success(t(\"memo.share.revoked\"));\n    } catch (e) {\n      toast.error((e as ConnectError).message || t(\"memo.share.revoke-failed\"));\n    }\n  };\n\n  return (\n    <div className=\"flex flex-col gap-1 rounded-md border border-border p-3\">\n      <div className=\"flex items-center justify-between gap-2\">\n        <span className=\"truncate font-mono text-xs text-muted-foreground\">{url}</span>\n        <div className=\"flex shrink-0 items-center gap-1\">\n          <Button variant=\"ghost\" size=\"icon\" className=\"h-7 w-7\" onClick={handleCopy} title={t(\"memo.share.copy\")}>\n            {copied ? <CheckIcon className=\"h-3.5 w-3.5 text-green-500\" /> : <CopyIcon className=\"h-3.5 w-3.5\" />}\n          </Button>\n          <Button\n            variant=\"ghost\"\n            size=\"icon\"\n            className=\"h-7 w-7 text-destructive hover:text-destructive\"\n            onClick={handleRevoke}\n            disabled={deleteShare.isPending}\n            title={t(\"memo.share.revoke\")}\n          >\n            <Trash2Icon className=\"h-3.5 w-3.5\" />\n          </Button>\n        </div>\n      </div>\n      <p className=\"text-xs text-muted-foreground\">{formatExpiry(share, t)}</p>\n    </div>\n  );\n}\n\ninterface MemoSharePanelProps {\n  open: boolean;\n  onClose: () => void;\n  memoName: string;\n}\n\nconst MemoSharePanel = ({ open, onClose, memoName }: MemoSharePanelProps) => {\n  const t = useTranslate();\n  const [expiry, setExpiry] = useState<ExpiryOption>(\"never\");\n  const { data: shares = [], isLoading } = useMemoShares(memoName, { enabled: open });\n  const createShare = useCreateMemoShare();\n\n  const handleCreate = async () => {\n    try {\n      await createShare.mutateAsync({ memoName, expireTime: getExpireDate(expiry) });\n    } catch (e) {\n      toast.error((e as ConnectError).message || t(\"memo.share.create-failed\"));\n    }\n  };\n\n  return (\n    <Dialog open={open} onOpenChange={(v) => !v && onClose()}>\n      <DialogContent className=\"sm:max-w-md\">\n        <DialogHeader>\n          <DialogTitle className=\"flex items-center gap-2\">\n            <LinkIcon className=\"h-4 w-4\" />\n            {t(\"memo.share.title\")}\n          </DialogTitle>\n        </DialogHeader>\n\n        <div className=\"flex flex-col gap-4 py-2\">\n          {/* Active links */}\n          <div className=\"flex flex-col gap-2\">\n            <p className=\"text-sm font-medium text-muted-foreground\">{t(\"memo.share.active-links\")}</p>\n            {isLoading ? (\n              <Loader2Icon className=\"h-4 w-4 animate-spin text-muted-foreground\" />\n            ) : shares.length === 0 ? (\n              <p className=\"text-sm text-muted-foreground\">{t(\"memo.share.no-links\")}</p>\n            ) : (\n              <div className=\"flex flex-col gap-2\">\n                {shares.map((share) => (\n                  <ShareLinkRow key={share.name} share={share} memoName={memoName} />\n                ))}\n              </div>\n            )}\n          </div>\n\n          {/* Create new link */}\n          <div className=\"flex items-center gap-2\">\n            <Select value={expiry} onValueChange={(v) => setExpiry(v as ExpiryOption)}>\n              <SelectTrigger className=\"w-36\">\n                <SelectValue />\n              </SelectTrigger>\n              <SelectContent>\n                <SelectItem value=\"never\">{t(\"memo.share.expiry-never\")}</SelectItem>\n                <SelectItem value=\"1d\">{t(\"memo.share.expiry-1-day\")}</SelectItem>\n                <SelectItem value=\"7d\">{t(\"memo.share.expiry-7-days\")}</SelectItem>\n                <SelectItem value=\"30d\">{t(\"memo.share.expiry-30-days\")}</SelectItem>\n              </SelectContent>\n            </Select>\n            <Button onClick={handleCreate} disabled={createShare.isPending} className=\"flex-1\">\n              {createShare.isPending ? (\n                <>\n                  <Loader2Icon className=\"mr-2 h-4 w-4 animate-spin\" />\n                  {t(\"memo.share.creating\")}\n                </>\n              ) : (\n                t(\"memo.share.create-link\")\n              )}\n            </Button>\n          </div>\n        </div>\n      </DialogContent>\n    </Dialog>\n  );\n};\n\nexport default MemoSharePanel;\n"
  },
  {
    "path": "web/src/components/MemoView/MemoView.tsx",
    "content": "import { memo, useMemo, useRef, useState } from \"react\";\nimport { useLocation } from \"react-router-dom\";\nimport useCurrentUser from \"@/hooks/useCurrentUser\";\nimport { useUser } from \"@/hooks/useUserQueries\";\nimport { cn } from \"@/lib/utils\";\nimport { State } from \"@/types/proto/api/v1/common_pb\";\nimport { isSuperUser } from \"@/utils/user\";\nimport MemoEditor from \"../MemoEditor\";\nimport PreviewImageDialog from \"../PreviewImageDialog\";\nimport { MemoBody, MemoCommentListView, MemoHeader } from \"./components\";\nimport { MEMO_CARD_BASE_CLASSES } from \"./constants\";\nimport { useImagePreview, useMemoActions, useMemoHandlers } from \"./hooks\";\nimport { computeCommentAmount, MemoViewContext } from \"./MemoViewContext\";\nimport type { MemoViewProps } from \"./types\";\n\nconst MemoView: React.FC<MemoViewProps> = (props: MemoViewProps) => {\n  const { memo: memoData, className, parentPage: parentPageProp, compact, showCreator, showVisibility, showPinned } = props;\n  const cardRef = useRef<HTMLDivElement>(null);\n  const [showEditor, setShowEditor] = useState(false);\n\n  const currentUser = useCurrentUser();\n  const creator = useUser(memoData.creator).data;\n  const isArchived = memoData.state === State.ARCHIVED;\n  const readonly = memoData.creator !== currentUser?.name && !isSuperUser(currentUser);\n  const parentPage = parentPageProp || \"/\";\n\n  // NSFW content management: always blur content tagged with NSFW (case-insensitive)\n  const [showNSFWContent, setShowNSFWContent] = useState(false);\n  const nsfw = memoData.tags?.some((tag) => tag.toUpperCase() === \"NSFW\") ?? false;\n  const toggleNsfwVisibility = () => setShowNSFWContent((prev) => !prev);\n\n  const { previewState, openPreview, setPreviewOpen } = useImagePreview();\n  const { unpinMemo } = useMemoActions(memoData);\n\n  const closeEditor = () => setShowEditor(false);\n  const openEditor = () => setShowEditor(true);\n\n  const { handleGotoMemoDetailPage, handleMemoContentClick, handleMemoContentDoubleClick } = useMemoHandlers({\n    memoName: memoData.name,\n    parentPage,\n    readonly,\n    openEditor,\n    openPreview,\n  });\n\n  const location = useLocation();\n  const isInMemoDetailPage = location.pathname.startsWith(`/${memoData.name}`);\n  const showCommentPreview = !isInMemoDetailPage && computeCommentAmount(memoData) > 0;\n\n  const contextValue = useMemo(\n    () => ({\n      memo: memoData,\n      creator,\n      currentUser,\n      parentPage,\n      isArchived,\n      readonly,\n      showNSFWContent,\n      nsfw,\n    }),\n    [memoData, creator, currentUser, parentPage, isArchived, readonly, showNSFWContent, nsfw],\n  );\n\n  if (showEditor) {\n    return (\n      <MemoEditor\n        autoFocus\n        className=\"mb-2\"\n        cacheKey={`inline-memo-editor-${memoData.name}`}\n        memo={memoData}\n        onConfirm={closeEditor}\n        onCancel={closeEditor}\n      />\n    );\n  }\n\n  const article = (\n    <article\n      className={cn(MEMO_CARD_BASE_CLASSES, showCommentPreview ? \"mb-0 rounded-b-none\" : \"mb-2\", className)}\n      ref={cardRef}\n      tabIndex={readonly ? -1 : 0}\n    >\n      <MemoHeader\n        showCreator={showCreator}\n        showVisibility={showVisibility}\n        showPinned={showPinned}\n        onEdit={openEditor}\n        onGotoDetail={handleGotoMemoDetailPage}\n        onUnpin={unpinMemo}\n      />\n\n      <MemoBody\n        compact={compact}\n        onContentClick={handleMemoContentClick}\n        onContentDoubleClick={handleMemoContentDoubleClick}\n        onToggleNsfwVisibility={toggleNsfwVisibility}\n      />\n\n      <PreviewImageDialog\n        open={previewState.open}\n        onOpenChange={setPreviewOpen}\n        imgUrls={previewState.urls}\n        initialIndex={previewState.index}\n      />\n    </article>\n  );\n\n  return (\n    <MemoViewContext.Provider value={contextValue}>\n      {showCommentPreview ? (\n        <div className=\"w-full mb-2\">\n          {article}\n          <MemoCommentListView />\n        </div>\n      ) : (\n        article\n      )}\n    </MemoViewContext.Provider>\n  );\n};\n\nexport default memo(MemoView);\n"
  },
  {
    "path": "web/src/components/MemoView/MemoViewContext.tsx",
    "content": "import { timestampDate } from \"@bufbuild/protobuf/wkt\";\nimport { createContext, useContext } from \"react\";\nimport { useLocation } from \"react-router-dom\";\nimport type { Memo } from \"@/types/proto/api/v1/memo_service_pb\";\nimport { MemoRelation_Type } from \"@/types/proto/api/v1/memo_service_pb\";\nimport type { User } from \"@/types/proto/api/v1/user_service_pb\";\nimport { RELATIVE_TIME_THRESHOLD_MS } from \"./constants\";\n\nexport interface MemoViewContextValue {\n  memo: Memo;\n  creator: User | undefined;\n  currentUser: User | undefined;\n  parentPage: string;\n  isArchived: boolean;\n  readonly: boolean;\n  showNSFWContent: boolean;\n  nsfw: boolean;\n}\n\nexport const MemoViewContext = createContext<MemoViewContextValue | null>(null);\n\nexport const useMemoViewContext = (): MemoViewContextValue => {\n  const context = useContext(MemoViewContext);\n  if (!context) {\n    throw new Error(\"useMemoViewContext must be used within MemoViewContext.Provider\");\n  }\n  return context;\n};\n\nexport const computeCommentAmount = (memo: Memo): number =>\n  memo.relations.filter((r) => r.type === MemoRelation_Type.COMMENT && r.relatedMemo?.name === memo.name).length;\n\nexport const useMemoViewDerived = () => {\n  const { memo, isArchived, readonly } = useMemoViewContext();\n  const location = useLocation();\n\n  const isInMemoDetailPage = location.pathname.startsWith(`/${memo.name}`);\n  const commentAmount = computeCommentAmount(memo);\n\n  const displayTime = memo.displayTime ? timestampDate(memo.displayTime) : undefined;\n  const relativeTimeFormat: \"datetime\" | \"auto\" =\n    displayTime && Date.now() - displayTime.getTime() > RELATIVE_TIME_THRESHOLD_MS ? \"datetime\" : \"auto\";\n\n  return {\n    isArchived,\n    readonly,\n    isInMemoDetailPage,\n    commentAmount,\n    relativeTimeFormat,\n  };\n};\n"
  },
  {
    "path": "web/src/components/MemoView/components/MemoBody.tsx",
    "content": "import { cn } from \"@/lib/utils\";\nimport { MemoRelation_Type } from \"@/types/proto/api/v1/memo_service_pb\";\nimport { useTranslate } from \"@/utils/i18n\";\nimport MemoContent from \"../../MemoContent\";\nimport { MemoReactionListView } from \"../../MemoReactionListView\";\nimport { useMemoViewContext } from \"../MemoViewContext\";\nimport type { MemoBodyProps } from \"../types\";\nimport { AttachmentList, LocationDisplay, RelationList } from \"./metadata\";\n\nconst NsfwOverlay: React.FC<{ onClick?: () => void }> = ({ onClick }) => {\n  const t = useTranslate();\n  return (\n    <div className=\"absolute inset-0 z-10 pt-4 flex items-center justify-center\" onClick={onClick}>\n      <button\n        type=\"button\"\n        className=\"rounded-lg border border-border bg-card px-2 py-1 text-xs text-muted-foreground transition-colors hover:border-accent hover:bg-accent hover:text-foreground\"\n      >\n        {t(\"memo.click-to-show-nsfw-content\")}\n      </button>\n    </div>\n  );\n};\n\nconst MemoBody: React.FC<MemoBodyProps> = ({ compact, onContentClick, onContentDoubleClick, onToggleNsfwVisibility }) => {\n  const { memo, parentPage, showNSFWContent, nsfw } = useMemoViewContext();\n\n  const referencedMemos = memo.relations.filter((relation) => relation.type === MemoRelation_Type.REFERENCE);\n\n  return (\n    <>\n      <div\n        className={cn(\n          \"w-full flex flex-col justify-start items-start gap-2\",\n          nsfw && !showNSFWContent && \"blur-lg transition-all duration-200\",\n        )}\n      >\n        <MemoContent\n          key={`${memo.name}-${memo.updateTime}`}\n          content={memo.content}\n          onClick={onContentClick}\n          onDoubleClick={onContentDoubleClick}\n          compact={memo.pinned ? false : compact} // Always show full content when pinned\n        />\n        <AttachmentList attachments={memo.attachments} />\n        <RelationList relations={referencedMemos} currentMemoName={memo.name} parentPage={parentPage} />\n        {memo.location && <LocationDisplay location={memo.location} />}\n        <MemoReactionListView memo={memo} reactions={memo.reactions} />\n      </div>\n\n      {nsfw && !showNSFWContent && <NsfwOverlay onClick={onToggleNsfwVisibility} />}\n    </>\n  );\n};\n\nexport default MemoBody;\n"
  },
  {
    "path": "web/src/components/MemoView/components/MemoCommentListView.tsx",
    "content": "import { ArrowUpRightIcon } from \"lucide-react\";\nimport { Link } from \"react-router-dom\";\nimport { extractMemoIdFromName } from \"@/helpers/resource-names\";\nimport { useMemoComments } from \"@/hooks/useMemoQueries\";\nimport { useMemoViewContext, useMemoViewDerived } from \"../MemoViewContext\";\nimport MemoSnippetLink from \"./MemoSnippetLink\";\n\nconst MemoCommentListView: React.FC = () => {\n  const { memo } = useMemoViewContext();\n  const { isInMemoDetailPage, commentAmount } = useMemoViewDerived();\n\n  const { data } = useMemoComments(memo.name, { enabled: !isInMemoDetailPage && commentAmount > 0 });\n  const comments = data?.memos ?? [];\n\n  if (isInMemoDetailPage || commentAmount === 0) {\n    return null;\n  }\n\n  const displayedComments = comments.slice(0, 3);\n\n  return (\n    <div className=\"border border-t-0 border-border rounded-b-lg px-4 pt-2 pb-3 flex flex-col gap-1\">\n      <div className=\"flex items-center justify-between mb-1\">\n        <span className=\"text-xs text-muted-foreground\">Comments{commentAmount > 1 ? ` (${commentAmount})` : \"\"}</span>\n        <Link\n          to={`/${memo.name}#comments`}\n          className=\"flex items-center gap-0.5 text-xs text-muted-foreground/80 hover:underline underline-offset-2 transition-colors\"\n        >\n          View all\n          <ArrowUpRightIcon className=\"w-3 h-3\" />\n        </Link>\n      </div>\n      {displayedComments.map((comment) => {\n        const uid = extractMemoIdFromName(comment.name);\n        return (\n          <MemoSnippetLink\n            key={comment.name}\n            name={comment.name}\n            snippet={comment.snippet || comment.content}\n            to={`/${memo.name}#${uid}`}\n            className=\"bg-muted/40 rounded-md\"\n          />\n        );\n      })}\n    </div>\n  );\n};\n\nexport default MemoCommentListView;\n"
  },
  {
    "path": "web/src/components/MemoView/components/MemoHeader.tsx",
    "content": "import { timestampDate } from \"@bufbuild/protobuf/wkt\";\nimport { BookmarkIcon } from \"lucide-react\";\nimport { useState } from \"react\";\nimport { Link } from \"react-router-dom\";\nimport { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from \"@/components/ui/tooltip\";\nimport i18n from \"@/i18n\";\nimport { cn } from \"@/lib/utils\";\nimport { Visibility } from \"@/types/proto/api/v1/memo_service_pb\";\nimport type { User } from \"@/types/proto/api/v1/user_service_pb\";\nimport { useTranslate } from \"@/utils/i18n\";\nimport { convertVisibilityToString } from \"@/utils/memo\";\nimport MemoActionMenu from \"../../MemoActionMenu\";\nimport { ReactionSelector } from \"../../MemoReactionListView\";\nimport UserAvatar from \"../../UserAvatar\";\nimport VisibilityIcon from \"../../VisibilityIcon\";\nimport { useMemoViewContext, useMemoViewDerived } from \"../MemoViewContext\";\nimport type { MemoHeaderProps } from \"../types\";\n\nconst MemoHeader: React.FC<MemoHeaderProps> = ({ showCreator, showVisibility, showPinned, onEdit, onGotoDetail, onUnpin }) => {\n  const t = useTranslate();\n  const [reactionSelectorOpen, setReactionSelectorOpen] = useState(false);\n\n  const { memo, creator, currentUser, isArchived, readonly } = useMemoViewContext();\n  const { relativeTimeFormat } = useMemoViewDerived();\n\n  const displayTime = isArchived ? (\n    (memo.displayTime ? timestampDate(memo.displayTime) : undefined)?.toLocaleString(i18n.language)\n  ) : (\n    <relative-time\n      datetime={(memo.displayTime ? timestampDate(memo.displayTime) : undefined)?.toISOString()}\n      lang={i18n.language}\n      format={relativeTimeFormat}\n    ></relative-time>\n  );\n\n  return (\n    <div className=\"w-full flex flex-row justify-between items-center gap-2\">\n      <div className=\"w-auto max-w-[calc(100%-8rem)] grow flex flex-row justify-start items-center\">\n        {showCreator && creator ? (\n          <CreatorDisplay creator={creator} displayTime={displayTime} onGotoDetail={onGotoDetail} />\n        ) : (\n          <TimeDisplay displayTime={displayTime} onGotoDetail={onGotoDetail} />\n        )}\n      </div>\n\n      <div className=\"flex flex-row justify-end items-center select-none shrink-0 gap-2\">\n        {currentUser && !isArchived && (\n          <ReactionSelector\n            className={cn(\"border-none w-auto h-auto\", reactionSelectorOpen && \"block!\", \"block sm:hidden sm:group-hover:block\")}\n            memo={memo}\n            onOpenChange={setReactionSelectorOpen}\n          />\n        )}\n\n        {showVisibility && memo.visibility !== Visibility.PRIVATE && (\n          <Tooltip>\n            <TooltipTrigger>\n              <span className=\"flex justify-center items-center rounded-md hover:opacity-80\">\n                <VisibilityIcon visibility={memo.visibility} />\n              </span>\n            </TooltipTrigger>\n            <TooltipContent>\n              {t(`memo.visibility.${convertVisibilityToString(memo.visibility).toLowerCase()}` as Parameters<typeof t>[0])}\n            </TooltipContent>\n          </Tooltip>\n        )}\n\n        {showPinned && memo.pinned && (\n          <TooltipProvider>\n            <Tooltip>\n              <TooltipTrigger asChild>\n                <span className=\"cursor-pointer\">\n                  <BookmarkIcon className=\"w-4 h-auto text-primary\" onClick={onUnpin} />\n                </span>\n              </TooltipTrigger>\n              <TooltipContent>\n                <p>{t(\"common.unpin\")}</p>\n              </TooltipContent>\n            </Tooltip>\n          </TooltipProvider>\n        )}\n\n        <MemoActionMenu memo={memo} readonly={readonly} onEdit={onEdit} />\n      </div>\n    </div>\n  );\n};\n\ninterface CreatorDisplayProps {\n  creator: User;\n  displayTime: React.ReactNode;\n  onGotoDetail: () => void;\n}\n\nconst CreatorDisplay: React.FC<CreatorDisplayProps> = ({ creator, displayTime, onGotoDetail }) => (\n  <div className=\"w-full flex flex-row justify-start items-center\">\n    <Link className=\"w-auto hover:opacity-80 rounded-md transition-colors\" to={`/u/${encodeURIComponent(creator.username)}`} viewTransition>\n      <UserAvatar className=\"mr-2 shrink-0\" avatarUrl={creator.avatarUrl} />\n    </Link>\n    <div className=\"w-full flex flex-col justify-center items-start\">\n      <Link\n        className=\"block leading-tight hover:opacity-80 rounded-md transition-colors truncate text-muted-foreground\"\n        to={`/u/${encodeURIComponent(creator.username)}`}\n        viewTransition\n      >\n        {creator.displayName || creator.username}\n      </Link>\n      <button\n        type=\"button\"\n        className=\"w-auto -mt-0.5 text-xs leading-tight text-muted-foreground select-none cursor-pointer hover:opacity-80 transition-colors text-left\"\n        onClick={onGotoDetail}\n      >\n        {displayTime}\n      </button>\n    </div>\n  </div>\n);\n\ninterface TimeDisplayProps {\n  displayTime: React.ReactNode;\n  onGotoDetail: () => void;\n}\n\nconst TimeDisplay: React.FC<TimeDisplayProps> = ({ displayTime, onGotoDetail }) => (\n  <button\n    type=\"button\"\n    className=\"w-full text-sm leading-tight text-muted-foreground select-none cursor-pointer hover:text-foreground transition-colors text-left\"\n    onClick={onGotoDetail}\n  >\n    {displayTime}\n  </button>\n);\n\nexport default MemoHeader;\n"
  },
  {
    "path": "web/src/components/MemoView/components/MemoSnippetLink.tsx",
    "content": "import { Link } from \"react-router-dom\";\nimport { extractMemoIdFromName } from \"@/helpers/resource-names\";\nimport { cn } from \"@/lib/utils\";\n\ninterface MemoSnippetLinkProps {\n  name: string;\n  snippet: string;\n  to: string;\n  state?: object;\n  className?: string;\n}\n\nconst MemoSnippetLink = ({ name, snippet, to, state, className }: MemoSnippetLinkProps) => {\n  const memoId = extractMemoIdFromName(name);\n\n  return (\n    <Link\n      className={cn(\n        \"flex items-center gap-1 px-1 py-1 rounded text-xs text-muted-foreground hover:text-foreground hover:bg-accent/20 transition-colors group\",\n        className,\n      )}\n      to={to}\n      viewTransition\n      state={state}\n    >\n      <span className=\"text-[8px] font-mono px-1 py-0.5 rounded border border-border bg-muted/40 group-hover:bg-accent/30 transition-colors shrink-0\">\n        {memoId.slice(0, 6)}\n      </span>\n      <span className=\"truncate\">{snippet || <span className=\"italic opacity-60\">No content</span>}</span>\n    </Link>\n  );\n};\n\nexport default MemoSnippetLink;\n"
  },
  {
    "path": "web/src/components/MemoView/components/index.ts",
    "content": "export { default as MemoBody } from \"./MemoBody\";\nexport { default as MemoCommentListView } from \"./MemoCommentListView\";\nexport { default as MemoHeader } from \"./MemoHeader\";\n"
  },
  {
    "path": "web/src/components/MemoView/components/metadata/AttachmentCard.tsx",
    "content": "import { cn } from \"@/lib/utils\";\nimport type { Attachment } from \"@/types/proto/api/v1/attachment_service_pb\";\nimport { getAttachmentType, getAttachmentUrl } from \"@/utils/attachment\";\n\ninterface AttachmentCardProps {\n  attachment: Attachment;\n  onClick?: () => void;\n  className?: string;\n}\n\nconst AttachmentCard = ({ attachment, onClick, className }: AttachmentCardProps) => {\n  const attachmentType = getAttachmentType(attachment);\n  const sourceUrl = getAttachmentUrl(attachment);\n\n  if (attachmentType === \"image/*\") {\n    return (\n      <img\n        src={sourceUrl}\n        alt={attachment.filename}\n        className={cn(\"w-full h-full object-cover rounded-lg cursor-pointer\", className)}\n        onClick={onClick}\n        loading=\"lazy\"\n      />\n    );\n  }\n\n  if (attachmentType === \"video/*\") {\n    return <video src={sourceUrl} className={cn(\"w-full h-full object-cover rounded-lg\", className)} controls preload=\"metadata\" />;\n  }\n\n  if (attachmentType === \"audio/*\") {\n    return <audio src={sourceUrl} className={cn(\"w-full rounded-lg\", className)} controls preload=\"metadata\" />;\n  }\n\n  return null;\n};\n\nexport default AttachmentCard;\n"
  },
  {
    "path": "web/src/components/MemoView/components/metadata/AttachmentList.tsx",
    "content": "import { FileAudioIcon, FileIcon, PaperclipIcon } from \"lucide-react\";\nimport { useMemo, useState } from \"react\";\nimport type { Attachment } from \"@/types/proto/api/v1/attachment_service_pb\";\nimport { getAttachmentType, getAttachmentUrl } from \"@/utils/attachment\";\nimport { formatFileSize, getFileTypeLabel } from \"@/utils/format\";\nimport PreviewImageDialog from \"../../../PreviewImageDialog\";\nimport AttachmentCard from \"./AttachmentCard\";\nimport SectionHeader from \"./SectionHeader\";\n\ninterface AttachmentListProps {\n  attachments: Attachment[];\n}\n\nconst isImageAttachment = (attachment: Attachment): boolean => getAttachmentType(attachment) === \"image/*\";\nconst isVideoAttachment = (attachment: Attachment): boolean => getAttachmentType(attachment) === \"video/*\";\nconst isAudioAttachment = (attachment: Attachment): boolean => getAttachmentType(attachment) === \"audio/*\";\n\nconst separateAttachments = (attachments: Attachment[]) => {\n  const visual: Attachment[] = [];\n  const audio: Attachment[] = [];\n  const docs: Attachment[] = [];\n\n  for (const attachment of attachments) {\n    if (isImageAttachment(attachment) || isVideoAttachment(attachment)) {\n      visual.push(attachment);\n    } else if (isAudioAttachment(attachment)) {\n      audio.push(attachment);\n    } else {\n      docs.push(attachment);\n    }\n  }\n\n  return { visual, audio, docs };\n};\n\nconst DocumentItem = ({ attachment }: { attachment: Attachment }) => {\n  const fileTypeLabel = getFileTypeLabel(attachment.type);\n  const fileSizeLabel = attachment.size ? formatFileSize(Number(attachment.size)) : undefined;\n\n  return (\n    <div className=\"flex items-center gap-1 px-1 py-1 rounded text-xs text-muted-foreground hover:text-foreground hover:bg-accent/20 transition-colors whitespace-nowrap\">\n      <div className=\"shrink-0 w-5 h-5 rounded overflow-hidden bg-muted/40 flex items-center justify-center\">\n        <FileIcon className=\"w-3 h-3 text-muted-foreground\" />\n      </div>\n      <div className=\"flex items-center gap-1 min-w-0\">\n        <span className=\"text-xs truncate\" title={attachment.filename}>\n          {attachment.filename}\n        </span>\n        <div className=\"flex items-center gap-1 text-xs text-muted-foreground shrink-0\">\n          <span className=\"text-muted-foreground/50\">•</span>\n          <span>{fileTypeLabel}</span>\n          {fileSizeLabel && (\n            <>\n              <span className=\"text-muted-foreground/50\">•</span>\n              <span>{fileSizeLabel}</span>\n            </>\n          )}\n        </div>\n      </div>\n    </div>\n  );\n};\n\nconst AudioItem = ({ attachment }: { attachment: Attachment }) => {\n  const sourceUrl = getAttachmentUrl(attachment);\n\n  return (\n    <div className=\"flex flex-col gap-1 px-1 py-1\">\n      <div className=\"flex items-center gap-1 text-xs text-muted-foreground\">\n        <FileAudioIcon className=\"w-3 h-3 shrink-0\" />\n        <span className=\"truncate\" title={attachment.filename}>\n          {attachment.filename}\n        </span>\n      </div>\n      <audio src={sourceUrl} controls preload=\"metadata\" className=\"w-full h-8\" />\n    </div>\n  );\n};\n\ninterface VisualItemProps {\n  attachment: Attachment;\n  onImageClick: (url: string) => void;\n}\n\nconst VisualItem = ({ attachment, onImageClick }: VisualItemProps) => {\n  const handleClick = () => {\n    if (isImageAttachment(attachment)) {\n      onImageClick(getAttachmentUrl(attachment));\n    }\n  };\n\n  return (\n    <div\n      className=\"aspect-square rounded-lg overflow-hidden bg-muted/40 border border-border hover:border-accent/50 transition-all cursor-pointer group\"\n      onClick={handleClick}\n    >\n      <AttachmentCard attachment={attachment} className=\"rounded-none\" />\n    </div>\n  );\n};\n\nconst VisualGrid = ({ attachments, onImageClick }: { attachments: Attachment[]; onImageClick: (url: string) => void }) => (\n  <div className=\"grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-2\">\n    {attachments.map((attachment) => (\n      <VisualItem key={attachment.name} attachment={attachment} onImageClick={onImageClick} />\n    ))}\n  </div>\n);\n\nconst AudioList = ({ attachments }: { attachments: Attachment[] }) => (\n  <div className=\"flex flex-col gap-1\">\n    {attachments.map((attachment) => (\n      <AudioItem key={attachment.name} attachment={attachment} />\n    ))}\n  </div>\n);\n\nconst DocsList = ({ attachments }: { attachments: Attachment[] }) => (\n  <div className=\"flex flex-col gap-0.5\">\n    {attachments.map((attachment) => (\n      <a key={attachment.name} href={getAttachmentUrl(attachment)} download title={`Download ${attachment.filename}`}>\n        <DocumentItem attachment={attachment} />\n      </a>\n    ))}\n  </div>\n);\n\nconst Divider = () => <div className=\"border-t mt-1 border-border opacity-60\" />;\n\nconst AttachmentList = ({ attachments }: AttachmentListProps) => {\n  const [previewImage, setPreviewImage] = useState<{ open: boolean; urls: string[]; index: number; mimeType?: string }>({\n    open: false,\n    urls: [],\n    index: 0,\n    mimeType: undefined,\n  });\n\n  const { visual, audio, docs } = useMemo(() => separateAttachments(attachments), [attachments]);\n\n  const imageAttachments = useMemo(() => visual.filter(isImageAttachment), [visual]);\n  const imageUrls = useMemo(() => imageAttachments.map(getAttachmentUrl), [imageAttachments]);\n\n  if (attachments.length === 0) {\n    return null;\n  }\n\n  const handleImageClick = (imgUrl: string) => {\n    const index = imageUrls.findIndex((url) => url === imgUrl);\n    const mimeType = imageAttachments[index]?.type;\n    setPreviewImage({ open: true, urls: imageUrls, index, mimeType });\n  };\n\n  const sections = [visual.length > 0, audio.length > 0, docs.length > 0];\n  const sectionCount = sections.filter(Boolean).length;\n\n  return (\n    <>\n      <div className=\"w-full rounded-lg border border-border bg-muted/20 overflow-hidden\">\n        <SectionHeader icon={PaperclipIcon} title=\"Attachments\" count={attachments.length} />\n\n        <div className=\"p-1.5 flex flex-col gap-1\">\n          {visual.length > 0 && <VisualGrid attachments={visual} onImageClick={handleImageClick} />}\n\n          {visual.length > 0 && sectionCount > 1 && <Divider />}\n\n          {audio.length > 0 && <AudioList attachments={audio} />}\n\n          {audio.length > 0 && docs.length > 0 && <Divider />}\n\n          {docs.length > 0 && <DocsList attachments={docs} />}\n        </div>\n      </div>\n\n      <PreviewImageDialog\n        open={previewImage.open}\n        onOpenChange={(open: boolean) => setPreviewImage((prev) => ({ ...prev, open }))}\n        imgUrls={previewImage.urls}\n        initialIndex={previewImage.index}\n      />\n    </>\n  );\n};\n\nexport default AttachmentList;\n"
  },
  {
    "path": "web/src/components/MemoView/components/metadata/LocationDisplay.tsx",
    "content": "import { LatLng } from \"leaflet\";\nimport { MapPinIcon } from \"lucide-react\";\nimport { useState } from \"react\";\nimport { LocationPicker } from \"@/components/map\";\nimport { cn } from \"@/lib/utils\";\nimport type { Location } from \"@/types/proto/api/v1/memo_service_pb\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"../../../ui/popover\";\n\ninterface LocationDisplayProps {\n  location?: Location;\n  className?: string;\n}\n\nconst LocationDisplay = ({ location, className }: LocationDisplayProps) => {\n  const [popoverOpen, setPopoverOpen] = useState<boolean>(false);\n\n  if (!location) {\n    return null;\n  }\n\n  const displayText = location.placeholder || `Position: [${location.latitude}, ${location.longitude}]`;\n\n  return (\n    <Popover open={popoverOpen} onOpenChange={setPopoverOpen}>\n      <PopoverTrigger asChild>\n        <div\n          className={cn(\n            \"w-full flex flex-row gap-2 cursor-pointer\",\n            \"relative inline-flex items-center gap-1.5 px-2 h-7 rounded-md border border-border bg-muted/20 hover:bg-accent/20 text-muted-foreground hover:text-foreground text-xs transition-colors\",\n            className,\n          )}\n          onClick={() => setPopoverOpen(true)}\n        >\n          <span className=\"shrink-0 text-muted-foreground\">\n            <MapPinIcon className=\"w-3.5 h-3.5\" />\n          </span>\n          <span className=\"text-nowrap opacity-80\">\n            [{location.latitude.toFixed(2)}°, {location.longitude.toFixed(2)}°]\n          </span>\n          <span className=\"text-nowrap truncate\">{displayText}</span>\n        </div>\n      </PopoverTrigger>\n      <PopoverContent align=\"start\">\n        <div className=\"min-w-80 sm:w-lg flex flex-col justify-start items-start\">\n          <LocationPicker latlng={new LatLng(location.latitude, location.longitude)} readonly={true} />\n        </div>\n      </PopoverContent>\n    </Popover>\n  );\n};\n\nexport default LocationDisplay;\n"
  },
  {
    "path": "web/src/components/MemoView/components/metadata/RelationCard.tsx",
    "content": "import type { MemoRelation_Memo } from \"@/types/proto/api/v1/memo_service_pb\";\nimport MemoSnippetLink from \"../MemoSnippetLink\";\n\ninterface RelationCardProps {\n  memo: MemoRelation_Memo;\n  parentPage?: string;\n  className?: string;\n}\n\nconst RelationCard = ({ memo, parentPage, className }: RelationCardProps) => {\n  return (\n    <MemoSnippetLink name={memo.name} snippet={memo.snippet} to={`/${memo.name}`} state={{ from: parentPage }} className={className} />\n  );\n};\n\nexport default RelationCard;\n"
  },
  {
    "path": "web/src/components/MemoView/components/metadata/RelationList.tsx",
    "content": "import { LinkIcon, MilestoneIcon } from \"lucide-react\";\nimport { useMemo, useState } from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport type { MemoRelation } from \"@/types/proto/api/v1/memo_service_pb\";\nimport { MemoRelation_Type } from \"@/types/proto/api/v1/memo_service_pb\";\nimport { useTranslate } from \"@/utils/i18n\";\nimport RelationCard from \"./RelationCard\";\nimport SectionHeader from \"./SectionHeader\";\n\ninterface RelationListProps {\n  relations: MemoRelation[];\n  currentMemoName?: string;\n  parentPage?: string;\n  className?: string;\n}\n\nfunction RelationList({ relations, currentMemoName, parentPage, className }: RelationListProps) {\n  const t = useTranslate();\n  const [activeTab, setActiveTab] = useState<\"referencing\" | \"referenced\">(\"referencing\");\n\n  const { referencingRelations, referencedRelations } = useMemo(() => {\n    return {\n      referencingRelations: relations.filter(\n        (r) => r.type === MemoRelation_Type.REFERENCE && r.memo?.name === currentMemoName && r.relatedMemo?.name !== currentMemoName,\n      ),\n      referencedRelations: relations.filter(\n        (r) => r.type === MemoRelation_Type.REFERENCE && r.memo?.name !== currentMemoName && r.relatedMemo?.name === currentMemoName,\n      ),\n    };\n  }, [relations, currentMemoName]);\n\n  if (referencingRelations.length === 0 && referencedRelations.length === 0) {\n    return null;\n  }\n\n  const hasBothTabs = referencingRelations.length > 0 && referencedRelations.length > 0;\n  const defaultTab = referencingRelations.length > 0 ? \"referencing\" : \"referenced\";\n  const tab = hasBothTabs ? activeTab : defaultTab;\n  const isReferencing = tab === \"referencing\";\n  const icon = isReferencing ? LinkIcon : MilestoneIcon;\n  const activeRelations = isReferencing ? referencingRelations : referencedRelations;\n\n  return (\n    <div className={cn(\"w-full rounded-lg border border-border bg-muted/20 overflow-hidden\", className)}>\n      <SectionHeader\n        icon={icon}\n        title={isReferencing ? t(\"common.referencing\") : t(\"common.referenced-by\")}\n        count={activeRelations.length}\n        tabs={\n          hasBothTabs\n            ? [\n                {\n                  id: \"referencing\",\n                  label: t(\"common.referencing\"),\n                  count: referencingRelations.length,\n                  active: isReferencing,\n                  onClick: () => setActiveTab(\"referencing\"),\n                },\n                {\n                  id: \"referenced\",\n                  label: t(\"common.referenced-by\"),\n                  count: referencedRelations.length,\n                  active: !isReferencing,\n                  onClick: () => setActiveTab(\"referenced\"),\n                },\n              ]\n            : undefined\n        }\n      />\n\n      <div className=\"p-1.5 flex flex-col gap-0\">\n        {activeRelations.map((relation) => (\n          <RelationCard\n            key={isReferencing ? relation.relatedMemo!.name : relation.memo!.name}\n            memo={isReferencing ? relation.relatedMemo! : relation.memo!}\n            parentPage={parentPage}\n          />\n        ))}\n      </div>\n    </div>\n  );\n}\n\nexport default RelationList;\n"
  },
  {
    "path": "web/src/components/MemoView/components/metadata/SectionHeader.tsx",
    "content": "import { LucideIcon } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\n\ninterface SectionHeaderProps {\n  icon: LucideIcon;\n  title: string;\n  count: number;\n  tabs?: Array<{\n    id: string;\n    label: string;\n    count: number;\n    active: boolean;\n    onClick: () => void;\n  }>;\n}\n\nconst SectionHeader = ({ icon: Icon, title, count, tabs }: SectionHeaderProps) => {\n  return (\n    <div className=\"flex items-center gap-1.5 px-2 py-1 border-b border-border bg-muted/30\">\n      <Icon className=\"w-3.5 h-3.5 text-muted-foreground\" />\n\n      {tabs && tabs.length > 1 ? (\n        <div className=\"flex items-center gap-0.5\">\n          {tabs.map((tab, idx) => (\n            <div key={tab.id} className=\"flex items-center gap-0.5\">\n              <button\n                onClick={tab.onClick}\n                className={cn(\n                  \"text-xs px-0 py-0 transition-colors\",\n                  tab.active ? \"text-muted-foreground\" : \"text-muted-foreground/60 hover:text-muted-foreground\",\n                )}\n              >\n                {tab.label} ({tab.count})\n              </button>\n              {idx < tabs.length - 1 && <span className=\"text-muted-foreground/40 font-mono text-xs\">/</span>}\n            </div>\n          ))}\n        </div>\n      ) : (\n        <span className=\"text-xs text-muted-foreground\">\n          {title} ({count})\n        </span>\n      )}\n    </div>\n  );\n};\n\nexport default SectionHeader;\n"
  },
  {
    "path": "web/src/components/MemoView/components/metadata/index.ts",
    "content": "export { default as AttachmentCard } from \"./AttachmentCard\";\nexport { default as AttachmentList } from \"./AttachmentList\";\nexport { default as LocationDisplay } from \"./LocationDisplay\";\n\nexport { default as RelationCard } from \"./RelationCard\";\nexport { default as RelationList } from \"./RelationList\";\n"
  },
  {
    "path": "web/src/components/MemoView/constants.ts",
    "content": "export const MEMO_CARD_BASE_CLASSES =\n  \"relative group flex flex-col justify-start items-start bg-card w-full px-4 py-3 mb-2 gap-2 text-card-foreground rounded-lg border border-border transition-colors\";\n\nexport const RELATIVE_TIME_THRESHOLD_MS = 1000 * 60 * 60 * 24;\n"
  },
  {
    "path": "web/src/components/MemoView/hooks/index.ts",
    "content": "export { useImagePreview } from \"./useImagePreview\";\nexport { useMemoActions } from \"./useMemoActions\";\nexport { useMemoHandlers } from \"./useMemoHandlers\";\n"
  },
  {
    "path": "web/src/components/MemoView/hooks/useImagePreview.ts",
    "content": "import { useState } from \"react\";\n\nexport interface ImagePreviewState {\n  open: boolean;\n  urls: string[];\n  index: number;\n}\n\nexport interface UseImagePreviewReturn {\n  previewState: ImagePreviewState;\n  openPreview: (url: string) => void;\n  setPreviewOpen: (open: boolean) => void;\n}\n\nexport const useImagePreview = (): UseImagePreviewReturn => {\n  const [previewState, setPreviewState] = useState<ImagePreviewState>({ open: false, urls: [], index: 0 });\n\n  return {\n    previewState,\n    openPreview: (url: string) => setPreviewState({ open: true, urls: [url], index: 0 }),\n    setPreviewOpen: (open: boolean) => setPreviewState((prev) => ({ ...prev, open })),\n  };\n};\n"
  },
  {
    "path": "web/src/components/MemoView/hooks/useMemoActions.ts",
    "content": "import { useUpdateMemo } from \"@/hooks/useMemoQueries\";\nimport type { Memo } from \"@/types/proto/api/v1/memo_service_pb\";\n\nexport const useMemoActions = (memo: Memo) => {\n  const { mutateAsync: updateMemo } = useUpdateMemo();\n\n  const unpinMemo = async () => {\n    if (!memo.pinned) return;\n    await updateMemo({ update: { name: memo.name, pinned: false }, updateMask: [\"pinned\"] });\n  };\n\n  return { unpinMemo };\n};\n"
  },
  {
    "path": "web/src/components/MemoView/hooks/useMemoHandlers.ts",
    "content": "import { useCallback } from \"react\";\nimport { useInstance } from \"@/contexts/InstanceContext\";\nimport useNavigateTo from \"@/hooks/useNavigateTo\";\n\ninterface UseMemoHandlersOptions {\n  memoName: string;\n  parentPage: string;\n  readonly: boolean;\n  openEditor: () => void;\n  openPreview: (url: string) => void;\n}\n\nexport const useMemoHandlers = (options: UseMemoHandlersOptions) => {\n  const { memoName, parentPage, readonly, openEditor, openPreview } = options;\n  const navigateTo = useNavigateTo();\n  const { memoRelatedSetting } = useInstance();\n\n  const handleGotoMemoDetailPage = useCallback(() => {\n    navigateTo(`/${memoName}`, { state: { from: parentPage } });\n  }, [memoName, parentPage, navigateTo]);\n\n  const handleMemoContentClick = useCallback(\n    (e: React.MouseEvent) => {\n      const targetEl = e.target as HTMLElement;\n      if (targetEl.tagName === \"IMG\") {\n        const linkElement = targetEl.closest(\"a\");\n        if (linkElement) return; // If image is inside a link, don't show preview\n        const imgUrl = targetEl.getAttribute(\"src\");\n        if (imgUrl) openPreview(imgUrl);\n      }\n    },\n    [openPreview],\n  );\n\n  const handleMemoContentDoubleClick = useCallback(\n    (e: React.MouseEvent) => {\n      if (readonly) return;\n      if (memoRelatedSetting.enableDoubleClickEdit) {\n        e.preventDefault();\n        openEditor();\n      }\n    },\n    [readonly, openEditor, memoRelatedSetting.enableDoubleClickEdit],\n  );\n\n  return { handleGotoMemoDetailPage, handleMemoContentClick, handleMemoContentDoubleClick };\n};\n"
  },
  {
    "path": "web/src/components/MemoView/index.ts",
    "content": "export { default, default as MemoView } from \"./MemoView\";\nexport type { MemoViewProps } from \"./types\";\n"
  },
  {
    "path": "web/src/components/MemoView/types.ts",
    "content": "import type { Memo } from \"@/types/proto/api/v1/memo_service_pb\";\n\nexport interface MemoViewProps {\n  memo: Memo;\n  compact?: boolean;\n  showCreator?: boolean;\n  showVisibility?: boolean;\n  showPinned?: boolean;\n  className?: string;\n  parentPage?: string;\n}\n\nexport interface MemoHeaderProps {\n  showCreator?: boolean;\n  showVisibility?: boolean;\n  showPinned?: boolean;\n  onEdit: () => void;\n  onGotoDetail: () => void;\n  onUnpin: () => void;\n}\n\nexport interface MemoBodyProps {\n  compact?: boolean;\n  onContentClick: (e: React.MouseEvent) => void;\n  onContentDoubleClick: (e: React.MouseEvent) => void;\n  onToggleNsfwVisibility: () => void;\n}\n"
  },
  {
    "path": "web/src/components/MemosLogo.tsx",
    "content": "import { useInstance } from \"@/contexts/InstanceContext\";\nimport { cn } from \"@/lib/utils\";\nimport UserAvatar from \"./UserAvatar\";\n\ninterface Props {\n  className?: string;\n  collapsed?: boolean;\n}\n\nfunction MemosLogo(props: Props) {\n  const { collapsed } = props;\n  const { generalSetting: instanceGeneralSetting } = useInstance();\n  const title = instanceGeneralSetting.customProfile?.title || \"Memos\";\n  const avatarUrl = instanceGeneralSetting.customProfile?.logoUrl || \"/full-logo.webp\";\n\n  return (\n    <div className={cn(\"relative w-full h-auto shrink-0\", props.className)}>\n      <div className={cn(\"w-auto flex flex-row justify-start items-center text-foreground\", collapsed ? \"px-1\" : \"px-3\")}>\n        <UserAvatar className=\"shrink-0\" avatarUrl={avatarUrl} />\n        {!collapsed && <span className=\"ml-2 text-lg font-medium text-foreground shrink truncate\">{title}</span>}\n      </div>\n    </div>\n  );\n}\n\nexport default MemosLogo;\n"
  },
  {
    "path": "web/src/components/MobileHeader.tsx",
    "content": "import useWindowScroll from \"react-use/lib/useWindowScroll\";\nimport useMediaQuery from \"@/hooks/useMediaQuery\";\nimport { cn } from \"@/lib/utils\";\nimport NavigationDrawer from \"./NavigationDrawer\";\n\ninterface Props {\n  className?: string;\n  children?: React.ReactNode;\n}\n\nconst MobileHeader = (props: Props) => {\n  const { className, children } = props;\n  const { y: offsetTop } = useWindowScroll();\n  const md = useMediaQuery(\"md\");\n  const sm = useMediaQuery(\"sm\");\n\n  if (md) return null;\n\n  return (\n    <div\n      className={cn(\n        \"sticky top-0 pt-3 pb-2 sm:pt-2 px-4 sm:px-6 sm:mb-1 bg-background bg-opacity-80 backdrop-blur-lg flex flex-row justify-between items-center w-full h-auto flex-nowrap shrink-0 z-1\",\n        offsetTop > 0 && \"shadow-md\",\n        className,\n      )}\n    >\n      {!sm && <NavigationDrawer />}\n      <div className=\"w-full flex flex-row justify-end items-center\">{children}</div>\n    </div>\n  );\n};\n\nexport default MobileHeader;\n"
  },
  {
    "path": "web/src/components/Navigation.tsx",
    "content": "import { BellIcon, EarthIcon, LibraryIcon, PaperclipIcon, UserCircleIcon } from \"lucide-react\";\nimport { NavLink } from \"react-router-dom\";\nimport { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from \"@/components/ui/tooltip\";\nimport useCurrentUser from \"@/hooks/useCurrentUser\";\nimport { useNotifications } from \"@/hooks/useUserQueries\";\nimport { cn } from \"@/lib/utils\";\nimport { Routes } from \"@/router\";\nimport { UserNotification_Status } from \"@/types/proto/api/v1/user_service_pb\";\nimport { useTranslate } from \"@/utils/i18n\";\nimport MemosLogo from \"./MemosLogo\";\nimport UserMenu from \"./UserMenu\";\n\ninterface NavLinkItem {\n  id: string;\n  path: string;\n  title: string;\n  icon: React.ReactNode;\n}\n\ninterface Props {\n  collapsed?: boolean;\n  className?: string;\n}\n\nconst Navigation = (props: Props) => {\n  const { collapsed, className } = props;\n  const t = useTranslate();\n  const currentUser = useCurrentUser();\n  const { data: notifications = [] } = useNotifications();\n\n  const homeNavLink: NavLinkItem = {\n    id: \"header-memos\",\n    path: Routes.ROOT,\n    title: t(\"common.memos\"),\n    icon: <LibraryIcon className=\"w-6 h-auto shrink-0\" />,\n  };\n  const exploreNavLink: NavLinkItem = {\n    id: \"header-explore\",\n    path: Routes.EXPLORE,\n    title: t(\"common.explore\"),\n    icon: <EarthIcon className=\"w-6 h-auto shrink-0\" />,\n  };\n  const attachmentsNavLink: NavLinkItem = {\n    id: \"header-attachments\",\n    path: Routes.ATTACHMENTS,\n    title: t(\"common.attachments\"),\n    icon: <PaperclipIcon className=\"w-6 h-auto shrink-0\" />,\n  };\n  const unreadCount = notifications.filter((n) => n.status === UserNotification_Status.UNREAD).length;\n  const inboxNavLink: NavLinkItem = {\n    id: \"header-inbox\",\n    path: Routes.INBOX,\n    title: t(\"common.inbox\"),\n    icon: (\n      <div className=\"relative\">\n        <BellIcon className=\"w-6 h-auto shrink-0\" />\n        {unreadCount > 0 && (\n          <span className=\"absolute -top-1 -right-1 min-w-[18px] h-[18px] px-1 flex items-center justify-center bg-primary text-primary-foreground text-[10px] font-semibold rounded-full border-2 border-background\">\n            {unreadCount > 99 ? \"99+\" : unreadCount}\n          </span>\n        )}\n      </div>\n    ),\n  };\n  const signInNavLink: NavLinkItem = {\n    id: \"header-auth\",\n    path: Routes.AUTH,\n    title: t(\"common.sign-in\"),\n    icon: <UserCircleIcon className=\"w-6 h-auto shrink-0\" />,\n  };\n\n  const navLinks: NavLinkItem[] = currentUser\n    ? [homeNavLink, exploreNavLink, attachmentsNavLink, inboxNavLink]\n    : [exploreNavLink, signInNavLink];\n\n  return (\n    <header className={cn(\"w-full h-full overflow-auto flex flex-col justify-between items-start gap-4\", className)}>\n      <div className=\"w-full px-1 py-1 flex flex-col justify-start items-start space-y-2 overflow-auto overflow-x-hidden shrink\">\n        <NavLink className=\"mb-3 cursor-default\" to={currentUser ? Routes.ROOT : Routes.EXPLORE}>\n          <MemosLogo collapsed={collapsed} />\n        </NavLink>\n        {navLinks.map((navLink) => (\n          <NavLink\n            className={({ isActive }) =>\n              cn(\n                \"px-2 py-2 rounded-2xl border flex flex-row items-center text-lg text-sidebar-foreground transition-colors\",\n                collapsed ? \"\" : \"w-full px-4\",\n                isActive\n                  ? \"bg-sidebar-accent text-sidebar-accent-foreground border-sidebar-accent-border drop-shadow\"\n                  : \"border-transparent hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:border-sidebar-accent-border opacity-80\",\n              )\n            }\n            key={navLink.id}\n            to={navLink.path}\n            id={navLink.id}\n            viewTransition\n          >\n            {props.collapsed ? (\n              <TooltipProvider>\n                <Tooltip>\n                  <TooltipTrigger asChild>\n                    <div>{navLink.icon}</div>\n                  </TooltipTrigger>\n                  <TooltipContent side=\"right\">\n                    <p>{navLink.title}</p>\n                  </TooltipContent>\n                </Tooltip>\n              </TooltipProvider>\n            ) : (\n              navLink.icon\n            )}\n            {!props.collapsed && <span className=\"ml-3 truncate\">{navLink.title}</span>}\n          </NavLink>\n        ))}\n      </div>\n      {currentUser && (\n        <div className={cn(\"w-full flex flex-col justify-end\", props.collapsed ? \"items-center\" : \"items-start pl-3\")}>\n          <UserMenu collapsed={collapsed} />\n        </div>\n      )}\n    </header>\n  );\n};\n\nexport default Navigation;\n"
  },
  {
    "path": "web/src/components/NavigationDrawer.tsx",
    "content": "import { useEffect, useState } from \"react\";\nimport { useLocation } from \"react-router-dom\";\nimport { Button } from \"@/components/ui/button\";\nimport { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from \"@/components/ui/sheet\";\nimport { useInstance } from \"@/contexts/InstanceContext\";\nimport Navigation from \"./Navigation\";\nimport UserAvatar from \"./UserAvatar\";\n\nconst NavigationDrawer = () => {\n  const location = useLocation();\n  const [open, setOpen] = useState(false);\n  const { generalSetting } = useInstance();\n  const title = generalSetting.customProfile?.title || \"Memos\";\n  const avatarUrl = generalSetting.customProfile?.logoUrl || \"/full-logo.webp\";\n\n  useEffect(() => {\n    setOpen(false);\n  }, [location.key]);\n\n  return (\n    <Sheet open={open} onOpenChange={setOpen}>\n      <SheetTrigger asChild>\n        <Button variant=\"ghost\" className=\"px-2\">\n          <UserAvatar className=\"shrink-0 w-6 h-6 rounded-md\" avatarUrl={avatarUrl} />\n          <span className=\"font-bold text-lg leading-10 text-ellipsis overflow-hidden text-foreground\">{title}</span>\n        </Button>\n      </SheetTrigger>\n      <SheetContent side=\"left\" className=\"w-80 max-w-full overflow-auto px-2 bg-background\">\n        <SheetHeader>\n          <SheetTitle />\n        </SheetHeader>\n        <Navigation className=\"pb-4\" />\n      </SheetContent>\n    </Sheet>\n  );\n};\n\nexport default NavigationDrawer;\n"
  },
  {
    "path": "web/src/components/PagedMemoList/PagedMemoList.tsx",
    "content": "import { useQueryClient } from \"@tanstack/react-query\";\nimport { ArrowUpIcon } from \"lucide-react\";\nimport { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport { matchPath } from \"react-router-dom\";\nimport { Button } from \"@/components/ui/button\";\nimport { userServiceClient } from \"@/connect\";\nimport { DEFAULT_LIST_MEMOS_PAGE_SIZE } from \"@/helpers/consts\";\nimport { useInfiniteMemos } from \"@/hooks/useMemoQueries\";\nimport { userKeys } from \"@/hooks/useUserQueries\";\nimport { Routes } from \"@/router\";\nimport { State } from \"@/types/proto/api/v1/common_pb\";\nimport type { Memo } from \"@/types/proto/api/v1/memo_service_pb\";\nimport { useTranslate } from \"@/utils/i18n\";\nimport Empty from \"../Empty\";\nimport MemoEditor from \"../MemoEditor\";\nimport MemoFilters from \"../MemoFilters\";\nimport Skeleton from \"../Skeleton\";\n\ninterface Props {\n  renderer: (memo: Memo) => JSX.Element;\n  listSort?: (list: Memo[]) => Memo[];\n  state?: State;\n  orderBy?: string;\n  filter?: string;\n  pageSize?: number;\n  showCreator?: boolean;\n  enabled?: boolean;\n}\n\nfunction useAutoFetchWhenNotScrollable({\n  hasNextPage,\n  isFetchingNextPage,\n  memoCount,\n  onFetchNext,\n}: {\n  hasNextPage: boolean | undefined;\n  isFetchingNextPage: boolean;\n  memoCount: number;\n  onFetchNext: () => Promise<unknown>;\n}) {\n  const autoFetchTimeoutRef = useRef<number | null>(null);\n\n  const isPageScrollable = useCallback(() => {\n    const documentHeight = Math.max(document.body.scrollHeight, document.documentElement.scrollHeight);\n    return documentHeight > window.innerHeight + 100;\n  }, []);\n\n  const checkAndFetchIfNeeded = useCallback(async () => {\n    if (autoFetchTimeoutRef.current) {\n      clearTimeout(autoFetchTimeoutRef.current);\n    }\n\n    await new Promise((resolve) => setTimeout(resolve, 200));\n\n    const shouldFetch = !isPageScrollable() && hasNextPage && !isFetchingNextPage && memoCount > 0;\n\n    if (shouldFetch) {\n      await onFetchNext();\n\n      autoFetchTimeoutRef.current = window.setTimeout(() => {\n        void checkAndFetchIfNeeded();\n      }, 500);\n    }\n  }, [hasNextPage, isFetchingNextPage, memoCount, isPageScrollable, onFetchNext]);\n\n  useEffect(() => {\n    if (!isFetchingNextPage && memoCount > 0) {\n      void checkAndFetchIfNeeded();\n    }\n  }, [memoCount, isFetchingNextPage, checkAndFetchIfNeeded]);\n\n  useEffect(() => {\n    return () => {\n      if (autoFetchTimeoutRef.current) {\n        clearTimeout(autoFetchTimeoutRef.current);\n      }\n    };\n  }, []);\n}\n\nconst PagedMemoList = (props: Props) => {\n  const t = useTranslate();\n  const queryClient = useQueryClient();\n\n  // Show memo editor only on the root route\n  const showMemoEditor = Boolean(matchPath(Routes.ROOT, window.location.pathname));\n\n  const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading } = useInfiniteMemos(\n    {\n      state: props.state || State.NORMAL,\n      orderBy: props.orderBy || \"display_time desc\",\n      filter: props.filter,\n      pageSize: props.pageSize || DEFAULT_LIST_MEMOS_PAGE_SIZE,\n    },\n    { enabled: props.enabled ?? true },\n  );\n\n  // Flatten pages into a single array of memos\n  const memos = useMemo(() => data?.pages.flatMap((page) => page.memos) || [], [data]);\n\n  // Apply custom sorting if provided, otherwise use memos directly\n  const sortedMemoList = useMemo(() => (props.listSort ? props.listSort(memos) : memos), [memos, props.listSort]);\n\n  // Prefetch creators when new data arrives to improve performance\n  useEffect(() => {\n    if (!data?.pages || !props.showCreator) return;\n\n    const lastPage = data.pages[data.pages.length - 1];\n    if (!lastPage?.memos) return;\n\n    const uniqueCreators = Array.from(new Set(lastPage.memos.map((memo) => memo.creator)));\n    for (const creator of uniqueCreators) {\n      void queryClient.prefetchQuery({\n        queryKey: userKeys.detail(creator),\n        queryFn: async () => {\n          const user = await userServiceClient.getUser({ name: creator });\n          return user;\n        },\n        staleTime: 1000 * 60 * 5,\n      });\n    }\n  }, [data?.pages, props.showCreator, queryClient]);\n\n  // Auto-fetch hook: fetches more content when page isn't scrollable\n  useAutoFetchWhenNotScrollable({\n    hasNextPage,\n    isFetchingNextPage,\n    memoCount: sortedMemoList.length,\n    onFetchNext: fetchNextPage,\n  });\n\n  // Infinite scroll: fetch more when user scrolls near bottom\n  useEffect(() => {\n    if (!hasNextPage) return;\n\n    const handleScroll = () => {\n      const nearBottom = window.innerHeight + window.scrollY >= document.body.offsetHeight - 300;\n      if (nearBottom && !isFetchingNextPage) {\n        fetchNextPage();\n      }\n    };\n\n    window.addEventListener(\"scroll\", handleScroll);\n    return () => window.removeEventListener(\"scroll\", handleScroll);\n  }, [hasNextPage, isFetchingNextPage, fetchNextPage]);\n\n  const children = (\n    <div className=\"flex flex-col justify-start w-full max-w-2xl mx-auto\">\n      {/* Show skeleton loader during initial load */}\n      {isLoading ? (\n        <Skeleton showCreator={props.showCreator} count={4} />\n      ) : (\n        <>\n          {showMemoEditor ? <MemoEditor className=\"mb-2\" cacheKey=\"home-memo-editor\" placeholder={t(\"editor.any-thoughts\")} /> : null}\n          <MemoFilters />\n          {sortedMemoList.map((memo) => props.renderer(memo))}\n\n          {/* Loading indicator for pagination */}\n          {isFetchingNextPage && <Skeleton showCreator={props.showCreator} count={2} />}\n\n          {/* Empty state or back-to-top button */}\n          {!isFetchingNextPage && (\n            <>\n              {!hasNextPage && sortedMemoList.length === 0 ? (\n                <div className=\"w-full mt-12 mb-8 flex flex-col justify-center items-center italic\">\n                  <Empty />\n                  <p className=\"mt-2 text-muted-foreground\">{t(\"message.no-data\")}</p>\n                </div>\n              ) : (\n                <div className=\"w-full opacity-70 flex flex-row justify-center items-center my-4\">\n                  <BackToTop />\n                </div>\n              )}\n            </>\n          )}\n        </>\n      )}\n    </div>\n  );\n\n  return children;\n};\n\nconst BackToTop = () => {\n  const t = useTranslate();\n  const [isVisible, setIsVisible] = useState(false);\n\n  useEffect(() => {\n    const handleScroll = () => {\n      const shouldShow = window.scrollY > 400;\n      setIsVisible(shouldShow);\n    };\n\n    window.addEventListener(\"scroll\", handleScroll);\n    return () => window.removeEventListener(\"scroll\", handleScroll);\n  }, []);\n\n  const scrollToTop = () => {\n    window.scrollTo({\n      top: 0,\n      behavior: \"smooth\",\n    });\n  };\n\n  // Don't render if not visible\n  if (!isVisible) {\n    return null;\n  }\n\n  return (\n    <Button variant=\"ghost\" onClick={scrollToTop}>\n      {t(\"router.back-to-top\")}\n      <ArrowUpIcon className=\"ml-1 w-4 h-auto\" />\n    </Button>\n  );\n};\n\nexport default PagedMemoList;\n"
  },
  {
    "path": "web/src/components/PagedMemoList/index.ts",
    "content": "import PagedMemoList from \"./PagedMemoList\";\n\nexport default PagedMemoList;\n"
  },
  {
    "path": "web/src/components/PasswordSignInForm.tsx",
    "content": "import { timestampDate } from \"@bufbuild/protobuf/wkt\";\nimport { LoaderIcon } from \"lucide-react\";\nimport { useState } from \"react\";\nimport { toast } from \"react-hot-toast\";\nimport { setAccessToken } from \"@/auth-state\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { authServiceClient } from \"@/connect\";\nimport { useAuth } from \"@/contexts/AuthContext\";\nimport { useInstance } from \"@/contexts/InstanceContext\";\nimport useLoading from \"@/hooks/useLoading\";\nimport useNavigateTo from \"@/hooks/useNavigateTo\";\nimport { handleError } from \"@/lib/error\";\nimport { useTranslate } from \"@/utils/i18n\";\n\nfunction PasswordSignInForm() {\n  const t = useTranslate();\n  const navigateTo = useNavigateTo();\n  const { profile } = useInstance();\n  const { initialize } = useAuth();\n  const actionBtnLoadingState = useLoading(false);\n  const [username, setUsername] = useState(profile.demo ? \"demo\" : \"\");\n  const [password, setPassword] = useState(profile.demo ? \"secret\" : \"\");\n\n  const handleUsernameInputChanged = (e: React.ChangeEvent<HTMLInputElement>) => {\n    const text = e.target.value as string;\n    setUsername(text);\n  };\n\n  const handlePasswordInputChanged = (e: React.ChangeEvent<HTMLInputElement>) => {\n    const text = e.target.value as string;\n    setPassword(text);\n  };\n\n  const handleFormSubmit = (e: React.FormEvent<HTMLFormElement>) => {\n    e.preventDefault();\n    handleSignInButtonClick();\n  };\n\n  const handleSignInButtonClick = async () => {\n    if (username === \"\" || password === \"\") {\n      return;\n    }\n\n    if (actionBtnLoadingState.isLoading) {\n      return;\n    }\n\n    try {\n      actionBtnLoadingState.setLoading();\n      const response = await authServiceClient.signIn({\n        credentials: {\n          case: \"passwordCredentials\",\n          value: { username, password },\n        },\n      });\n      // Store access token from login response\n      if (response.accessToken) {\n        setAccessToken(response.accessToken, response.accessTokenExpiresAt ? timestampDate(response.accessTokenExpiresAt) : undefined);\n      }\n      await initialize();\n      navigateTo(\"/\");\n    } catch (error: unknown) {\n      handleError(error, toast.error, {\n        fallbackMessage: \"Failed to sign in.\",\n      });\n    }\n    actionBtnLoadingState.setFinish();\n  };\n\n  return (\n    <form className=\"w-full mt-2\" onSubmit={handleFormSubmit}>\n      <div className=\"flex flex-col justify-start items-start w-full gap-4\">\n        <div className=\"w-full flex flex-col justify-start items-start\">\n          <span className=\"leading-8 text-muted-foreground\">{t(\"common.username\")}</span>\n          <Input\n            className=\"w-full bg-background h-10\"\n            type=\"text\"\n            readOnly={actionBtnLoadingState.isLoading}\n            placeholder={t(\"common.username\")}\n            value={username}\n            autoComplete=\"username\"\n            autoCapitalize=\"off\"\n            spellCheck={false}\n            onChange={handleUsernameInputChanged}\n            required\n          />\n        </div>\n        <div className=\"w-full flex flex-col justify-start items-start\">\n          <span className=\"leading-8 text-muted-foreground\">{t(\"common.password\")}</span>\n          <Input\n            className=\"w-full bg-background h-10\"\n            type=\"password\"\n            readOnly={actionBtnLoadingState.isLoading}\n            placeholder={t(\"common.password\")}\n            value={password}\n            autoComplete=\"current-password\"\n            autoCapitalize=\"off\"\n            spellCheck={false}\n            onChange={handlePasswordInputChanged}\n            required\n          />\n        </div>\n      </div>\n      <div className=\"flex flex-row justify-end items-center w-full mt-6\">\n        <Button type=\"submit\" className=\"w-full h-10\" disabled={actionBtnLoadingState.isLoading} onClick={handleSignInButtonClick}>\n          {t(\"common.sign-in\")}\n          {actionBtnLoadingState.isLoading && <LoaderIcon className=\"w-5 h-auto ml-2 animate-spin opacity-60\" />}\n        </Button>\n      </div>\n    </form>\n  );\n}\n\nexport default PasswordSignInForm;\n"
  },
  {
    "path": "web/src/components/PreviewImageDialog.tsx",
    "content": "import { X } from \"lucide-react\";\nimport React, { useEffect, useState } from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Dialog, DialogContent } from \"@/components/ui/dialog\";\n\ninterface Props {\n  open: boolean;\n  onOpenChange: (open: boolean) => void;\n  imgUrls: string[];\n  initialIndex?: number;\n}\n\nfunction PreviewImageDialog({ open, onOpenChange, imgUrls, initialIndex = 0 }: Props) {\n  const [currentIndex, setCurrentIndex] = useState(initialIndex);\n\n  // Update current index when initialIndex prop changes\n  useEffect(() => {\n    setCurrentIndex(initialIndex);\n  }, [initialIndex]);\n\n  // Handle keyboard navigation\n  useEffect(() => {\n    const handleKeyDown = (event: KeyboardEvent) => {\n      if (!open) return;\n\n      switch (event.key) {\n        case \"Escape\":\n          onOpenChange(false);\n          break;\n        case \"ArrowRight\":\n          setCurrentIndex((prev) => Math.min(prev + 1, imgUrls.length - 1));\n          break;\n        case \"ArrowLeft\":\n          setCurrentIndex((prev) => Math.max(prev - 1, 0));\n          break;\n        default:\n          break;\n      }\n    };\n\n    document.addEventListener(\"keydown\", handleKeyDown);\n    return () => document.removeEventListener(\"keydown\", handleKeyDown);\n  }, [open, onOpenChange]);\n\n  const handleClose = () => {\n    onOpenChange(false);\n  };\n\n  const handleBackdropClick = (event: React.MouseEvent<HTMLDivElement>) => {\n    if (event.target === event.currentTarget) {\n      handleClose();\n    }\n  };\n\n  // Return early if no images provided\n  if (!imgUrls.length) return null;\n\n  // Ensure currentIndex is within bounds\n  const safeIndex = Math.max(0, Math.min(currentIndex, imgUrls.length - 1));\n\n  return (\n    <Dialog open={open} onOpenChange={onOpenChange}>\n      <DialogContent\n        className=\"!w-[100vw] !h-[100vh] !max-w-[100vw] !max-h-[100vw] p-0 border-0 shadow-none bg-transparent [&>button]:hidden\"\n        aria-describedby=\"image-preview-description\"\n      >\n        {/* Close button */}\n        <div className=\"fixed top-4 right-4 z-50\">\n          <Button\n            onClick={handleClose}\n            variant=\"secondary\"\n            size=\"icon\"\n            className=\"rounded-full bg-popover/20 hover:bg-popover/30 border-border/20 backdrop-blur-sm\"\n            aria-label=\"Close image preview\"\n          >\n            <X className=\"h-4 w-4 text-popover-foreground\" />\n          </Button>\n        </div>\n\n        {/* Image container */}\n        <div className=\"w-full h-full flex items-center justify-center p-4 sm:p-8 overflow-auto\" onClick={handleBackdropClick}>\n          <img\n            src={imgUrls[safeIndex]}\n            alt={`Preview image ${safeIndex + 1} of ${imgUrls.length}`}\n            className=\"max-w-full max-h-full object-contain select-none\"\n            draggable={false}\n            loading=\"eager\"\n            decoding=\"async\"\n          />\n        </div>\n\n        {/* Screen reader description */}\n        <div id=\"image-preview-description\" className=\"sr-only\">\n          Image preview dialog. Press Escape to close or click outside the image.\n        </div>\n      </DialogContent>\n    </Dialog>\n  );\n}\n\nexport default PreviewImageDialog;\n"
  },
  {
    "path": "web/src/components/RequiredBadge.tsx",
    "content": "interface Props {\n  className?: string;\n}\n\nconst RequiredBadge: React.FC<Props> = (props: Props) => {\n  const { className } = props;\n\n  return <span className={`mx-0.5 text-destructive font-bold ${className ?? \"\"}`}>*</span>;\n};\n\nexport default RequiredBadge;\n"
  },
  {
    "path": "web/src/components/SearchBar.tsx",
    "content": "import { SearchIcon } from \"lucide-react\";\nimport { useRef, useState } from \"react\";\nimport { useMemoFilterContext } from \"@/contexts/MemoFilterContext\";\nimport { cn } from \"@/lib/utils\";\nimport { useTranslate } from \"@/utils/i18n\";\nimport MemoDisplaySettingMenu from \"./MemoDisplaySettingMenu\";\n\nconst SearchBar = () => {\n  const t = useTranslate();\n  const { addFilter } = useMemoFilterContext();\n  const [queryText, setQueryText] = useState(\"\");\n  const inputRef = useRef<HTMLInputElement>(null);\n\n  const onTextChange = (event: React.FormEvent<HTMLInputElement>) => {\n    setQueryText(event.currentTarget.value);\n  };\n\n  const onKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {\n    if (e.key === \"Enter\") {\n      e.preventDefault();\n      const trimmedText = queryText.trim();\n      if (trimmedText !== \"\") {\n        const words = trimmedText.split(/\\s+/);\n        words.forEach((word) => {\n          addFilter({\n            factor: \"contentSearch\",\n            value: word,\n          });\n        });\n        setQueryText(\"\");\n      }\n    }\n  };\n\n  return (\n    <div className=\"relative w-full h-auto flex flex-row justify-start items-center\">\n      <SearchIcon className=\"absolute left-2 w-4 h-auto opacity-40 text-sidebar-foreground\" />\n      <input\n        className={cn(\"w-full text-sidebar-foreground leading-6 bg-sidebar border border-border text-sm rounded-lg p-1 pl-8 outline-0\")}\n        placeholder={t(\"memo.search-placeholder\")}\n        value={queryText}\n        onChange={onTextChange}\n        onKeyDown={onKeyDown}\n        ref={inputRef}\n      />\n      <MemoDisplaySettingMenu className=\"absolute right-2 top-2 text-sidebar-foreground\" />\n    </div>\n  );\n};\n\nexport default SearchBar;\n"
  },
  {
    "path": "web/src/components/Settings/AccessTokenSection.tsx",
    "content": "import { timestampDate } from \"@bufbuild/protobuf/wkt\";\nimport copy from \"copy-to-clipboard\";\nimport { PlusIcon, TrashIcon } from \"lucide-react\";\nimport { useEffect, useState } from \"react\";\nimport { toast } from \"react-hot-toast\";\nimport ConfirmDialog from \"@/components/ConfirmDialog\";\nimport { Button } from \"@/components/ui/button\";\nimport { userServiceClient } from \"@/connect\";\nimport useCurrentUser from \"@/hooks/useCurrentUser\";\nimport { useDialog } from \"@/hooks/useDialog\";\nimport { handleError } from \"@/lib/error\";\nimport { CreatePersonalAccessTokenResponse, PersonalAccessToken } from \"@/types/proto/api/v1/user_service_pb\";\nimport { useTranslate } from \"@/utils/i18n\";\nimport CreateAccessTokenDialog from \"../CreateAccessTokenDialog\";\nimport SettingTable from \"./SettingTable\";\n\nconst listAccessTokens = async (parent: string) => {\n  const { personalAccessTokens } = await userServiceClient.listPersonalAccessTokens({ parent });\n  return personalAccessTokens.sort(\n    (a, b) =>\n      ((b.createdAt ? timestampDate(b.createdAt) : undefined)?.getTime() ?? 0) -\n      ((a.createdAt ? timestampDate(a.createdAt) : undefined)?.getTime() ?? 0),\n  );\n};\n\nconst AccessTokenSection = () => {\n  const t = useTranslate();\n  const currentUser = useCurrentUser();\n  const [personalAccessTokens, setPersonalAccessTokens] = useState<PersonalAccessToken[]>([]);\n  const createTokenDialog = useDialog();\n  const [deleteTarget, setDeleteTarget] = useState<PersonalAccessToken | undefined>(undefined);\n\n  useEffect(() => {\n    if (!currentUser?.name) return;\n    let canceled = false;\n    listAccessTokens(currentUser.name)\n      .then((tokens) => {\n        if (!canceled) {\n          setPersonalAccessTokens(tokens);\n        }\n      })\n      .catch((error: unknown) => {\n        if (!canceled) {\n          handleError(error, toast.error, { context: \"List access tokens\" });\n        }\n      });\n    return () => {\n      canceled = true;\n    };\n  }, [currentUser?.name]);\n\n  const handleCreateAccessTokenDialogConfirm = async (response: CreatePersonalAccessTokenResponse) => {\n    const tokens = await listAccessTokens(currentUser?.name ?? \"\");\n    setPersonalAccessTokens(tokens);\n    // Copy the token to clipboard - this is the only time it will be shown\n    if (response.token) {\n      copy(response.token);\n      toast.success(t(\"setting.access-token.access-token-copied-to-clipboard\"));\n    }\n    toast.success(\n      t(\"setting.access-token.create-dialog.access-token-created\", {\n        description: response.personalAccessToken?.description ?? \"\",\n      }),\n    );\n  };\n\n  const handleCreateToken = () => {\n    createTokenDialog.open();\n  };\n\n  const handleDeleteAccessToken = async (token: PersonalAccessToken) => {\n    setDeleteTarget(token);\n  };\n\n  const confirmDeleteAccessToken = async () => {\n    if (!deleteTarget) return;\n    const { name: tokenName, description } = deleteTarget;\n    await userServiceClient.deletePersonalAccessToken({ name: tokenName });\n    setPersonalAccessTokens((prev) => prev.filter((token) => token.name !== tokenName));\n    setDeleteTarget(undefined);\n    toast.success(t(\"setting.access-token.access-token-deleted\", { description }));\n  };\n\n  return (\n    <div className=\"w-full flex flex-col gap-2\">\n      <div className=\"flex flex-col sm:flex-row sm:items-start sm:justify-between gap-2\">\n        <div className=\"flex flex-col gap-1\">\n          <h4 className=\"text-sm font-medium text-muted-foreground\">{t(\"setting.access-token.title\")}</h4>\n          <p className=\"text-xs text-muted-foreground\">{t(\"setting.access-token.description\")}</p>\n        </div>\n        <Button onClick={handleCreateToken} size=\"sm\">\n          <PlusIcon className=\"w-4 h-4 mr-1.5\" />\n          {t(\"common.create\")}\n        </Button>\n      </div>\n\n      <SettingTable\n        columns={[\n          {\n            key: \"description\",\n            header: t(\"common.description\"),\n            render: (_, token: PersonalAccessToken) => <span className=\"text-foreground\">{token.description}</span>,\n          },\n          {\n            key: \"createdAt\",\n            header: t(\"setting.access-token.create-dialog.created-at\"),\n            render: (_, token: PersonalAccessToken) => (token.createdAt ? timestampDate(token.createdAt) : undefined)?.toLocaleString(),\n          },\n          {\n            key: \"expiresAt\",\n            header: t(\"setting.access-token.create-dialog.expires-at\"),\n            render: (_, token: PersonalAccessToken) =>\n              (token.expiresAt ? timestampDate(token.expiresAt) : undefined)?.toLocaleString() ??\n              t(\"setting.access-token.create-dialog.duration-never\"),\n          },\n          {\n            key: \"actions\",\n            header: \"\",\n            className: \"text-right\",\n            render: (_, token: PersonalAccessToken) => (\n              <Button variant=\"ghost\" size=\"sm\" onClick={() => handleDeleteAccessToken(token)}>\n                <TrashIcon className=\"text-destructive w-4 h-auto\" />\n              </Button>\n            ),\n          },\n        ]}\n        data={personalAccessTokens}\n        emptyMessage=\"No access tokens found\"\n        getRowKey={(token) => token.name}\n      />\n\n      {/* Create Access Token Dialog */}\n      <CreateAccessTokenDialog\n        open={createTokenDialog.isOpen}\n        onOpenChange={createTokenDialog.setOpen}\n        onSuccess={handleCreateAccessTokenDialogConfirm}\n      />\n      <ConfirmDialog\n        open={!!deleteTarget}\n        onOpenChange={(open) => !open && setDeleteTarget(undefined)}\n        title={deleteTarget ? t(\"setting.access-token.access-token-deletion\", { description: deleteTarget.description }) : \"\"}\n        description={t(\"setting.access-token.access-token-deletion-description\")}\n        confirmLabel={t(\"common.delete\")}\n        cancelLabel={t(\"common.cancel\")}\n        onConfirm={confirmDeleteAccessToken}\n        confirmVariant=\"destructive\"\n      />\n    </div>\n  );\n};\n\nexport default AccessTokenSection;\n"
  },
  {
    "path": "web/src/components/Settings/InstanceSection.tsx",
    "content": "import { create } from \"@bufbuild/protobuf\";\nimport { isEqual } from \"lodash-es\";\nimport { useEffect, useState } from \"react\";\nimport { toast } from \"react-hot-toast\";\nimport { Button } from \"@/components/ui/button\";\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from \"@/components/ui/select\";\nimport { Switch } from \"@/components/ui/switch\";\nimport { Textarea } from \"@/components/ui/textarea\";\nimport { identityProviderServiceClient } from \"@/connect\";\nimport { useInstance } from \"@/contexts/InstanceContext\";\nimport useDialog from \"@/hooks/useDialog\";\nimport { handleError } from \"@/lib/error\";\nimport { IdentityProvider } from \"@/types/proto/api/v1/idp_service_pb\";\nimport {\n  InstanceSetting_GeneralSetting,\n  InstanceSetting_GeneralSettingSchema,\n  InstanceSetting_Key,\n  InstanceSettingSchema,\n} from \"@/types/proto/api/v1/instance_service_pb\";\nimport { useTranslate } from \"@/utils/i18n\";\nimport UpdateCustomizedProfileDialog from \"../UpdateCustomizedProfileDialog\";\nimport SettingGroup from \"./SettingGroup\";\nimport SettingRow from \"./SettingRow\";\nimport SettingSection from \"./SettingSection\";\n\nconst InstanceSection = () => {\n  const t = useTranslate();\n  const customizeDialog = useDialog();\n  const { generalSetting: originalSetting, profile, updateSetting, fetchSetting } = useInstance();\n  const [instanceGeneralSetting, setInstanceGeneralSetting] = useState<InstanceSetting_GeneralSetting>(originalSetting);\n  const [identityProviderList, setIdentityProviderList] = useState<IdentityProvider[]>([]);\n\n  useEffect(() => {\n    setInstanceGeneralSetting({ ...instanceGeneralSetting, customProfile: originalSetting.customProfile });\n  }, [originalSetting]);\n\n  const handleUpdateCustomizedProfileButtonClick = () => {\n    customizeDialog.open();\n  };\n\n  const updatePartialSetting = (partial: Partial<InstanceSetting_GeneralSetting>) => {\n    setInstanceGeneralSetting(\n      create(InstanceSetting_GeneralSettingSchema, {\n        ...instanceGeneralSetting,\n        ...partial,\n      }),\n    );\n  };\n\n  const handleSaveGeneralSetting = async () => {\n    try {\n      await updateSetting(\n        create(InstanceSettingSchema, {\n          name: `instance/settings/${InstanceSetting_Key[InstanceSetting_Key.GENERAL]}`,\n          value: {\n            case: \"generalSetting\",\n            value: instanceGeneralSetting,\n          },\n        }),\n      );\n      await fetchSetting(InstanceSetting_Key.GENERAL);\n    } catch (error: unknown) {\n      await handleError(error, toast.error, {\n        context: \"Update general settings\",\n      });\n      return;\n    }\n    toast.success(t(\"message.update-succeed\"));\n  };\n\n  useEffect(() => {\n    fetchIdentityProviderList();\n  }, []);\n\n  const fetchIdentityProviderList = async () => {\n    const { identityProviders } = await identityProviderServiceClient.listIdentityProviders({});\n    setIdentityProviderList(identityProviders);\n  };\n\n  return (\n    <SettingSection>\n      <SettingGroup title={t(\"common.basic\")}>\n        <SettingRow label={t(\"setting.system.server-name\")} description={instanceGeneralSetting.customProfile?.title || \"Memos\"}>\n          <Button variant=\"outline\" onClick={handleUpdateCustomizedProfileButtonClick}>\n            {t(\"common.edit\")}\n          </Button>\n        </SettingRow>\n      </SettingGroup>\n\n      <SettingGroup title={t(\"setting.system.title\")} showSeparator>\n        <SettingRow label={t(\"setting.system.additional-style\")} vertical>\n          <Textarea\n            className=\"font-mono w-full\"\n            rows={3}\n            placeholder={t(\"setting.system.additional-style-placeholder\")}\n            value={instanceGeneralSetting.additionalStyle}\n            onChange={(event) => updatePartialSetting({ additionalStyle: event.target.value })}\n          />\n        </SettingRow>\n\n        <SettingRow label={t(\"setting.system.additional-script\")} vertical>\n          <Textarea\n            className=\"font-mono w-full\"\n            rows={3}\n            placeholder={t(\"setting.system.additional-script-placeholder\")}\n            value={instanceGeneralSetting.additionalScript}\n            onChange={(event) => updatePartialSetting({ additionalScript: event.target.value })}\n          />\n        </SettingRow>\n      </SettingGroup>\n\n      <SettingGroup>\n        <SettingRow label={t(\"setting.instance.disallow-user-registration\")}>\n          <Switch\n            disabled={profile.demo}\n            checked={instanceGeneralSetting.disallowUserRegistration}\n            onCheckedChange={(checked) => updatePartialSetting({ disallowUserRegistration: checked })}\n          />\n        </SettingRow>\n\n        <SettingRow label={t(\"setting.instance.disallow-password-auth\")}>\n          <Switch\n            disabled={profile.demo || (identityProviderList.length === 0 && !instanceGeneralSetting.disallowPasswordAuth)}\n            checked={instanceGeneralSetting.disallowPasswordAuth}\n            onCheckedChange={(checked) => updatePartialSetting({ disallowPasswordAuth: checked })}\n          />\n        </SettingRow>\n\n        <SettingRow label={t(\"setting.instance.disallow-change-username\")}>\n          <Switch\n            checked={instanceGeneralSetting.disallowChangeUsername}\n            onCheckedChange={(checked) => updatePartialSetting({ disallowChangeUsername: checked })}\n          />\n        </SettingRow>\n\n        <SettingRow label={t(\"setting.instance.disallow-change-nickname\")}>\n          <Switch\n            checked={instanceGeneralSetting.disallowChangeNickname}\n            onCheckedChange={(checked) => updatePartialSetting({ disallowChangeNickname: checked })}\n          />\n        </SettingRow>\n\n        <SettingRow label={t(\"setting.instance.week-start-day\")}>\n          <Select\n            value={instanceGeneralSetting.weekStartDayOffset.toString()}\n            onValueChange={(value) => {\n              updatePartialSetting({ weekStartDayOffset: parseInt(value) || 0 });\n            }}\n          >\n            <SelectTrigger className=\"min-w-fit\">\n              <SelectValue />\n            </SelectTrigger>\n            <SelectContent>\n              <SelectItem value=\"-1\">{t(\"setting.instance.saturday\")}</SelectItem>\n              <SelectItem value=\"0\">{t(\"setting.instance.sunday\")}</SelectItem>\n              <SelectItem value=\"1\">{t(\"setting.instance.monday\")}</SelectItem>\n            </SelectContent>\n          </Select>\n        </SettingRow>\n      </SettingGroup>\n\n      <div className=\"w-full flex justify-end\">\n        <Button disabled={isEqual(instanceGeneralSetting, originalSetting)} onClick={handleSaveGeneralSetting}>\n          {t(\"common.save\")}\n        </Button>\n      </div>\n\n      <UpdateCustomizedProfileDialog\n        open={customizeDialog.isOpen}\n        onOpenChange={customizeDialog.setOpen}\n        onSuccess={() => {\n          // Refresh instance settings if needed\n          toast.success(\"Profile updated successfully!\");\n        }}\n      />\n    </SettingSection>\n  );\n};\n\nexport default InstanceSection;\n"
  },
  {
    "path": "web/src/components/Settings/MemberSection.tsx",
    "content": "import { create } from \"@bufbuild/protobuf\";\nimport { FieldMaskSchema } from \"@bufbuild/protobuf/wkt\";\nimport { sortBy } from \"lodash-es\";\nimport { MoreVerticalIcon, PlusIcon } from \"lucide-react\";\nimport { useState } from \"react\";\nimport toast from \"react-hot-toast\";\nimport ConfirmDialog from \"@/components/ConfirmDialog\";\nimport { Button } from \"@/components/ui/button\";\nimport { userServiceClient } from \"@/connect\";\nimport useCurrentUser from \"@/hooks/useCurrentUser\";\nimport { useDialog } from \"@/hooks/useDialog\";\nimport { useDeleteUser, useListUsers } from \"@/hooks/useUserQueries\";\nimport { State } from \"@/types/proto/api/v1/common_pb\";\nimport { User, User_Role } from \"@/types/proto/api/v1/user_service_pb\";\nimport { useTranslate } from \"@/utils/i18n\";\nimport CreateUserDialog from \"../CreateUserDialog\";\nimport { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from \"../ui/dropdown-menu\";\nimport SettingSection from \"./SettingSection\";\nimport SettingTable from \"./SettingTable\";\n\nconst MemberSection = () => {\n  const t = useTranslate();\n  const currentUser = useCurrentUser();\n  const { data: users = [], refetch: refetchUsers } = useListUsers();\n  const deleteUserMutation = useDeleteUser();\n  const createDialog = useDialog();\n  const editDialog = useDialog();\n  const [editingUser, setEditingUser] = useState<User | undefined>();\n  const sortedUsers = sortBy(users, \"id\");\n  const [archiveTarget, setArchiveTarget] = useState<User | undefined>(undefined);\n  const [deleteTarget, setDeleteTarget] = useState<User | undefined>(undefined);\n\n  const stringifyUserRole = (role: User_Role) => {\n    if (role === User_Role.ADMIN) {\n      return t(\"setting.member.admin\");\n    } else {\n      return t(\"setting.member.user\");\n    }\n  };\n\n  const handleCreateUser = () => {\n    setEditingUser(undefined);\n    createDialog.open();\n  };\n\n  const handleEditUser = (user: User) => {\n    setEditingUser(user);\n    editDialog.open();\n  };\n\n  const handleArchiveUserClick = async (user: User) => {\n    setArchiveTarget(user);\n  };\n\n  const confirmArchiveUser = async () => {\n    if (!archiveTarget) return;\n    const username = archiveTarget.username;\n    await userServiceClient.updateUser({\n      user: {\n        name: archiveTarget.name,\n        state: State.ARCHIVED,\n      },\n      updateMask: create(FieldMaskSchema, { paths: [\"state\"] }),\n    });\n    setArchiveTarget(undefined);\n    toast.success(t(\"setting.member.archive-success\", { username }));\n    await refetchUsers();\n  };\n\n  const handleRestoreUserClick = async (user: User) => {\n    const { username } = user;\n    await userServiceClient.updateUser({\n      user: {\n        name: user.name,\n        state: State.NORMAL,\n      },\n      updateMask: create(FieldMaskSchema, { paths: [\"state\"] }),\n    });\n    toast.success(t(\"setting.member.restore-success\", { username }));\n    await refetchUsers();\n  };\n\n  const handleDeleteUserClick = async (user: User) => {\n    setDeleteTarget(user);\n  };\n\n  const confirmDeleteUser = async () => {\n    if (!deleteTarget) return;\n    const { username, name } = deleteTarget;\n    deleteUserMutation.mutate(name);\n    setDeleteTarget(undefined);\n    toast.success(t(\"setting.member.delete-success\", { username }));\n  };\n\n  return (\n    <SettingSection\n      title={t(\"setting.member.list-title\")}\n      actions={\n        <Button onClick={handleCreateUser}>\n          <PlusIcon className=\"w-4 h-4 mr-2\" />\n          {t(\"common.create\")}\n        </Button>\n      }\n    >\n      <SettingTable\n        columns={[\n          {\n            key: \"username\",\n            header: t(\"common.username\"),\n            render: (_, user: User) => (\n              <span className=\"text-foreground\">\n                {user.username}\n                {user.state === State.ARCHIVED && <span className=\"ml-2 italic text-muted-foreground\">(Archived)</span>}\n              </span>\n            ),\n          },\n          {\n            key: \"role\",\n            header: t(\"common.role\"),\n            render: (_, user: User) => stringifyUserRole(user.role),\n          },\n          {\n            key: \"displayName\",\n            header: t(\"common.nickname\"),\n            render: (_, user: User) => user.displayName,\n          },\n          {\n            key: \"email\",\n            header: t(\"common.email\"),\n            render: (_, user: User) => user.email,\n          },\n          {\n            key: \"actions\",\n            header: \"\",\n            className: \"text-right\",\n            render: (_, user: User) =>\n              currentUser?.name === user.name ? (\n                <span className=\"text-muted-foreground\">{t(\"common.yourself\")}</span>\n              ) : (\n                <DropdownMenu>\n                  <DropdownMenuTrigger asChild>\n                    <Button variant=\"outline\" size=\"sm\">\n                      <MoreVerticalIcon className=\"w-4 h-auto\" />\n                    </Button>\n                  </DropdownMenuTrigger>\n                  <DropdownMenuContent align=\"end\" sideOffset={2}>\n                    <DropdownMenuItem onClick={() => handleEditUser(user)}>{t(\"common.update\")}</DropdownMenuItem>\n                    {user.state === State.NORMAL ? (\n                      <DropdownMenuItem onClick={() => handleArchiveUserClick(user)}>{t(\"setting.member.archive-member\")}</DropdownMenuItem>\n                    ) : (\n                      <>\n                        <DropdownMenuItem onClick={() => handleRestoreUserClick(user)}>{t(\"common.restore\")}</DropdownMenuItem>\n                        <DropdownMenuItem onClick={() => handleDeleteUserClick(user)} className=\"text-destructive focus:text-destructive\">\n                          {t(\"setting.member.delete-member\")}\n                        </DropdownMenuItem>\n                      </>\n                    )}\n                  </DropdownMenuContent>\n                </DropdownMenu>\n              ),\n          },\n        ]}\n        data={sortedUsers}\n        emptyMessage=\"No members found\"\n        getRowKey={(user) => user.name}\n      />\n\n      {/* Create User Dialog */}\n      <CreateUserDialog open={createDialog.isOpen} onOpenChange={createDialog.setOpen} onSuccess={refetchUsers} />\n\n      {/* Edit User Dialog */}\n      <CreateUserDialog open={editDialog.isOpen} onOpenChange={editDialog.setOpen} user={editingUser} onSuccess={refetchUsers} />\n\n      <ConfirmDialog\n        open={!!archiveTarget}\n        onOpenChange={(open) => !open && setArchiveTarget(undefined)}\n        title={archiveTarget ? t(\"setting.member.archive-warning\", { username: archiveTarget.username }) : \"\"}\n        description={archiveTarget ? t(\"setting.member.archive-warning-description\") : \"\"}\n        confirmLabel={t(\"common.confirm\")}\n        cancelLabel={t(\"common.cancel\")}\n        onConfirm={confirmArchiveUser}\n        confirmVariant=\"default\"\n      />\n\n      <ConfirmDialog\n        open={!!deleteTarget}\n        onOpenChange={(open) => !open && setDeleteTarget(undefined)}\n        title={deleteTarget ? t(\"setting.member.delete-warning\", { username: deleteTarget.username }) : \"\"}\n        description={deleteTarget ? t(\"setting.member.delete-warning-description\") : \"\"}\n        confirmLabel={t(\"common.delete\")}\n        cancelLabel={t(\"common.cancel\")}\n        onConfirm={confirmDeleteUser}\n        confirmVariant=\"destructive\"\n      />\n    </SettingSection>\n  );\n};\n\nexport default MemberSection;\n"
  },
  {
    "path": "web/src/components/Settings/MemoRelatedSettings.tsx",
    "content": "import { create } from \"@bufbuild/protobuf\";\nimport { isEqual, uniq } from \"lodash-es\";\nimport { CheckIcon, X } from \"lucide-react\";\nimport { useState } from \"react\";\nimport { toast } from \"react-hot-toast\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { Switch } from \"@/components/ui/switch\";\nimport { useInstance } from \"@/contexts/InstanceContext\";\nimport { handleError } from \"@/lib/error\";\nimport {\n  InstanceSetting_Key,\n  InstanceSetting_MemoRelatedSetting,\n  InstanceSetting_MemoRelatedSettingSchema,\n  InstanceSettingSchema,\n} from \"@/types/proto/api/v1/instance_service_pb\";\nimport { useTranslate } from \"@/utils/i18n\";\nimport SettingGroup from \"./SettingGroup\";\nimport SettingRow from \"./SettingRow\";\nimport SettingSection from \"./SettingSection\";\n\nconst MemoRelatedSettings = () => {\n  const t = useTranslate();\n  const { memoRelatedSetting: originalSetting, updateSetting, fetchSetting } = useInstance();\n  const [memoRelatedSetting, setMemoRelatedSetting] = useState<InstanceSetting_MemoRelatedSetting>(originalSetting);\n  const [editingReaction, setEditingReaction] = useState<string>(\"\");\n\n  const updatePartialSetting = (partial: Partial<InstanceSetting_MemoRelatedSetting>) => {\n    const newInstanceMemoRelatedSetting = create(InstanceSetting_MemoRelatedSettingSchema, {\n      ...memoRelatedSetting,\n      ...partial,\n    });\n    setMemoRelatedSetting(newInstanceMemoRelatedSetting);\n  };\n\n  const upsertReaction = () => {\n    if (!editingReaction) {\n      return;\n    }\n\n    updatePartialSetting({ reactions: uniq([...memoRelatedSetting.reactions, editingReaction.trim()]) });\n    setEditingReaction(\"\");\n  };\n\n  const handleUpdateSetting = async () => {\n    if (memoRelatedSetting.reactions.length === 0) {\n      toast.error(\"Reactions must not be empty.\");\n      return;\n    }\n\n    try {\n      await updateSetting(\n        create(InstanceSettingSchema, {\n          name: `instance/settings/${InstanceSetting_Key[InstanceSetting_Key.MEMO_RELATED]}`,\n          value: {\n            case: \"memoRelatedSetting\",\n            value: memoRelatedSetting,\n          },\n        }),\n      );\n      await fetchSetting(InstanceSetting_Key.MEMO_RELATED);\n      toast.success(t(\"message.update-succeed\"));\n    } catch (error: unknown) {\n      await handleError(error, toast.error, {\n        context: \"Update memo-related settings\",\n      });\n    }\n  };\n\n  return (\n    <SettingSection>\n      <SettingGroup title={t(\"setting.memo.title\")}>\n        <SettingRow label={t(\"setting.system.display-with-updated-time\")}>\n          <Switch\n            checked={memoRelatedSetting.displayWithUpdateTime}\n            onCheckedChange={(checked) => updatePartialSetting({ displayWithUpdateTime: checked })}\n          />\n        </SettingRow>\n\n        <SettingRow label={t(\"setting.system.enable-double-click-to-edit\")}>\n          <Switch\n            checked={memoRelatedSetting.enableDoubleClickEdit}\n            onCheckedChange={(checked) => updatePartialSetting({ enableDoubleClickEdit: checked })}\n          />\n        </SettingRow>\n\n        <SettingRow label={t(\"setting.memo.content-length-limit\")}>\n          <Input\n            className=\"w-24\"\n            type=\"number\"\n            defaultValue={memoRelatedSetting.contentLengthLimit}\n            onBlur={(event) => updatePartialSetting({ contentLengthLimit: Number(event.target.value) })}\n          />\n        </SettingRow>\n      </SettingGroup>\n\n      <SettingGroup title={t(\"setting.memo.reactions\")} showSeparator>\n        <div className=\"w-full flex flex-row flex-wrap gap-2\">\n          {memoRelatedSetting.reactions.map((reactionType) => (\n            <Badge key={reactionType} variant=\"outline\" className=\"flex items-center gap-1.5 h-8 px-3\">\n              {reactionType}\n              <span\n                className=\"cursor-pointer text-muted-foreground hover:text-destructive\"\n                onClick={() => updatePartialSetting({ reactions: memoRelatedSetting.reactions.filter((r) => r !== reactionType) })}\n              >\n                <X className=\"w-3.5 h-3.5\" />\n              </span>\n            </Badge>\n          ))}\n          <div className=\"flex items-center gap-1.5\">\n            <Input\n              className=\"w-32 h-8\"\n              placeholder={t(\"common.input\")}\n              value={editingReaction}\n              onChange={(event) => setEditingReaction(event.target.value.trim())}\n              onKeyDown={(e) => e.key === \"Enter\" && upsertReaction()}\n            />\n            <Button variant=\"ghost\" size=\"sm\" onClick={upsertReaction} className=\"h-8 w-8 p-0\">\n              <CheckIcon className=\"w-4 h-4\" />\n            </Button>\n          </div>\n        </div>\n      </SettingGroup>\n\n      <div className=\"w-full flex justify-end\">\n        <Button disabled={isEqual(memoRelatedSetting, originalSetting)} onClick={handleUpdateSetting}>\n          {t(\"common.save\")}\n        </Button>\n      </div>\n    </SettingSection>\n  );\n};\n\nexport default MemoRelatedSettings;\n"
  },
  {
    "path": "web/src/components/Settings/MyAccountSection.tsx",
    "content": "import { MoreVerticalIcon, PenLineIcon } from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport useCurrentUser from \"@/hooks/useCurrentUser\";\nimport { useDialog } from \"@/hooks/useDialog\";\nimport { useTranslate } from \"@/utils/i18n\";\nimport ChangeMemberPasswordDialog from \"../ChangeMemberPasswordDialog\";\nimport UpdateAccountDialog from \"../UpdateAccountDialog\";\nimport UserAvatar from \"../UserAvatar\";\nimport { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from \"../ui/dropdown-menu\";\nimport AccessTokenSection from \"./AccessTokenSection\";\nimport SettingGroup from \"./SettingGroup\";\nimport SettingSection from \"./SettingSection\";\n\nconst MyAccountSection = () => {\n  const t = useTranslate();\n  const user = useCurrentUser();\n  const accountDialog = useDialog();\n  const passwordDialog = useDialog();\n\n  const handleEditAccount = () => {\n    accountDialog.open();\n  };\n\n  const handleChangePassword = () => {\n    passwordDialog.open();\n  };\n\n  return (\n    <SettingSection>\n      <SettingGroup title={t(\"setting.account.title\")}>\n        <div className=\"w-full flex flex-row justify-start items-center gap-3\">\n          <UserAvatar className=\"shrink-0 w-12 h-12\" avatarUrl={user?.avatarUrl} />\n          <div className=\"flex-1 min-w-0 flex flex-col justify-center items-start gap-1\">\n            <div className=\"w-full\">\n              <span className=\"text-lg font-semibold\">{user?.displayName}</span>\n              <span className=\"ml-2 text-sm text-muted-foreground\">@{user?.username}</span>\n            </div>\n            {user?.description && <p className=\"w-full text-sm text-muted-foreground truncate\">{user?.description}</p>}\n          </div>\n          <div className=\"flex items-center gap-2 shrink-0\">\n            <Button variant=\"outline\" size=\"sm\" onClick={handleEditAccount}>\n              <PenLineIcon className=\"w-4 h-4 mr-1.5\" />\n              {t(\"common.edit\")}\n            </Button>\n            <DropdownMenu>\n              <DropdownMenuTrigger asChild>\n                <Button variant=\"outline\" size=\"sm\">\n                  <MoreVerticalIcon className=\"w-4 h-4\" />\n                </Button>\n              </DropdownMenuTrigger>\n              <DropdownMenuContent align=\"end\">\n                <DropdownMenuItem onClick={handleChangePassword}>{t(\"setting.account.change-password\")}</DropdownMenuItem>\n              </DropdownMenuContent>\n            </DropdownMenu>\n          </div>\n        </div>\n      </SettingGroup>\n\n      <SettingGroup showSeparator>\n        <AccessTokenSection />\n      </SettingGroup>\n\n      {/* Update Account Dialog */}\n      <UpdateAccountDialog open={accountDialog.isOpen} onOpenChange={accountDialog.setOpen} />\n\n      {/* Change Password Dialog */}\n      <ChangeMemberPasswordDialog open={passwordDialog.isOpen} onOpenChange={passwordDialog.setOpen} user={user} />\n    </SettingSection>\n  );\n};\n\nexport default MyAccountSection;\n"
  },
  {
    "path": "web/src/components/Settings/PreferencesSection.tsx",
    "content": "import { create } from \"@bufbuild/protobuf\";\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from \"@/components/ui/select\";\nimport { useAuth } from \"@/contexts/AuthContext\";\nimport { useUpdateUserGeneralSetting } from \"@/hooks/useUserQueries\";\nimport { Visibility } from \"@/types/proto/api/v1/memo_service_pb\";\nimport { UserSetting_GeneralSetting, UserSetting_GeneralSettingSchema } from \"@/types/proto/api/v1/user_service_pb\";\nimport { loadLocale, useTranslate } from \"@/utils/i18n\";\nimport { convertVisibilityFromString, convertVisibilityToString } from \"@/utils/memo\";\nimport { loadTheme } from \"@/utils/theme\";\nimport LocaleSelect from \"../LocaleSelect\";\nimport ThemeSelect from \"../ThemeSelect\";\nimport VisibilityIcon from \"../VisibilityIcon\";\nimport SettingGroup from \"./SettingGroup\";\nimport SettingRow from \"./SettingRow\";\nimport SettingSection from \"./SettingSection\";\nimport WebhookSection from \"./WebhookSection\";\n\nconst PreferencesSection = () => {\n  const t = useTranslate();\n  const { currentUser, userGeneralSetting: generalSetting, refetchSettings } = useAuth();\n  const { mutate: updateUserGeneralSetting } = useUpdateUserGeneralSetting(currentUser?.name);\n\n  const handleLocaleSelectChange = async (locale: Locale) => {\n    // Apply locale immediately for instant UI feedback and persist to localStorage\n    loadLocale(locale);\n    // Persist to user settings\n    updateUserGeneralSetting(\n      { generalSetting: { locale }, updateMask: [\"locale\"] },\n      {\n        onSuccess: () => {\n          refetchSettings();\n        },\n      },\n    );\n  };\n\n  const handleDefaultMemoVisibilityChanged = (value: string) => {\n    updateUserGeneralSetting(\n      { generalSetting: { memoVisibility: value }, updateMask: [\"memo_visibility\"] },\n      {\n        onSuccess: () => {\n          refetchSettings();\n        },\n      },\n    );\n  };\n\n  const handleThemeChange = async (theme: string) => {\n    // Apply theme immediately for instant UI feedback\n    loadTheme(theme);\n    // Persist to user settings\n    updateUserGeneralSetting(\n      { generalSetting: { theme }, updateMask: [\"theme\"] },\n      {\n        onSuccess: () => {\n          refetchSettings();\n        },\n      },\n    );\n  };\n\n  // Provide default values if setting is not loaded yet\n  const setting: UserSetting_GeneralSetting =\n    generalSetting ||\n    create(UserSetting_GeneralSettingSchema, {\n      locale: \"en\",\n      memoVisibility: \"PRIVATE\",\n      theme: \"system\",\n    });\n\n  return (\n    <SettingSection>\n      <SettingGroup title={t(\"common.basic\")}>\n        <SettingRow label={t(\"common.language\")}>\n          <LocaleSelect value={setting.locale} onChange={handleLocaleSelectChange} />\n        </SettingRow>\n\n        <SettingRow label={t(\"setting.preference.theme\")}>\n          <ThemeSelect value={setting.theme} onValueChange={handleThemeChange} />\n        </SettingRow>\n      </SettingGroup>\n\n      <SettingGroup title={t(\"setting.preference.label\")} showSeparator>\n        <SettingRow label={t(\"setting.preference.default-memo-visibility\")}>\n          <Select value={setting.memoVisibility || \"PRIVATE\"} onValueChange={handleDefaultMemoVisibilityChanged}>\n            <SelectTrigger className=\"min-w-fit\">\n              <div className=\"flex items-center gap-2\">\n                <VisibilityIcon visibility={convertVisibilityFromString(setting.memoVisibility)} />\n                <SelectValue />\n              </div>\n            </SelectTrigger>\n            <SelectContent>\n              {[Visibility.PRIVATE, Visibility.PROTECTED, Visibility.PUBLIC]\n                .map((v) => convertVisibilityToString(v))\n                .map((item) => (\n                  <SelectItem key={item} value={item} className=\"whitespace-nowrap\">\n                    {t(`memo.visibility.${item.toLowerCase() as Lowercase<typeof item>}`)}\n                  </SelectItem>\n                ))}\n            </SelectContent>\n          </Select>\n        </SettingRow>\n      </SettingGroup>\n\n      <SettingGroup showSeparator>\n        <WebhookSection />\n      </SettingGroup>\n    </SettingSection>\n  );\n};\n\nexport default PreferencesSection;\n"
  },
  {
    "path": "web/src/components/Settings/SSOSection.tsx",
    "content": "import { MoreVerticalIcon, PlusIcon } from \"lucide-react\";\nimport { useEffect, useState } from \"react\";\nimport { toast } from \"react-hot-toast\";\nimport ConfirmDialog from \"@/components/ConfirmDialog\";\nimport { Button } from \"@/components/ui/button\";\nimport { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from \"@/components/ui/dropdown-menu\";\nimport { identityProviderServiceClient } from \"@/connect\";\nimport { handleError } from \"@/lib/error\";\nimport { IdentityProvider } from \"@/types/proto/api/v1/idp_service_pb\";\nimport { useTranslate } from \"@/utils/i18n\";\nimport CreateIdentityProviderDialog from \"../CreateIdentityProviderDialog\";\nimport LearnMore from \"../LearnMore\";\nimport SettingSection from \"./SettingSection\";\nimport SettingTable from \"./SettingTable\";\n\nconst SSOSection = () => {\n  const t = useTranslate();\n  const [identityProviderList, setIdentityProviderList] = useState<IdentityProvider[]>([]);\n  const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);\n  const [editingIdentityProvider, setEditingIdentityProvider] = useState<IdentityProvider | undefined>();\n  const [deleteTarget, setDeleteTarget] = useState<IdentityProvider | undefined>(undefined);\n\n  useEffect(() => {\n    fetchIdentityProviderList();\n  }, []);\n\n  const fetchIdentityProviderList = async () => {\n    const { identityProviders } = await identityProviderServiceClient.listIdentityProviders({});\n    setIdentityProviderList(identityProviders);\n  };\n\n  const handleDeleteIdentityProvider = async (identityProvider: IdentityProvider) => {\n    setDeleteTarget(identityProvider);\n  };\n\n  const confirmDeleteIdentityProvider = async () => {\n    if (!deleteTarget) return;\n    try {\n      await identityProviderServiceClient.deleteIdentityProvider({ name: deleteTarget.name });\n    } catch (error: unknown) {\n      handleError(error, toast.error, {\n        context: \"Delete identity provider\",\n      });\n    }\n    await fetchIdentityProviderList();\n    setDeleteTarget(undefined);\n  };\n\n  const handleCreateIdentityProvider = () => {\n    setEditingIdentityProvider(undefined);\n    setIsCreateDialogOpen(true);\n  };\n\n  const handleEditIdentityProvider = (identityProvider: IdentityProvider) => {\n    setEditingIdentityProvider(identityProvider);\n    setIsCreateDialogOpen(true);\n  };\n\n  const handleDialogSuccess = async () => {\n    await fetchIdentityProviderList();\n    setIsCreateDialogOpen(false);\n    setEditingIdentityProvider(undefined);\n  };\n\n  const handleDialogOpenChange = (open: boolean) => {\n    setIsCreateDialogOpen(open);\n    // Clear editing state when dialog is closed\n    if (!open) {\n      setEditingIdentityProvider(undefined);\n    }\n  };\n\n  return (\n    <SettingSection\n      title={\n        <div className=\"flex items-center gap-2\">\n          <span>{t(\"setting.sso.sso-list\")}</span>\n          <LearnMore url=\"https://usememos.com/docs/configuration/authentication\" />\n        </div>\n      }\n      actions={\n        <Button onClick={handleCreateIdentityProvider}>\n          <PlusIcon className=\"w-4 h-4 mr-2\" />\n          {t(\"common.create\")}\n        </Button>\n      }\n    >\n      <SettingTable\n        columns={[\n          {\n            key: \"title\",\n            header: t(\"common.name\"),\n            render: (_, provider: IdentityProvider) => (\n              <span className=\"text-foreground\">\n                {provider.title}\n                <span className=\"ml-2 text-sm text-muted-foreground\">({provider.type})</span>\n              </span>\n            ),\n          },\n          {\n            key: \"actions\",\n            header: \"\",\n            className: \"text-right\",\n            render: (_, provider: IdentityProvider) => (\n              <DropdownMenu>\n                <DropdownMenuTrigger asChild>\n                  <Button variant=\"outline\" size=\"sm\">\n                    <MoreVerticalIcon className=\"w-4 h-auto\" />\n                  </Button>\n                </DropdownMenuTrigger>\n                <DropdownMenuContent align=\"end\" sideOffset={2}>\n                  <DropdownMenuItem onClick={() => handleEditIdentityProvider(provider)}>{t(\"common.edit\")}</DropdownMenuItem>\n                  <DropdownMenuItem\n                    onClick={() => handleDeleteIdentityProvider(provider)}\n                    className=\"text-destructive focus:text-destructive\"\n                  >\n                    {t(\"common.delete\")}\n                  </DropdownMenuItem>\n                </DropdownMenuContent>\n              </DropdownMenu>\n            ),\n          },\n        ]}\n        data={identityProviderList}\n        emptyMessage={t(\"setting.sso.no-sso-found\")}\n        getRowKey={(provider) => provider.name}\n      />\n\n      <CreateIdentityProviderDialog\n        open={isCreateDialogOpen}\n        onOpenChange={handleDialogOpenChange}\n        identityProvider={editingIdentityProvider}\n        onSuccess={handleDialogSuccess}\n      />\n\n      <ConfirmDialog\n        open={!!deleteTarget}\n        onOpenChange={(open) => !open && setDeleteTarget(undefined)}\n        title={deleteTarget ? t(\"setting.sso.confirm-delete\", { name: deleteTarget.title }) : \"\"}\n        confirmLabel={t(\"common.delete\")}\n        cancelLabel={t(\"common.cancel\")}\n        onConfirm={confirmDeleteIdentityProvider}\n        confirmVariant=\"destructive\"\n      />\n    </SettingSection>\n  );\n};\n\nexport default SSOSection;\n"
  },
  {
    "path": "web/src/components/Settings/SectionMenuItem.tsx",
    "content": "import { LucideIcon } from \"lucide-react\";\nimport React from \"react\";\n\ninterface SettingMenuItemProps {\n  text: string;\n  icon: LucideIcon;\n  isSelected: boolean;\n  onClick: () => void;\n}\n\nconst SectionMenuItem: React.FC<SettingMenuItemProps> = ({ text, icon: IconComponent, isSelected, onClick }) => {\n  return (\n    <div\n      onClick={onClick}\n      className={`w-auto max-w-full px-3 leading-8 flex flex-row justify-start items-center cursor-pointer rounded-lg select-none hover:opacity-80 ${\n        isSelected ? \"bg-accent shadow\" : \"\"\n      }`}\n    >\n      <IconComponent className=\"w-4 h-auto mr-2 opacity-80 shrink-0\" />\n      <span className=\"truncate\">{text}</span>\n    </div>\n  );\n};\n\nexport default SectionMenuItem;\n"
  },
  {
    "path": "web/src/components/Settings/SettingGroup.tsx",
    "content": "import React from \"react\";\nimport { Separator } from \"@/components/ui/separator\";\nimport { cn } from \"@/lib/utils\";\n\ninterface SettingGroupProps {\n  title?: string;\n  description?: string;\n  children: React.ReactNode;\n  className?: string;\n  showSeparator?: boolean;\n}\n\nconst SettingGroup: React.FC<SettingGroupProps> = ({ title, description, children, className, showSeparator = false }) => {\n  return (\n    <>\n      {showSeparator && <Separator className=\"my-2\" />}\n      <div className={cn(\"flex flex-col gap-3\", className)}>\n        {(title || description) && (\n          <div className=\"flex flex-col gap-1\">\n            {title && <h4 className=\"text-sm font-medium text-muted-foreground\">{title}</h4>}\n            {description && <p className=\"text-xs text-muted-foreground\">{description}</p>}\n          </div>\n        )}\n        <div className=\"flex flex-col gap-3\">{children}</div>\n      </div>\n    </>\n  );\n};\n\nexport default SettingGroup;\n"
  },
  {
    "path": "web/src/components/Settings/SettingRow.tsx",
    "content": "import { HelpCircleIcon } from \"lucide-react\";\nimport React from \"react\";\nimport { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from \"@/components/ui/tooltip\";\nimport { cn } from \"@/lib/utils\";\n\ninterface SettingRowProps {\n  label: string;\n  description?: string;\n  tooltip?: string;\n  children: React.ReactNode;\n  className?: string;\n  vertical?: boolean;\n}\n\nconst SettingRow: React.FC<SettingRowProps> = ({ label, description, tooltip, children, className, vertical = false }) => {\n  return (\n    <div className={cn(\"w-full flex gap-3\", vertical ? \"flex-col\" : \"flex-row justify-between items-center\", className)}>\n      <div className={cn(\"flex flex-col gap-1\", vertical ? \"w-full\" : \"flex-1 min-w-0\")}>\n        <div className=\"flex items-center gap-1.5\">\n          <span className={cn(\"text-sm\", vertical ? \"font-medium\" : \"\")}>{label}</span>\n          {tooltip && (\n            <TooltipProvider>\n              <Tooltip>\n                <TooltipTrigger asChild>\n                  <HelpCircleIcon className=\"w-4 h-4 text-muted-foreground cursor-help\" />\n                </TooltipTrigger>\n                <TooltipContent>\n                  <p className=\"max-w-xs\">{tooltip}</p>\n                </TooltipContent>\n              </Tooltip>\n            </TooltipProvider>\n          )}\n        </div>\n        {description && <p className=\"text-xs text-muted-foreground\">{description}</p>}\n      </div>\n      <div className={cn(\"flex items-center\", vertical ? \"w-full\" : \"shrink-0\")}>{children}</div>\n    </div>\n  );\n};\n\nexport default SettingRow;\n"
  },
  {
    "path": "web/src/components/Settings/SettingSection.tsx",
    "content": "import React from \"react\";\nimport { cn } from \"@/lib/utils\";\n\ninterface SettingSectionProps {\n  title?: React.ReactNode;\n  description?: string;\n  children: React.ReactNode;\n  className?: string;\n  actions?: React.ReactNode;\n}\n\nconst SettingSection: React.FC<SettingSectionProps> = ({ title, description, children, className, actions }) => {\n  return (\n    <div className={cn(\"w-full flex flex-col gap-4 pt-2 pb-4\", className)}>\n      {(title || description || actions) && (\n        <div className=\"flex flex-col sm:flex-row sm:items-start sm:justify-between gap-2\">\n          <div className=\"flex-1\">\n            {title && (\n              <div className=\"text-base font-semibold text-foreground mb-1\">{typeof title === \"string\" ? <h3>{title}</h3> : title}</div>\n            )}\n            {description && <p className=\"text-sm text-muted-foreground\">{description}</p>}\n          </div>\n          {actions && <div className=\"flex items-center gap-2\">{actions}</div>}\n        </div>\n      )}\n      <div className=\"flex flex-col gap-4\">{children}</div>\n    </div>\n  );\n};\n\nexport default SettingSection;\n"
  },
  {
    "path": "web/src/components/Settings/SettingTable.tsx",
    "content": "import React from \"react\";\nimport { cn } from \"@/lib/utils\";\n\ninterface SettingTableColumn<T = Record<string, unknown>> {\n  key: string;\n  header: string;\n  className?: string;\n  render?: (value: T[keyof T], row: T) => React.ReactNode;\n}\n\ninterface SettingTableProps<T = Record<string, unknown>> {\n  columns: SettingTableColumn<T>[];\n  data: T[];\n  emptyMessage?: string;\n  className?: string;\n  getRowKey?: (row: T, index: number) => string;\n}\n\nconst SettingTable = <T extends Record<string, unknown>>({\n  columns,\n  data,\n  emptyMessage = \"No data\",\n  className,\n  getRowKey,\n}: SettingTableProps<T>) => {\n  return (\n    <div className={cn(\"w-full overflow-x-auto\", className)}>\n      <div className=\"inline-block min-w-full align-middle border border-border rounded-lg\">\n        <table className=\"min-w-full divide-y divide-border\">\n          <thead>\n            <tr className=\"text-sm font-semibold text-left text-foreground\">\n              {columns.map((column) => (\n                <th key={column.key} scope=\"col\" className={cn(\"px-3 py-2\", column.className)}>\n                  {column.header}\n                </th>\n              ))}\n            </tr>\n          </thead>\n          <tbody className=\"divide-y divide-border\">\n            {data.length === 0 ? (\n              <tr>\n                <td colSpan={columns.length} className=\"px-3 py-4 text-center text-sm text-muted-foreground\">\n                  {emptyMessage}\n                </td>\n              </tr>\n            ) : (\n              data.map((row, rowIndex) => {\n                const rowKey = getRowKey ? getRowKey(row, rowIndex) : rowIndex.toString();\n                return (\n                  <tr key={rowKey}>\n                    {columns.map((column) => {\n                      const value = row[column.key as keyof T] as T[keyof T];\n                      const content = column.render ? column.render(value, row) : (value as React.ReactNode);\n                      return (\n                        <td key={column.key} className={cn(\"whitespace-nowrap px-3 py-2 text-sm text-muted-foreground\", column.className)}>\n                          {content}\n                        </td>\n                      );\n                    })}\n                  </tr>\n                );\n              })\n            )}\n          </tbody>\n        </table>\n      </div>\n    </div>\n  );\n};\n\nexport default SettingTable;\n"
  },
  {
    "path": "web/src/components/Settings/StorageSection.tsx",
    "content": "import { create } from \"@bufbuild/protobuf\";\nimport { isEqual } from \"lodash-es\";\nimport React, { useEffect, useMemo, useState } from \"react\";\nimport { toast } from \"react-hot-toast\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport { RadioGroup, RadioGroupItem } from \"@/components/ui/radio-group\";\nimport { Switch } from \"@/components/ui/switch\";\nimport { useInstance } from \"@/contexts/InstanceContext\";\nimport { handleError } from \"@/lib/error\";\nimport {\n  InstanceSetting_Key,\n  InstanceSetting_StorageSetting,\n  InstanceSetting_StorageSetting_S3Config,\n  InstanceSetting_StorageSetting_S3ConfigSchema,\n  InstanceSetting_StorageSetting_StorageType,\n  InstanceSetting_StorageSettingSchema,\n  InstanceSettingSchema,\n} from \"@/types/proto/api/v1/instance_service_pb\";\nimport { useTranslate } from \"@/utils/i18n\";\nimport SettingGroup from \"./SettingGroup\";\nimport SettingRow from \"./SettingRow\";\nimport SettingSection from \"./SettingSection\";\n\nconst StorageSection = () => {\n  const t = useTranslate();\n  const { storageSetting: originalSetting, updateSetting, fetchSetting } = useInstance();\n  const [instanceStorageSetting, setInstanceStorageSetting] = useState<InstanceSetting_StorageSetting>(originalSetting);\n\n  useEffect(() => {\n    setInstanceStorageSetting(originalSetting);\n  }, [originalSetting]);\n\n  const allowSaveStorageSetting = useMemo(() => {\n    if (instanceStorageSetting.uploadSizeLimitMb <= 0) {\n      return false;\n    }\n\n    if (instanceStorageSetting.storageType === InstanceSetting_StorageSetting_StorageType.LOCAL) {\n      if (instanceStorageSetting.filepathTemplate.length === 0) {\n        return false;\n      }\n    } else if (instanceStorageSetting.storageType === InstanceSetting_StorageSetting_StorageType.S3) {\n      if (\n        instanceStorageSetting.s3Config?.accessKeyId.length === 0 ||\n        instanceStorageSetting.s3Config?.accessKeySecret.length === 0 ||\n        instanceStorageSetting.s3Config?.endpoint.length === 0 ||\n        instanceStorageSetting.s3Config?.region.length === 0 ||\n        instanceStorageSetting.s3Config?.bucket.length === 0\n      ) {\n        return false;\n      }\n    }\n    return !isEqual(originalSetting, instanceStorageSetting);\n  }, [instanceStorageSetting, originalSetting]);\n\n  const handleMaxUploadSizeChanged = async (event: React.FocusEvent<HTMLInputElement>) => {\n    let num = parseInt(event.target.value);\n    if (Number.isNaN(num)) {\n      num = 0;\n    }\n    const update = create(InstanceSetting_StorageSettingSchema, {\n      ...instanceStorageSetting,\n      uploadSizeLimitMb: BigInt(num),\n    });\n    setInstanceStorageSetting(update);\n  };\n\n  const handleFilepathTemplateChanged = async (event: React.FocusEvent<HTMLInputElement>) => {\n    const update = create(InstanceSetting_StorageSettingSchema, {\n      ...instanceStorageSetting,\n      filepathTemplate: event.target.value,\n    });\n    setInstanceStorageSetting(update);\n  };\n\n  const handlePartialS3ConfigChanged = async (s3Config: Partial<InstanceSetting_StorageSetting_S3Config>) => {\n    const existingS3Config = instanceStorageSetting.s3Config;\n    const s3ConfigInit = {\n      accessKeyId: existingS3Config?.accessKeyId ?? \"\",\n      accessKeySecret: existingS3Config?.accessKeySecret ?? \"\",\n      endpoint: existingS3Config?.endpoint ?? \"\",\n      region: existingS3Config?.region ?? \"\",\n      bucket: existingS3Config?.bucket ?? \"\",\n      usePathStyle: existingS3Config?.usePathStyle ?? false,\n      ...s3Config,\n    };\n    const update = create(InstanceSetting_StorageSettingSchema, {\n      storageType: instanceStorageSetting.storageType,\n      filepathTemplate: instanceStorageSetting.filepathTemplate,\n      uploadSizeLimitMb: instanceStorageSetting.uploadSizeLimitMb,\n      s3Config: create(InstanceSetting_StorageSetting_S3ConfigSchema, s3ConfigInit),\n    });\n    setInstanceStorageSetting(update);\n  };\n\n  const handleS3ConfigAccessKeyIdChanged = async (event: React.FocusEvent<HTMLInputElement>) => {\n    handlePartialS3ConfigChanged({ accessKeyId: event.target.value });\n  };\n\n  const handleS3ConfigAccessKeySecretChanged = async (event: React.FocusEvent<HTMLInputElement>) => {\n    handlePartialS3ConfigChanged({ accessKeySecret: event.target.value });\n  };\n\n  const handleS3ConfigEndpointChanged = async (event: React.FocusEvent<HTMLInputElement>) => {\n    handlePartialS3ConfigChanged({ endpoint: event.target.value });\n  };\n\n  const handleS3ConfigRegionChanged = async (event: React.FocusEvent<HTMLInputElement>) => {\n    handlePartialS3ConfigChanged({ region: event.target.value });\n  };\n\n  const handleS3ConfigBucketChanged = async (event: React.FocusEvent<HTMLInputElement>) => {\n    handlePartialS3ConfigChanged({ bucket: event.target.value });\n  };\n\n  const handleS3ConfigUsePathStyleChanged = (event: React.ChangeEvent<HTMLInputElement>) => {\n    handlePartialS3ConfigChanged({\n      usePathStyle: event.target.checked,\n    });\n  };\n\n  const handleStorageTypeChanged = async (storageType: InstanceSetting_StorageSetting_StorageType) => {\n    const update = create(InstanceSetting_StorageSettingSchema, {\n      ...instanceStorageSetting,\n      storageType: storageType,\n    });\n    setInstanceStorageSetting(update);\n  };\n\n  const saveInstanceStorageSetting = async () => {\n    try {\n      await updateSetting(\n        create(InstanceSettingSchema, {\n          name: `instance/settings/${InstanceSetting_Key[InstanceSetting_Key.STORAGE]}`,\n          value: {\n            case: \"storageSetting\",\n            value: instanceStorageSetting,\n          },\n        }),\n      );\n      await fetchSetting(InstanceSetting_Key.STORAGE);\n      toast.success(\"Updated\");\n    } catch (error: unknown) {\n      handleError(error, toast.error, {\n        context: \"Update storage settings\",\n      });\n    }\n  };\n\n  return (\n    <SettingSection>\n      <SettingGroup title={t(\"setting.storage.current-storage\")}>\n        <div className=\"w-full\">\n          <RadioGroup\n            value={String(instanceStorageSetting.storageType)}\n            onValueChange={(value) => {\n              handleStorageTypeChanged(Number(value) as InstanceSetting_StorageSetting_StorageType);\n            }}\n            className=\"flex flex-row gap-4\"\n          >\n            <div className=\"flex items-center space-x-2\">\n              <RadioGroupItem value={String(InstanceSetting_StorageSetting_StorageType.DATABASE)} id=\"database\" />\n              <Label htmlFor=\"database\">{t(\"setting.storage.type-database\")}</Label>\n            </div>\n            <div className=\"flex items-center space-x-2\">\n              <RadioGroupItem value={String(InstanceSetting_StorageSetting_StorageType.LOCAL)} id=\"local\" />\n              <Label htmlFor=\"local\">{t(\"setting.storage.type-local\")}</Label>\n            </div>\n            <div className=\"flex items-center space-x-2\">\n              <RadioGroupItem value={String(InstanceSetting_StorageSetting_StorageType.S3)} id=\"s3\" />\n              <Label htmlFor=\"s3\">S3</Label>\n            </div>\n          </RadioGroup>\n        </div>\n\n        <SettingRow label={t(\"setting.system.max-upload-size\")} tooltip={t(\"setting.system.max-upload-size-hint\")}>\n          <Input\n            className=\"w-24 font-mono\"\n            value={String(instanceStorageSetting.uploadSizeLimitMb)}\n            onChange={handleMaxUploadSizeChanged}\n          />\n        </SettingRow>\n\n        {instanceStorageSetting.storageType !== InstanceSetting_StorageSetting_StorageType.DATABASE && (\n          <SettingRow label={t(\"setting.storage.filepath-template\")}>\n            <Input\n              className=\"w-64\"\n              value={instanceStorageSetting.filepathTemplate}\n              placeholder=\"assets/{timestamp}_{filename}\"\n              onChange={handleFilepathTemplateChanged}\n            />\n          </SettingRow>\n        )}\n      </SettingGroup>\n\n      {instanceStorageSetting.storageType === InstanceSetting_StorageSetting_StorageType.S3 && (\n        <SettingGroup title=\"S3 Configuration\" showSeparator>\n          <SettingRow label=\"Access key id\">\n            <Input className=\"w-64\" value={instanceStorageSetting.s3Config?.accessKeyId} onChange={handleS3ConfigAccessKeyIdChanged} />\n          </SettingRow>\n\n          <SettingRow label=\"Access key secret\">\n            <Input\n              className=\"w-64\"\n              type=\"password\"\n              value={instanceStorageSetting.s3Config?.accessKeySecret}\n              onChange={handleS3ConfigAccessKeySecretChanged}\n            />\n          </SettingRow>\n\n          <SettingRow label=\"Endpoint\">\n            <Input className=\"w-64\" value={instanceStorageSetting.s3Config?.endpoint} onChange={handleS3ConfigEndpointChanged} />\n          </SettingRow>\n\n          <SettingRow label=\"Region\">\n            <Input className=\"w-64\" value={instanceStorageSetting.s3Config?.region} onChange={handleS3ConfigRegionChanged} />\n          </SettingRow>\n\n          <SettingRow label=\"Bucket\">\n            <Input className=\"w-64\" value={instanceStorageSetting.s3Config?.bucket} onChange={handleS3ConfigBucketChanged} />\n          </SettingRow>\n\n          <SettingRow label=\"Use Path Style\">\n            <Switch\n              checked={instanceStorageSetting.s3Config?.usePathStyle}\n              onCheckedChange={(checked) =>\n                handleS3ConfigUsePathStyleChanged({ target: { checked } } as React.ChangeEvent<HTMLInputElement> & {\n                  target: { checked: boolean };\n                })\n              }\n            />\n          </SettingRow>\n        </SettingGroup>\n      )}\n\n      <div className=\"w-full flex justify-end\">\n        <Button disabled={!allowSaveStorageSetting} onClick={saveInstanceStorageSetting}>\n          {t(\"common.save\")}\n        </Button>\n      </div>\n    </SettingSection>\n  );\n};\n\nexport default StorageSection;\n"
  },
  {
    "path": "web/src/components/Settings/WebhookSection.tsx",
    "content": "import { ExternalLinkIcon, PlusIcon, TrashIcon } from \"lucide-react\";\nimport { useEffect, useState } from \"react\";\nimport toast from \"react-hot-toast\";\nimport { Link } from \"react-router-dom\";\nimport ConfirmDialog from \"@/components/ConfirmDialog\";\nimport { Button } from \"@/components/ui/button\";\nimport { userServiceClient } from \"@/connect\";\nimport useCurrentUser from \"@/hooks/useCurrentUser\";\nimport { UserWebhook } from \"@/types/proto/api/v1/user_service_pb\";\nimport { useTranslate } from \"@/utils/i18n\";\nimport CreateWebhookDialog from \"../CreateWebhookDialog\";\nimport SettingTable from \"./SettingTable\";\n\nconst WebhookSection = () => {\n  const t = useTranslate();\n  const currentUser = useCurrentUser();\n  const [webhooks, setWebhooks] = useState<UserWebhook[]>([]);\n  const [isCreateWebhookDialogOpen, setIsCreateWebhookDialogOpen] = useState(false);\n  const [deleteTarget, setDeleteTarget] = useState<UserWebhook | undefined>(undefined);\n\n  const listWebhooks = async () => {\n    if (!currentUser) return [];\n    const { webhooks } = await userServiceClient.listUserWebhooks({\n      parent: currentUser.name,\n    });\n    return webhooks;\n  };\n\n  useEffect(() => {\n    listWebhooks().then((webhooks) => {\n      setWebhooks(webhooks);\n    });\n  }, [currentUser]);\n\n  const handleCreateWebhookDialogConfirm = async () => {\n    const webhooks = await listWebhooks();\n    const name = webhooks[webhooks.length - 1]?.displayName || \"\";\n    setWebhooks(webhooks);\n    setIsCreateWebhookDialogOpen(false);\n    toast.success(t(\"setting.webhook.create-dialog.create-webhook-success\", { name }));\n  };\n\n  const handleDeleteWebhook = async (webhook: UserWebhook) => {\n    setDeleteTarget(webhook);\n  };\n\n  const confirmDeleteWebhook = async () => {\n    if (!deleteTarget) return;\n    await userServiceClient.deleteUserWebhook({ name: deleteTarget.name });\n    setWebhooks(webhooks.filter((item) => item.name !== deleteTarget.name));\n    setDeleteTarget(undefined);\n    toast.success(t(\"setting.webhook.delete-dialog.delete-webhook-success\", { name: deleteTarget.displayName }));\n  };\n\n  return (\n    <div className=\"w-full flex flex-col gap-2\">\n      <div className=\"flex flex-col sm:flex-row sm:items-start sm:justify-between gap-2\">\n        <h4 className=\"text-sm font-medium text-muted-foreground\">{t(\"setting.webhook.title\")}</h4>\n        <Button onClick={() => setIsCreateWebhookDialogOpen(true)} size=\"sm\">\n          <PlusIcon className=\"w-4 h-4 mr-1.5\" />\n          {t(\"common.create\")}\n        </Button>\n      </div>\n\n      <SettingTable\n        columns={[\n          {\n            key: \"displayName\",\n            header: t(\"common.name\"),\n            render: (_, webhook: UserWebhook) => <span className=\"text-foreground\">{webhook.displayName}</span>,\n          },\n          {\n            key: \"url\",\n            header: t(\"setting.webhook.url\"),\n            render: (_, webhook: UserWebhook) => (\n              <span className=\"max-w-[300px] inline-block truncate text-foreground\" title={webhook.url}>\n                {webhook.url}\n              </span>\n            ),\n          },\n          {\n            key: \"actions\",\n            header: \"\",\n            className: \"text-right\",\n            render: (_, webhook: UserWebhook) => (\n              <Button variant=\"ghost\" size=\"sm\" onClick={() => handleDeleteWebhook(webhook)}>\n                <TrashIcon className=\"text-destructive w-4 h-auto\" />\n              </Button>\n            ),\n          },\n        ]}\n        data={webhooks}\n        emptyMessage={t(\"setting.webhook.no-webhooks-found\")}\n        getRowKey={(webhook) => webhook.name}\n      />\n\n      <div className=\"w-full\">\n        <Link\n          className=\"text-muted-foreground text-sm inline-flex items-center hover:underline hover:text-primary\"\n          to=\"https://usememos.com/docs/integrations/webhooks\"\n          target=\"_blank\"\n        >\n          {t(\"common.learn-more\")}\n          <ExternalLinkIcon className=\"w-4 h-4 ml-1\" />\n        </Link>\n      </div>\n\n      <CreateWebhookDialog\n        open={isCreateWebhookDialogOpen}\n        onOpenChange={setIsCreateWebhookDialogOpen}\n        onSuccess={handleCreateWebhookDialogConfirm}\n      />\n      <ConfirmDialog\n        open={!!deleteTarget}\n        onOpenChange={(open) => !open && setDeleteTarget(undefined)}\n        title={t(\"setting.webhook.delete-dialog.delete-webhook-title\", { name: deleteTarget?.displayName || \"\" })}\n        description={t(\"setting.webhook.delete-dialog.delete-webhook-description\")}\n        confirmLabel={t(\"common.delete\")}\n        cancelLabel={t(\"common.cancel\")}\n        onConfirm={confirmDeleteWebhook}\n        confirmVariant=\"destructive\"\n      />\n    </div>\n  );\n};\n\nexport default WebhookSection;\n"
  },
  {
    "path": "web/src/components/Skeleton.tsx",
    "content": "import { cn } from \"@/lib/utils\";\n\ninterface SkeletonProps {\n  showCreator?: boolean;\n  count?: number;\n}\n\nconst skeletonBase = \"bg-muted/70 rounded animate-pulse\";\n\nconst MemoCardSkeleton = ({ showCreator, index }: { showCreator?: boolean; index: number }) => (\n  <div className=\"relative flex flex-col bg-card w-full px-4 py-3 mb-2 gap-2 rounded-lg border border-border\">\n    <div className=\"w-full flex justify-between items-center gap-2\">\n      <div className=\"grow flex items-center max-w-[calc(100%-8rem)]\">\n        {showCreator ? (\n          <div className=\"w-full flex items-center gap-2\">\n            <div className={cn(\"w-8 h-8 rounded-full shrink-0\", skeletonBase)} />\n            <div className=\"flex flex-col gap-1\">\n              <div className={cn(\"h-4 w-24\", skeletonBase)} />\n              <div className={cn(\"h-3 w-16\", skeletonBase)} />\n            </div>\n          </div>\n        ) : (\n          <div className={cn(\"h-4 w-32\", skeletonBase)} />\n        )}\n      </div>\n      <div className=\"flex gap-2\">\n        <div className={cn(\"w-4 h-4\", skeletonBase)} />\n        <div className={cn(\"w-4 h-4\", skeletonBase)} />\n      </div>\n    </div>\n    <div className=\"space-y-2\">\n      <div className={cn(\"h-4\", skeletonBase, index % 3 === 0 ? \"w-full\" : index % 3 === 1 ? \"w-4/5\" : \"w-5/6\")} />\n      <div className={cn(\"h-4\", skeletonBase, index % 2 === 0 ? \"w-3/4\" : \"w-4/5\")} />\n      {index % 2 === 0 && <div className={cn(\"h-4 w-2/3\", skeletonBase)} />}\n    </div>\n  </div>\n);\n\n/**\n * Memo list loading skeleton - shows card structure while loading.\n * Only use for memo lists in PagedMemoList component.\n */\nconst Skeleton = ({ showCreator = false, count = 4 }: SkeletonProps) => (\n  <div className=\"w-full max-w-2xl mx-auto\">\n    {Array.from({ length: count }, (_, i) => (\n      <MemoCardSkeleton key={i} showCreator={showCreator} index={i} />\n    ))}\n  </div>\n);\n\nexport default Skeleton;\n"
  },
  {
    "path": "web/src/components/StatisticsView/MonthNavigator.tsx",
    "content": "import dayjs from \"dayjs\";\nimport { ChevronLeftIcon, ChevronRightIcon } from \"lucide-react\";\nimport { memo, useCallback, useMemo, useState } from \"react\";\nimport { useTranslation } from \"react-i18next\";\nimport { YearCalendar } from \"@/components/ActivityCalendar\";\nimport { Button } from \"@/components/ui/button\";\nimport { Dialog, DialogContent, DialogTitle, DialogTrigger } from \"@/components/ui/dialog\";\nimport { addMonths, formatMonth, getMonthFromDate, getYearFromDate, setYearAndMonth } from \"@/lib/calendar-utils\";\nimport type { MonthNavigatorProps } from \"@/types/statistics\";\n\nexport const MonthNavigator = memo(({ visibleMonth, onMonthChange, activityStats }: MonthNavigatorProps) => {\n  const { i18n } = useTranslation();\n  const [isOpen, setIsOpen] = useState(false);\n\n  const { currentMonth, currentYear, currentMonthNum } = useMemo(\n    () => ({\n      currentMonth: dayjs(visibleMonth).toDate(),\n      currentYear: getYearFromDate(visibleMonth),\n      currentMonthNum: getMonthFromDate(visibleMonth),\n    }),\n    [visibleMonth],\n  );\n\n  const monthLabel = useMemo(\n    () => currentMonth.toLocaleString(i18n.language, { year: \"numeric\", month: \"long\" }),\n    [currentMonth, i18n.language],\n  );\n\n  const handlePrevMonth = useCallback(() => onMonthChange(addMonths(visibleMonth, -1)), [visibleMonth, onMonthChange]);\n  const handleNextMonth = useCallback(() => onMonthChange(addMonths(visibleMonth, 1)), [visibleMonth, onMonthChange]);\n\n  const handleDateClick = useCallback(\n    (date: string) => {\n      onMonthChange(formatMonth(date));\n      setIsOpen(false);\n    },\n    [onMonthChange],\n  );\n\n  const handleYearChange = useCallback(\n    (year: number) => onMonthChange(setYearAndMonth(year, currentMonthNum)),\n    [currentMonthNum, onMonthChange],\n  );\n\n  return (\n    <header className=\"w-full mb-2 flex items-center justify-between gap-2\">\n      <Dialog open={isOpen} onOpenChange={setIsOpen}>\n        <DialogTrigger asChild>\n          <button\n            type=\"button\"\n            className=\"py-0.5 text-sm text-foreground font-medium transition-colors hover:text-foreground/80 select-none\"\n          >\n            {monthLabel}\n          </button>\n        </DialogTrigger>\n        <DialogContent\n          className=\"p-0 border border-border/20 bg-background md:max-w-6xl w-[min(100vw-24px,1200px)] max-h-[85vh] overflow-y-auto rounded-xl shadow-xl\"\n          size=\"2xl\"\n          showCloseButton={false}\n        >\n          <DialogTitle className=\"sr-only\">Select Month</DialogTitle>\n          <YearCalendar selectedYear={currentYear} data={activityStats} onYearChange={handleYearChange} onDateClick={handleDateClick} />\n        </DialogContent>\n      </Dialog>\n\n      <nav className=\"flex items-center shrink-0\" aria-label=\"Month navigation\">\n        <Button\n          variant=\"ghost\"\n          size=\"sm\"\n          onClick={handlePrevMonth}\n          aria-label=\"Previous month\"\n          className=\"h-7 w-7 p-0 rounded-md text-muted-foreground hover:text-foreground hover:bg-muted/40\"\n        >\n          <ChevronLeftIcon className=\"w-4 h-4\" />\n        </Button>\n        <Button\n          variant=\"ghost\"\n          size=\"sm\"\n          onClick={handleNextMonth}\n          aria-label=\"Next month\"\n          className=\"h-7 w-7 p-0 rounded-md text-muted-foreground hover:text-foreground hover:bg-muted/40\"\n        >\n          <ChevronRightIcon className=\"w-4 h-4\" />\n        </Button>\n      </nav>\n    </header>\n  );\n});\n\nMonthNavigator.displayName = \"MonthNavigator\";\n"
  },
  {
    "path": "web/src/components/StatisticsView/StatisticsView.tsx",
    "content": "import dayjs from \"dayjs\";\nimport { useMemo, useState } from \"react\";\nimport { MonthCalendar } from \"@/components/ActivityCalendar\";\nimport { useDateFilterNavigation } from \"@/hooks\";\nimport type { StatisticsData } from \"@/types/statistics\";\nimport { MonthNavigator } from \"./MonthNavigator\";\n\ninterface Props {\n  statisticsData: StatisticsData;\n}\n\nconst StatisticsView = (props: Props) => {\n  const { statisticsData } = props;\n  const { activityStats } = statisticsData;\n  const navigateToDateFilter = useDateFilterNavigation();\n  const [visibleMonthString, setVisibleMonthString] = useState(dayjs().format(\"YYYY-MM\"));\n\n  const maxCount = useMemo(() => {\n    const counts = Object.values(activityStats);\n    return Math.max(...counts, 1);\n  }, [activityStats]);\n\n  return (\n    <div className=\"group w-full mt-2 flex flex-col text-muted-foreground animate-fade-in\">\n      <MonthNavigator visibleMonth={visibleMonthString} onMonthChange={setVisibleMonthString} activityStats={activityStats} />\n\n      <div className=\"w-full animate-scale-in\">\n        <MonthCalendar month={visibleMonthString} data={activityStats} maxCount={maxCount} onClick={navigateToDateFilter} />\n      </div>\n    </div>\n  );\n};\n\nexport default StatisticsView;\n"
  },
  {
    "path": "web/src/components/StatisticsView/index.ts",
    "content": "export { default } from \"./StatisticsView\";\n"
  },
  {
    "path": "web/src/components/TagTree.tsx",
    "content": "import { ChevronRightIcon, HashIcon } from \"lucide-react\";\nimport { useEffect, useState } from \"react\";\nimport useToggle from \"react-use/lib/useToggle\";\nimport { type MemoFilter, useMemoFilterContext } from \"@/contexts/MemoFilterContext\";\n\ninterface Tag {\n  key: string;\n  text: string;\n  amount: number;\n  subTags: Tag[];\n}\n\ninterface Props {\n  tagAmounts: [tag: string, amount: number][];\n  expandSubTags: boolean;\n}\n\nconst TagTree = ({ tagAmounts: rawTagAmounts, expandSubTags }: Props) => {\n  const [tags, setTags] = useState<Tag[]>([]);\n\n  useEffect(() => {\n    const sortedTagAmounts = Array.from(rawTagAmounts).sort();\n    const root: Tag = {\n      key: \"\",\n      text: \"\",\n      amount: 0,\n      subTags: [],\n    };\n\n    for (const tagAmount of sortedTagAmounts) {\n      const subtags = tagAmount[0].split(\"/\");\n      let tempObj = root;\n      let tagText = \"\";\n\n      for (let i = 0; i < subtags.length; i++) {\n        const key = subtags[i];\n        let amount: number = 0;\n\n        if (i === 0) {\n          tagText += key;\n        } else {\n          tagText += \"/\" + key;\n        }\n        if (sortedTagAmounts.some(([tag, amount]) => tag === tagText && amount > 1)) {\n          amount = tagAmount[1];\n        }\n\n        let obj = null;\n\n        for (const t of tempObj.subTags) {\n          if (t.text === tagText) {\n            obj = t;\n            break;\n          }\n        }\n\n        if (!obj) {\n          obj = {\n            key,\n            text: tagText,\n            amount: amount,\n            subTags: [],\n          };\n          tempObj.subTags.push(obj);\n        }\n\n        tempObj = obj;\n      }\n    }\n\n    setTags(root.subTags as Tag[]);\n  }, [rawTagAmounts]);\n\n  return (\n    <div className=\"flex flex-col justify-start items-start relative w-full h-auto flex-nowrap gap-2 mt-1\">\n      {tags.map((t, idx) => (\n        <TagItemContainer key={t.text + \"-\" + idx} tag={t} expandSubTags={expandSubTags} />\n      ))}\n    </div>\n  );\n};\n\ninterface TagItemContainerProps {\n  tag: Tag;\n  expandSubTags: boolean;\n}\n\nconst TagItemContainer = (props: TagItemContainerProps) => {\n  const { tag, expandSubTags } = props;\n  const { getFiltersByFactor, addFilter, removeFilter } = useMemoFilterContext();\n  const tagFilters = getFiltersByFactor(\"tagSearch\");\n  const isActive = tagFilters.some((f: MemoFilter) => f.value === tag.text);\n  const hasSubTags = tag.subTags.length > 0;\n  const [showSubTags, toggleSubTags] = useToggle(false);\n\n  useEffect(() => {\n    toggleSubTags(expandSubTags);\n  }, [expandSubTags]);\n\n  const handleTagClick = () => {\n    if (isActive) {\n      removeFilter((f: MemoFilter) => f.factor === \"tagSearch\" && f.value === tag.text);\n    } else {\n      // Remove all existing tag filters first, then add the new one\n      removeFilter((f: MemoFilter) => f.factor === \"tagSearch\");\n      addFilter({\n        factor: \"tagSearch\",\n        value: tag.text,\n      });\n    }\n  };\n\n  const handleToggleBtnClick = (event: React.MouseEvent) => {\n    event.stopPropagation();\n    toggleSubTags();\n  };\n\n  return (\n    <>\n      <div className=\"relative flex flex-row justify-between items-center w-full leading-6 py-0 mt-px text-sm select-none shrink-0\">\n        <div\n          className={`flex flex-row justify-start items-center truncate shrink leading-5 mr-1 cursor-pointer transition-colors ${\n            isActive ? \"text-primary\" : \"text-muted-foreground\"\n          }`}\n          onClick={handleTagClick}\n        >\n          <HashIcon className=\"w-4 h-auto shrink-0 mr-1\" />\n          <span className={`truncate hover:opacity-80 ${isActive ? \"font-medium\" : \"\"}`}>\n            {tag.key} {tag.amount > 1 && <span className=\"opacity-60\">({tag.amount})</span>}\n          </span>\n        </div>\n        <div className=\"flex flex-row justify-end items-center\">\n          {hasSubTags ? (\n            <span\n              className={`flex flex-row justify-center items-center w-6 h-6 shrink-0 transition-all rotate-0 cursor-pointer ${\n                showSubTags && \"rotate-90\"\n              }`}\n              onClick={handleToggleBtnClick}\n            >\n              <ChevronRightIcon className=\"w-5 h-5 text-muted-foreground hover:text-foreground\" />\n            </span>\n          ) : null}\n        </div>\n      </div>\n      {hasSubTags ? (\n        <div\n          className={`w-[calc(100%-0.5rem)] flex flex-col justify-start items-start h-auto ml-2 pl-2 border-l-2 border-l-border ${\n            !showSubTags && \"hidden\"\n          }`}\n        >\n          {tag.subTags.map((st, idx) => (\n            <TagItemContainer key={st.text + \"-\" + idx} tag={st} expandSubTags={expandSubTags} />\n          ))}\n        </div>\n      ) : null}\n    </>\n  );\n};\n\nexport default TagTree;\n"
  },
  {
    "path": "web/src/components/ThemeSelect.tsx",
    "content": "import { Monitor, Moon, Palette, Sun } from \"lucide-react\";\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from \"@/components/ui/select\";\nimport { loadTheme, THEME_OPTIONS } from \"@/utils/theme\";\n\ninterface ThemeSelectProps {\n  value?: string;\n  onValueChange?: (theme: string) => void;\n  className?: string;\n}\n\nconst THEME_ICONS: Record<string, JSX.Element> = {\n  system: <Monitor className=\"w-4 h-4\" />,\n  default: <Sun className=\"w-4 h-4\" />,\n  \"default-dark\": <Moon className=\"w-4 h-4\" />,\n  paper: <Palette className=\"w-4 h-4\" />,\n};\n\nconst ThemeSelect = ({ value, onValueChange, className }: ThemeSelectProps = {}) => {\n  const currentTheme = value || \"system\";\n\n  const handleThemeChange = (newTheme: string) => {\n    // Apply theme globally immediately\n    loadTheme(newTheme);\n    // Also notify parent component if callback is provided\n    if (onValueChange) {\n      onValueChange(newTheme);\n    }\n  };\n\n  return (\n    <Select value={currentTheme} onValueChange={handleThemeChange}>\n      <SelectTrigger className={className}>\n        <div className=\"flex items-center gap-2\">\n          <SelectValue placeholder=\"Select theme\" />\n        </div>\n      </SelectTrigger>\n      <SelectContent>\n        {THEME_OPTIONS.map((option) => (\n          <SelectItem key={option.value} value={option.value}>\n            <div className=\"flex items-center gap-2\">\n              {THEME_ICONS[option.value]}\n              <span>{option.label}</span>\n            </div>\n          </SelectItem>\n        ))}\n      </SelectContent>\n    </Select>\n  );\n};\n\nexport default ThemeSelect;\n"
  },
  {
    "path": "web/src/components/UpdateAccountDialog.tsx",
    "content": "import { isEqual } from \"lodash-es\";\nimport { XIcon } from \"lucide-react\";\nimport { useState } from \"react\";\nimport { toast } from \"react-hot-toast\";\nimport { Button } from \"@/components/ui/button\";\nimport { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from \"@/components/ui/dialog\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport { Textarea } from \"@/components/ui/textarea\";\nimport { useInstance } from \"@/contexts/InstanceContext\";\nimport { convertFileToBase64 } from \"@/helpers/utils\";\nimport useCurrentUser from \"@/hooks/useCurrentUser\";\nimport { useUpdateUser } from \"@/hooks/useUserQueries\";\nimport { handleError } from \"@/lib/error\";\nimport { useTranslate } from \"@/utils/i18n\";\nimport UserAvatar from \"./UserAvatar\";\n\ninterface Props {\n  open: boolean;\n  onOpenChange: (open: boolean) => void;\n  onSuccess?: () => void;\n}\n\ninterface State {\n  avatarUrl: string;\n  username: string;\n  displayName: string;\n  email: string;\n  description: string;\n}\n\nfunction UpdateAccountDialog({ open, onOpenChange, onSuccess }: Props) {\n  const t = useTranslate();\n  const currentUser = useCurrentUser();\n  const { generalSetting: instanceGeneralSetting } = useInstance();\n  const { mutateAsync: updateUser } = useUpdateUser();\n  const [state, setState] = useState<State>({\n    avatarUrl: currentUser?.avatarUrl ?? \"\",\n    username: currentUser?.username ?? \"\",\n    displayName: currentUser?.displayName ?? \"\",\n    email: currentUser?.email ?? \"\",\n    description: currentUser?.description ?? \"\",\n  });\n\n  const handleCloseBtnClick = () => {\n    onOpenChange(false);\n  };\n\n  const setPartialState = (partialState: Partial<State>) => {\n    setState((state) => {\n      return {\n        ...state,\n        ...partialState,\n      };\n    });\n  };\n\n  const handleAvatarChanged = async (e: React.ChangeEvent<HTMLInputElement>) => {\n    const files = e.target.files;\n    if (files && files.length > 0) {\n      const image = files[0];\n      if (image.size > 2 * 1024 * 1024) {\n        toast.error(\"Max file size is 2MB\");\n        return;\n      }\n      try {\n        const base64 = await convertFileToBase64(image);\n        setPartialState({\n          avatarUrl: base64,\n        });\n      } catch (error) {\n        console.error(error);\n        toast.error(`Failed to convert image to base64`);\n      }\n    }\n  };\n\n  const handleDisplayNameChanged = (e: React.ChangeEvent<HTMLInputElement>) => {\n    setPartialState({\n      displayName: e.target.value as string,\n    });\n  };\n\n  const handleUsernameChanged = (e: React.ChangeEvent<HTMLInputElement>) => {\n    setPartialState({\n      username: e.target.value as string,\n    });\n  };\n\n  const handleEmailChanged = (e: React.ChangeEvent<HTMLInputElement>) => {\n    setState((state) => {\n      return {\n        ...state,\n        email: e.target.value as string,\n      };\n    });\n  };\n\n  const handleDescriptionChanged = (e: React.ChangeEvent<HTMLTextAreaElement>) => {\n    setState((state) => {\n      return {\n        ...state,\n        description: e.target.value as string,\n      };\n    });\n  };\n\n  const handleSaveBtnClick = async () => {\n    if (state.username === \"\") {\n      toast.error(t(\"message.fill-all\"));\n      return;\n    }\n\n    try {\n      const updateMask = [];\n      if (!isEqual(currentUser?.username, state.username)) {\n        updateMask.push(\"username\");\n      }\n      if (!isEqual(currentUser?.displayName, state.displayName)) {\n        updateMask.push(\"display_name\");\n      }\n      if (!isEqual(currentUser?.email, state.email)) {\n        updateMask.push(\"email\");\n      }\n      if (!isEqual(currentUser?.avatarUrl, state.avatarUrl)) {\n        updateMask.push(\"avatar_url\");\n      }\n      if (!isEqual(currentUser?.description, state.description)) {\n        updateMask.push(\"description\");\n      }\n      await updateUser({\n        user: {\n          name: currentUser?.name,\n          username: state.username,\n          displayName: state.displayName,\n          email: state.email,\n          avatarUrl: state.avatarUrl,\n          description: state.description,\n        },\n        updateMask,\n      });\n      toast.success(t(\"message.update-succeed\"));\n      onSuccess?.();\n      onOpenChange(false);\n    } catch (error: unknown) {\n      await handleError(error, toast.error, {\n        context: \"Update account\",\n      });\n    }\n  };\n\n  return (\n    <Dialog open={open} onOpenChange={onOpenChange}>\n      <DialogContent className=\"max-w-md\">\n        <DialogHeader>\n          <DialogTitle>{t(\"setting.account.update-information\")}</DialogTitle>\n        </DialogHeader>\n        <div className=\"flex flex-col gap-4\">\n          <div className=\"flex flex-row items-center gap-2\">\n            <Label>{t(\"common.avatar\")}</Label>\n            <label className=\"relative cursor-pointer hover:opacity-80\">\n              <UserAvatar className=\"w-10 h-10\" avatarUrl={state.avatarUrl} />\n              <input type=\"file\" accept=\"image/*\" className=\"absolute invisible w-full h-full inset-0\" onChange={handleAvatarChanged} />\n            </label>\n            {state.avatarUrl && (\n              <XIcon\n                className=\"w-4 h-auto cursor-pointer opacity-60 hover:opacity-80\"\n                onClick={() =>\n                  setPartialState({\n                    avatarUrl: \"\",\n                  })\n                }\n              />\n            )}\n          </div>\n          <div className=\"grid gap-2\">\n            <Label htmlFor=\"username\">\n              {t(\"common.username\")}\n              <span className=\"text-sm text-muted-foreground ml-1\">({t(\"setting.account.username-note\")})</span>\n            </Label>\n            <Input\n              id=\"username\"\n              value={state.username}\n              onChange={handleUsernameChanged}\n              disabled={instanceGeneralSetting.disallowChangeUsername}\n            />\n          </div>\n          <div className=\"grid gap-2\">\n            <Label htmlFor=\"displayName\">\n              {t(\"common.nickname\")}\n              <span className=\"text-sm text-muted-foreground ml-1\">({t(\"setting.account.nickname-note\")})</span>\n            </Label>\n            <Input\n              id=\"displayName\"\n              value={state.displayName}\n              onChange={handleDisplayNameChanged}\n              disabled={instanceGeneralSetting.disallowChangeNickname}\n            />\n          </div>\n          <div className=\"grid gap-2\">\n            <Label htmlFor=\"email\">\n              {t(\"common.email\")}\n              <span className=\"text-sm text-muted-foreground ml-1\">({t(\"setting.account.email-note\")})</span>\n            </Label>\n            <Input id=\"email\" type=\"email\" value={state.email} onChange={handleEmailChanged} />\n          </div>\n          <div className=\"grid gap-2\">\n            <Label htmlFor=\"description\">{t(\"common.description\")}</Label>\n            <Textarea id=\"description\" rows={2} value={state.description} onChange={handleDescriptionChanged} />\n          </div>\n        </div>\n        <DialogFooter>\n          <Button variant=\"ghost\" onClick={handleCloseBtnClick}>\n            {t(\"common.cancel\")}\n          </Button>\n          <Button onClick={handleSaveBtnClick}>{t(\"common.save\")}</Button>\n        </DialogFooter>\n      </DialogContent>\n    </Dialog>\n  );\n}\n\nexport default UpdateAccountDialog;\n"
  },
  {
    "path": "web/src/components/UpdateCustomizedProfileDialog.tsx",
    "content": "import { create } from \"@bufbuild/protobuf\";\nimport { useState } from \"react\";\nimport { toast } from \"react-hot-toast\";\nimport { Button } from \"@/components/ui/button\";\nimport { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from \"@/components/ui/dialog\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport { Textarea } from \"@/components/ui/textarea\";\nimport { useInstance } from \"@/contexts/InstanceContext\";\nimport { buildInstanceSettingName } from \"@/helpers/resource-names\";\nimport { handleError } from \"@/lib/error\";\nimport {\n  InstanceSetting_GeneralSetting_CustomProfile,\n  InstanceSetting_GeneralSetting_CustomProfileSchema,\n  InstanceSetting_Key,\n  InstanceSettingSchema,\n} from \"@/types/proto/api/v1/instance_service_pb\";\nimport { useTranslate } from \"@/utils/i18n\";\n\ninterface Props {\n  open: boolean;\n  onOpenChange: (open: boolean) => void;\n  onSuccess?: () => void;\n}\n\nfunction UpdateCustomizedProfileDialog({ open, onOpenChange, onSuccess }: Props) {\n  const t = useTranslate();\n  const { generalSetting: instanceGeneralSetting, updateSetting } = useInstance();\n  const [customProfile, setCustomProfile] = useState<InstanceSetting_GeneralSetting_CustomProfile>(\n    create(InstanceSetting_GeneralSetting_CustomProfileSchema, instanceGeneralSetting.customProfile || {}),\n  );\n\n  const [isLoading, setIsLoading] = useState(false);\n\n  const setPartialState = (partialState: Partial<InstanceSetting_GeneralSetting_CustomProfile>) => {\n    setCustomProfile((state) => ({\n      ...state,\n      ...partialState,\n    }));\n  };\n\n  const handleNameChanged = (e: React.ChangeEvent<HTMLInputElement>) => {\n    setPartialState({\n      title: e.target.value as string,\n    });\n  };\n\n  const handleLogoUrlChanged = (e: React.ChangeEvent<HTMLInputElement>) => {\n    setPartialState({\n      logoUrl: e.target.value as string,\n    });\n  };\n\n  const handleDescriptionChanged = (e: React.ChangeEvent<HTMLTextAreaElement>) => {\n    setPartialState({\n      description: e.target.value as string,\n    });\n  };\n\n  const handleRestoreButtonClick = () => {\n    setPartialState({\n      title: \"Memos\",\n      logoUrl: \"/logo.webp\",\n      description: \"\",\n    });\n  };\n\n  const handleCloseButtonClick = () => {\n    onOpenChange(false);\n  };\n\n  const handleSaveButtonClick = async () => {\n    if (customProfile.title === \"\") {\n      toast.error(\"Title cannot be empty.\");\n      return;\n    }\n\n    setIsLoading(true);\n    try {\n      await updateSetting(\n        create(InstanceSettingSchema, {\n          name: buildInstanceSettingName(InstanceSetting_Key.GENERAL),\n          value: {\n            case: \"generalSetting\",\n            value: {\n              ...instanceGeneralSetting,\n              customProfile: customProfile,\n            },\n          },\n        }),\n      );\n      toast.success(t(\"message.update-succeed\"));\n      onSuccess?.();\n      onOpenChange(false);\n    } catch (error) {\n      handleError(error, toast.error, {\n        context: \"Update customized profile\",\n        fallbackMessage: \"Failed to update profile\",\n      });\n    } finally {\n      setIsLoading(false);\n    }\n  };\n\n  return (\n    <Dialog open={open} onOpenChange={onOpenChange}>\n      <DialogContent className=\"max-w-2xl\">\n        <DialogHeader>\n          <DialogTitle>{t(\"setting.system.customize-server.title\")}</DialogTitle>\n          <DialogDescription>Customize your instance appearance and settings.</DialogDescription>\n        </DialogHeader>\n\n        <div className=\"grid gap-4\">\n          <div className=\"grid gap-2\">\n            <Label htmlFor=\"server-name\">{t(\"setting.system.server-name\")}</Label>\n            <Input id=\"server-name\" type=\"text\" value={customProfile.title} onChange={handleNameChanged} placeholder=\"Enter server name\" />\n          </div>\n\n          <div className=\"grid gap-2\">\n            <Label htmlFor=\"icon-url\">{t(\"setting.system.customize-server.icon-url\")}</Label>\n            <Input id=\"icon-url\" type=\"text\" value={customProfile.logoUrl} onChange={handleLogoUrlChanged} placeholder=\"Enter icon URL\" />\n          </div>\n\n          <div className=\"grid gap-2\">\n            <Label htmlFor=\"description\">{t(\"setting.system.customize-server.description\")}</Label>\n            <Textarea\n              id=\"description\"\n              rows={3}\n              value={customProfile.description}\n              onChange={handleDescriptionChanged}\n              placeholder=\"Enter description\"\n            />\n          </div>\n        </div>\n\n        <DialogFooter className=\"flex-col sm:flex-row sm:justify-between gap-2\">\n          <Button variant=\"outline\" onClick={handleRestoreButtonClick} disabled={isLoading} className=\"sm:mr-auto\">\n            {t(\"common.restore\")}\n          </Button>\n          <div className=\"flex gap-2 w-full sm:w-auto\">\n            <Button variant=\"ghost\" onClick={handleCloseButtonClick} disabled={isLoading} className=\"flex-1 sm:flex-initial\">\n              {t(\"common.cancel\")}\n            </Button>\n            <Button onClick={handleSaveButtonClick} disabled={isLoading} className=\"flex-1 sm:flex-initial\">\n              {isLoading ? \"Saving...\" : t(\"common.save\")}\n            </Button>\n          </div>\n        </DialogFooter>\n      </DialogContent>\n    </Dialog>\n  );\n}\n\nexport default UpdateCustomizedProfileDialog;\n"
  },
  {
    "path": "web/src/components/UserAvatar.tsx",
    "content": "import { cn } from \"@/lib/utils\";\n\ninterface Props {\n  avatarUrl?: string;\n  className?: string;\n}\n\nconst UserAvatar = (props: Props) => {\n  const { avatarUrl, className } = props;\n  return (\n    <div className={cn(`w-8 h-8 overflow-clip rounded-xl border border-border`, className)}>\n      <img\n        className=\"w-full h-auto shadow min-w-full min-h-full object-cover\"\n        src={avatarUrl || \"/full-logo.webp\"}\n        decoding=\"async\"\n        loading=\"lazy\"\n        alt=\"\"\n      />\n    </div>\n  );\n};\n\nexport default UserAvatar;\n"
  },
  {
    "path": "web/src/components/UserMemoMap/UserMemoMap.tsx",
    "content": "import { timestampDate } from \"@bufbuild/protobuf/wkt\";\nimport L, { DivIcon } from \"leaflet\";\nimport \"leaflet.markercluster/dist/MarkerCluster.Default.css\";\nimport \"leaflet.markercluster/dist/MarkerCluster.css\";\nimport { ArrowUpRightIcon, MapPinIcon } from \"lucide-react\";\nimport { useEffect, useMemo } from \"react\";\nimport { MapContainer, Marker, Popup, useMap } from \"react-leaflet\";\nimport MarkerClusterGroup from \"react-leaflet-cluster\";\nimport { Link } from \"react-router-dom\";\nimport { defaultMarkerIcon, ThemedTileLayer } from \"@/components/map/map-utils\";\nimport { useInfiniteMemos } from \"@/hooks/useMemoQueries\";\nimport { cn } from \"@/lib/utils\";\nimport { State } from \"@/types/proto/api/v1/common_pb\";\nimport { Memo } from \"@/types/proto/api/v1/memo_service_pb\";\n\ninterface Props {\n  creator: string;\n  className?: string;\n}\n\ninterface ClusterGroup {\n  getChildCount(): number;\n}\n\nconst createClusterCustomIcon = (cluster: ClusterGroup) => {\n  return new DivIcon({\n    html: `<span class=\"flex items-center justify-center w-full h-full bg-primary text-primary-foreground text-xs font-bold rounded-full shadow-md border-2 border-background\">${cluster.getChildCount()}</span>`,\n    className: \"custom-marker-cluster\",\n    iconSize: L.point(32, 32, true),\n  });\n};\n\nconst extractUserIdFromName = (name: string): string => {\n  const match = name.match(/users\\/(\\d+)/);\n  return match ? match[1] : \"\";\n};\n\nconst MapFitBounds = ({ memos }: { memos: Memo[] }) => {\n  const map = useMap();\n\n  useEffect(() => {\n    if (memos.length === 0) return;\n\n    const validMemos = memos.filter((m) => m.location);\n    if (validMemos.length === 0) return;\n\n    const bounds = L.latLngBounds(validMemos.map((memo) => [memo.location!.latitude, memo.location!.longitude]));\n    map.fitBounds(bounds, { padding: [50, 50] });\n  }, [memos, map]);\n\n  return null;\n};\n\nconst UserMemoMap = ({ creator, className }: Props) => {\n  const creatorId = useMemo(() => extractUserIdFromName(creator), [creator]);\n\n  const { data, isLoading } = useInfiniteMemos({\n    state: State.NORMAL,\n    orderBy: \"display_time desc\",\n    pageSize: 1000,\n    filter: `creator_id == ${creatorId}`,\n  });\n\n  const memosWithLocation = useMemo(() => data?.pages.flatMap((page) => page.memos).filter((memo) => memo.location) || [], [data]);\n\n  if (isLoading) return null;\n\n  const defaultCenter = { lat: 48.8566, lng: 2.3522 };\n\n  return (\n    <div className={cn(\"relative z-0 w-full h-[380px] rounded-xl overflow-hidden border border-border shadow-sm\", className)}>\n      {memosWithLocation.length === 0 && (\n        <div className=\"absolute inset-0 z-[1000] flex items-center justify-center pointer-events-none\">\n          <div className=\"flex flex-col items-center gap-1 rounded-2xl border border-border bg-background/70 px-4 py-2 shadow-sm backdrop-blur-sm\">\n            <MapPinIcon className=\"h-5 w-5 text-muted-foreground opacity-60\" />\n            <p className=\"text-xs font-medium text-muted-foreground\">No location data found</p>\n          </div>\n        </div>\n      )}\n\n      <MapContainer center={defaultCenter} zoom={2} className=\"h-full w-full z-0\" scrollWheelZoom attributionControl={false}>\n        <ThemedTileLayer />\n        <MarkerClusterGroup\n          chunkedLoading\n          iconCreateFunction={createClusterCustomIcon}\n          maxClusterRadius={40}\n          spiderfyOnMaxZoom\n          showCoverageOnHover={false}\n        >\n          {memosWithLocation.map((memo) => (\n            <Marker key={memo.name} position={[memo.location!.latitude, memo.location!.longitude]} icon={defaultMarkerIcon}>\n              <Popup closeButton={false} className=\"w-48!\">\n                <div className=\"flex flex-col p-0.5\">\n                  <div className=\"flex items-center justify-between border-b border-border pb-1 mb-1\">\n                    <span className=\"text-[10px] font-medium text-muted-foreground\">\n                      {memo.displayTime &&\n                        timestampDate(memo.displayTime).toLocaleDateString(undefined, {\n                          year: \"numeric\",\n                          month: \"short\",\n                          day: \"numeric\",\n                        })}\n                    </span>\n                    <Link\n                      to={`/memos/${memo.name.split(\"/\").pop()}`}\n                      className=\"flex items-center gap-0.5 text-[10px] text-primary hover:opacity-80\"\n                    >\n                      View\n                      <ArrowUpRightIcon className=\"h-3 w-3\" />\n                    </Link>\n                  </div>\n                  <div className=\"line-clamp-3 py-0.5 text-xs font-sans leading-snug text-foreground\">{memo.snippet || \"No content\"}</div>\n                </div>\n              </Popup>\n            </Marker>\n          ))}\n        </MarkerClusterGroup>\n        <MapFitBounds memos={memosWithLocation} />\n      </MapContainer>\n    </div>\n  );\n};\n\nexport default UserMemoMap;\n"
  },
  {
    "path": "web/src/components/UserMemoMap/index.ts",
    "content": "export { default } from \"./UserMemoMap\";\n"
  },
  {
    "path": "web/src/components/UserMenu.tsx",
    "content": "import { ArchiveIcon, CheckIcon, GlobeIcon, LogOutIcon, PaletteIcon, SettingsIcon, SquareUserIcon, User2Icon } from \"lucide-react\";\nimport { useAuth } from \"@/contexts/AuthContext\";\nimport useCurrentUser from \"@/hooks/useCurrentUser\";\nimport { useSSEConnectionStatus } from \"@/hooks/useLiveMemoRefresh\";\nimport useNavigateTo from \"@/hooks/useNavigateTo\";\nimport { useUpdateUserGeneralSetting } from \"@/hooks/useUserQueries\";\nimport { locales } from \"@/i18n\";\nimport { cn } from \"@/lib/utils\";\nimport { Routes } from \"@/router\";\nimport { getLocaleDisplayName, getLocaleWithFallback, loadLocale, useTranslate } from \"@/utils/i18n\";\nimport { getThemeWithFallback, loadTheme, THEME_OPTIONS } from \"@/utils/theme\";\nimport UserAvatar from \"./UserAvatar\";\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuSub,\n  DropdownMenuSubContent,\n  DropdownMenuSubTrigger,\n  DropdownMenuTrigger,\n} from \"./ui/dropdown-menu\";\nimport { Tooltip, TooltipContent, TooltipTrigger } from \"./ui/tooltip\";\n\ninterface Props {\n  collapsed?: boolean;\n}\n\nconst UserMenu = (props: Props) => {\n  const { collapsed } = props;\n  const t = useTranslate();\n  const navigateTo = useNavigateTo();\n  const currentUser = useCurrentUser();\n  const { userGeneralSetting, refetchSettings, logout } = useAuth();\n  const { mutate: updateUserGeneralSetting } = useUpdateUserGeneralSetting(currentUser?.name);\n  const sseStatus = useSSEConnectionStatus();\n  const currentLocale = getLocaleWithFallback(userGeneralSetting?.locale);\n  const currentTheme = getThemeWithFallback(userGeneralSetting?.theme);\n\n  const handleLocaleChange = async (locale: Locale) => {\n    if (!currentUser) return;\n    // Apply locale immediately for instant UI feedback and persist to localStorage\n    loadLocale(locale);\n    // Persist to user settings\n    updateUserGeneralSetting(\n      { generalSetting: { locale }, updateMask: [\"locale\"] },\n      {\n        onSuccess: () => {\n          refetchSettings();\n        },\n      },\n    );\n  };\n\n  const handleThemeChange = async (theme: string) => {\n    if (!currentUser) return;\n    // Apply theme immediately for instant UI feedback\n    loadTheme(theme);\n    // Persist to user settings\n    updateUserGeneralSetting(\n      { generalSetting: { theme }, updateMask: [\"theme\"] },\n      {\n        onSuccess: () => {\n          refetchSettings();\n        },\n      },\n    );\n  };\n\n  const handleSignOut = async () => {\n    // First, clear auth state and cache BEFORE doing anything else\n    await logout();\n\n    try {\n      // Then clear user-specific localStorage items\n      // Preserve app-wide settings (theme, locale, view preferences, tag view settings)\n      const keysToPreserve = [\"memos-theme\", \"memos-locale\", \"memos-view-setting\", \"tag-view-as-tree\", \"tag-tree-auto-expand\"];\n      const keysToRemove: string[] = [];\n\n      for (let i = 0; i < localStorage.length; i++) {\n        const key = localStorage.key(i);\n        if (key && !keysToPreserve.includes(key)) {\n          keysToRemove.push(key);\n        }\n      }\n\n      keysToRemove.forEach((key) => localStorage.removeItem(key));\n    } catch {\n      // Ignore errors from localStorage operations\n    }\n\n    // Always redirect to auth page (use replace to prevent back navigation)\n    window.location.replace(Routes.AUTH);\n  };\n\n  return (\n    <DropdownMenu>\n      <DropdownMenuTrigger asChild disabled={!currentUser}>\n        <div className={cn(\"w-auto flex flex-row justify-start items-center cursor-pointer text-foreground\", collapsed ? \"px-1\" : \"px-3\")}>\n          <div className=\"relative shrink-0\">\n            {currentUser?.avatarUrl ? (\n              <UserAvatar avatarUrl={currentUser?.avatarUrl} />\n            ) : (\n              <User2Icon className=\"w-6 mx-auto h-auto text-muted-foreground\" />\n            )}\n            {sseStatus !== \"connected\" && (\n              <Tooltip>\n                <TooltipTrigger asChild>\n                  <span\n                    className={cn(\n                      \"absolute -bottom-0.5 -right-0.5 size-2.5 rounded-full border-2 border-background\",\n                      sseStatus === \"connecting\" ? \"bg-muted-foreground animate-pulse\" : \"bg-destructive\",\n                    )}\n                  />\n                </TooltipTrigger>\n                <TooltipContent side=\"right\">{t(`live-update.${sseStatus}` as Parameters<typeof t>[0])}</TooltipContent>\n              </Tooltip>\n            )}\n          </div>\n          {!collapsed && (\n            <span className=\"ml-2 text-lg font-medium text-foreground grow truncate\">\n              {currentUser?.displayName || currentUser?.username}\n            </span>\n          )}\n        </div>\n      </DropdownMenuTrigger>\n      <DropdownMenuContent align=\"start\">\n        <DropdownMenuItem onClick={() => navigateTo(`/u/${encodeURIComponent(currentUser?.username ?? \"\")}`)}>\n          <SquareUserIcon className=\"size-4 text-muted-foreground\" />\n          {t(\"common.profile\")}\n        </DropdownMenuItem>\n        <DropdownMenuItem onClick={() => navigateTo(Routes.ARCHIVED)}>\n          <ArchiveIcon className=\"size-4 text-muted-foreground\" />\n          {t(\"common.archived\")}\n        </DropdownMenuItem>\n        <DropdownMenuSub>\n          <DropdownMenuSubTrigger>\n            <GlobeIcon className=\"size-4 text-muted-foreground\" />\n            {t(\"common.language\")}\n          </DropdownMenuSubTrigger>\n          <DropdownMenuSubContent className=\"max-h-[90vh] overflow-y-auto\">\n            {locales.map((locale) => (\n              <DropdownMenuItem key={locale} onClick={() => handleLocaleChange(locale)}>\n                {currentLocale === locale && <CheckIcon className=\"w-4 h-auto\" />}\n                {currentLocale !== locale && <span className=\"w-4\" />}\n                {getLocaleDisplayName(locale)}\n              </DropdownMenuItem>\n            ))}\n          </DropdownMenuSubContent>\n        </DropdownMenuSub>\n        <DropdownMenuSub>\n          <DropdownMenuSubTrigger>\n            <PaletteIcon className=\"size-4 text-muted-foreground\" />\n            {t(\"setting.preference.theme\")}\n          </DropdownMenuSubTrigger>\n          <DropdownMenuSubContent>\n            {THEME_OPTIONS.map((option) => (\n              <DropdownMenuItem key={option.value} onClick={() => handleThemeChange(option.value)}>\n                {currentTheme === option.value && <CheckIcon className=\"w-4 h-auto\" />}\n                {currentTheme !== option.value && <span className=\"w-4\" />}\n                {option.label}\n              </DropdownMenuItem>\n            ))}\n          </DropdownMenuSubContent>\n        </DropdownMenuSub>\n        <DropdownMenuItem onClick={() => navigateTo(Routes.SETTING)}>\n          <SettingsIcon className=\"size-4 text-muted-foreground\" />\n          {t(\"common.settings\")}\n        </DropdownMenuItem>\n        <DropdownMenuItem onClick={handleSignOut}>\n          <LogOutIcon className=\"size-4 text-muted-foreground\" />\n          {t(\"common.sign-out\")}\n        </DropdownMenuItem>\n      </DropdownMenuContent>\n    </DropdownMenu>\n  );\n};\n\nexport default UserMenu;\n"
  },
  {
    "path": "web/src/components/VisibilityIcon.tsx",
    "content": "import { Globe2Icon, LockIcon, UsersIcon } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\nimport { Visibility } from \"@/types/proto/api/v1/memo_service_pb\";\n\ninterface Props {\n  visibility: Visibility;\n  className?: string;\n}\n\nconst VisibilityIcon = (props: Props) => {\n  const { className, visibility } = props;\n\n  let VIcon = null;\n  if (visibility === Visibility.PRIVATE) {\n    VIcon = LockIcon;\n  } else if (visibility === Visibility.PROTECTED) {\n    VIcon = UsersIcon;\n  } else if (visibility === Visibility.PUBLIC) {\n    VIcon = Globe2Icon;\n  }\n  if (!VIcon) {\n    return null;\n  }\n\n  return <VIcon className={cn(\"w-4 h-auto text-muted-foreground\", className)} />;\n};\n\nexport default VisibilityIcon;\n"
  },
  {
    "path": "web/src/components/kit/OverflowTip.tsx",
    "content": "import { useEffect, useRef, useState } from \"react\";\nimport { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from \"@/components/ui/tooltip\";\nimport { cn } from \"@/lib/utils\";\n\ninterface Props {\n  children: React.ReactNode;\n  className?: string;\n}\n\nconst OverflowTip = ({ children, className }: Props) => {\n  const [isOverflowed, setIsOverflow] = useState(false);\n  const textElementRef = useRef<HTMLDivElement>(null);\n\n  useEffect(() => {\n    if (!textElementRef.current) {\n      return;\n    }\n\n    setIsOverflow(textElementRef.current.scrollWidth > textElementRef.current.clientWidth);\n  }, []);\n\n  return (\n    <TooltipProvider>\n      <Tooltip>\n        <TooltipTrigger asChild>\n          <div ref={textElementRef} className={cn(\"truncate\", className)}>\n            {children}\n          </div>\n        </TooltipTrigger>\n        {isOverflowed && (\n          <TooltipContent>\n            <p>{children}</p>\n          </TooltipContent>\n        )}\n      </Tooltip>\n    </TooltipProvider>\n  );\n};\n\nexport default OverflowTip;\n"
  },
  {
    "path": "web/src/components/kit/SquareDiv.tsx",
    "content": "import { useEffect, useRef } from \"react\";\n\ninterface Props {\n  children: React.ReactNode;\n  baseSide?: \"width\" | \"height\";\n  className?: string;\n}\n\nconst SquareDiv: React.FC<Props> = (props: Props) => {\n  const { children, className } = props;\n  const baseSide = props.baseSide || \"width\";\n  const squareDivRef = useRef<HTMLDivElement>(null);\n\n  useEffect(() => {\n    const adjustSquareSize = () => {\n      if (!squareDivRef.current) {\n        return;\n      }\n\n      if (baseSide === \"width\") {\n        const width = squareDivRef.current.clientWidth;\n        squareDivRef.current.style.height = width + \"px\";\n      } else {\n        const height = squareDivRef.current.clientHeight;\n        squareDivRef.current.style.width = height + \"px\";\n      }\n    };\n\n    adjustSquareSize();\n\n    window.addEventListener(\"resize\", adjustSquareSize);\n\n    return () => {\n      window.removeEventListener(\"resize\", adjustSquareSize);\n    };\n  }, []);\n\n  return (\n    <div ref={squareDivRef} className={`${[baseSide === \"width\" ? \"w-full\" : \"h-full\", className ?? \"\"].join(\" \")}`}>\n      {children}\n    </div>\n  );\n};\n\nexport default SquareDiv;\n"
  },
  {
    "path": "web/src/components/map/LocationPicker.tsx",
    "content": "import L, { LatLng } from \"leaflet\";\nimport { ExternalLinkIcon, MinusIcon, PlusIcon } from \"lucide-react\";\nimport { type ReactNode, useEffect, useRef, useState } from \"react\";\nimport { createRoot } from \"react-dom/client\";\nimport { MapContainer, Marker, useMap, useMapEvents } from \"react-leaflet\";\nimport { cn } from \"@/lib/utils\";\nimport { defaultMarkerIcon, ThemedTileLayer } from \"./map-utils\";\n\ninterface MarkerProps {\n  position: LatLng | undefined;\n  onChange: (position: LatLng) => void;\n  readonly?: boolean;\n}\n\nconst LocationMarker = (props: MarkerProps) => {\n  const [position, setPosition] = useState(props.position);\n  const initializedRef = useRef(false);\n\n  const map = useMapEvents({\n    click(e) {\n      if (props.readonly) {\n        return;\n      }\n\n      setPosition(e.latlng);\n      map.locate();\n      // Call the parent onChange function.\n      props.onChange(e.latlng);\n    },\n    locationfound() {},\n  });\n\n  useEffect(() => {\n    if (!initializedRef.current) {\n      map.locate();\n      initializedRef.current = true;\n    }\n  }, [map]);\n\n  // Keep marker and map in sync with external position updates\n  useEffect(() => {\n    if (props.position) {\n      setPosition(props.position);\n      map.setView(props.position);\n    } else {\n      setPosition(undefined);\n    }\n  }, [props.position, map]);\n\n  return position === undefined ? null : <Marker position={position} icon={defaultMarkerIcon}></Marker>;\n};\n\n// Reusable glass-style button component\ninterface GlassButtonProps {\n  icon: ReactNode;\n  onClick: () => void;\n  ariaLabel: string;\n  title: string;\n}\n\nconst GlassButton = ({ icon, onClick, ariaLabel, title }: GlassButtonProps) => {\n  return (\n    <button\n      type=\"button\"\n      onClick={onClick}\n      aria-label={ariaLabel}\n      title={title}\n      className={cn(\n        \"h-8 w-8 flex items-center justify-center rounded-lg\",\n        \"cursor-pointer transition-all duration-200\",\n        \"bg-white/80 backdrop-blur-md border border-white/30 shadow-lg\",\n        \"hover:bg-white/90 hover:scale-105 active:scale-95\",\n        \"dark:bg-black/80 dark:border-white/10 dark:hover:bg-black/90\",\n        \"focus:outline-none focus:ring-2 focus:ring-blue-500\",\n      )}\n    >\n      {icon}\n    </button>\n  );\n};\n\n// Container for all map control buttons\ninterface ControlButtonsProps {\n  position: LatLng | undefined;\n  onZoomIn: () => void;\n  onZoomOut: () => void;\n  onOpenGoogleMaps: () => void;\n}\n\nconst ControlButtons = ({ position, onZoomIn, onZoomOut, onOpenGoogleMaps }: ControlButtonsProps) => {\n  return (\n    <div className=\"flex flex-col gap-1.5\">\n      {position && (\n        <GlassButton\n          icon={<ExternalLinkIcon size={16} className=\"text-foreground\" />}\n          onClick={onOpenGoogleMaps}\n          ariaLabel=\"Open location in Google Maps\"\n          title=\"Open in Google Maps\"\n        />\n      )}\n      <GlassButton icon={<PlusIcon size={16} className=\"text-foreground\" />} onClick={onZoomIn} ariaLabel=\"Zoom in\" title=\"Zoom in\" />\n      <GlassButton icon={<MinusIcon size={16} className=\"text-foreground\" />} onClick={onZoomOut} ariaLabel=\"Zoom out\" title=\"Zoom out\" />\n    </div>\n  );\n};\n\n// Custom Leaflet Control class\nclass MapControlsContainer extends L.Control {\n  private container: HTMLDivElement | undefined = undefined;\n\n  onAdd() {\n    this.container = L.DomUtil.create(\"div\", \"\");\n    this.container.style.pointerEvents = \"auto\";\n\n    // Prevent map interactions when clicking controls\n    L.DomEvent.disableClickPropagation(this.container);\n    L.DomEvent.disableScrollPropagation(this.container);\n\n    return this.container;\n  }\n\n  onRemove() {\n    this.container = undefined;\n  }\n\n  getContainer() {\n    return this.container;\n  }\n}\n\ninterface MapControlsProps {\n  position: LatLng | undefined;\n}\n\nconst MapControls = ({ position }: MapControlsProps) => {\n  const map = useMap();\n  const controlRef = useRef<MapControlsContainer | null>(null);\n  const rootRef = useRef<ReturnType<typeof createRoot> | null>(null);\n\n  const handleOpenInGoogleMaps = () => {\n    if (!position) return;\n    const url = `https://www.google.com/maps?q=${position.lat},${position.lng}`;\n    window.open(url, \"_blank\", \"noopener,noreferrer\");\n  };\n\n  const handleZoomIn = () => {\n    map.zoomIn();\n  };\n\n  const handleZoomOut = () => {\n    map.zoomOut();\n  };\n\n  useEffect(() => {\n    // Create custom Leaflet control\n    const control = new MapControlsContainer({ position: \"topright\" });\n    controlRef.current = control;\n    control.addTo(map);\n\n    // Get container and render React component into it\n    const container = control.getContainer();\n    if (container) {\n      rootRef.current = createRoot(container);\n      rootRef.current.render(\n        <ControlButtons position={position} onZoomIn={handleZoomIn} onZoomOut={handleZoomOut} onOpenGoogleMaps={handleOpenInGoogleMaps} />,\n      );\n    }\n\n    return () => {\n      // Cleanup: unmount React component and remove control\n      if (rootRef.current) {\n        rootRef.current.unmount();\n        rootRef.current = null;\n      }\n      if (controlRef.current) {\n        controlRef.current.remove();\n        controlRef.current = null;\n      }\n    };\n  }, [map]);\n\n  // Update rendered content when position changes\n  useEffect(() => {\n    if (rootRef.current) {\n      rootRef.current.render(\n        <ControlButtons position={position} onZoomIn={handleZoomIn} onZoomOut={handleZoomOut} onOpenGoogleMaps={handleOpenInGoogleMaps} />,\n      );\n    }\n  }, [position]);\n\n  return null;\n};\n\nconst MapCleanup = () => {\n  const map = useMap();\n\n  useEffect(() => {\n    return () => {\n      // Cleanup map instance when component unmounts\n      setTimeout(() => {\n        if (map) {\n          try {\n            map.remove();\n          } catch {\n            // Ignore errors during cleanup\n          }\n        }\n      }, 0);\n    };\n  }, [map]);\n\n  return null;\n};\n\ninterface MapProps {\n  readonly?: boolean;\n  latlng?: LatLng;\n  onChange?: (position: LatLng) => void;\n}\n\nconst DEFAULT_CENTER_LAT_LNG = new LatLng(48.8584, 2.2945);\n\nconst LeafletMap = (props: MapProps) => {\n  const position = props.latlng || DEFAULT_CENTER_LAT_LNG;\n\n  return (\n    <MapContainer\n      className=\"w-full h-72\"\n      center={position}\n      zoom={13}\n      scrollWheelZoom={false}\n      zoomControl={false}\n      attributionControl={false}\n    >\n      <ThemedTileLayer />\n      <LocationMarker position={position} readonly={props.readonly} onChange={props.onChange ? props.onChange : () => {}} />\n      <MapControls position={props.latlng} />\n      <MapCleanup />\n    </MapContainer>\n  );\n};\n\nexport default LeafletMap;\n"
  },
  {
    "path": "web/src/components/map/index.ts",
    "content": "export { default as LocationPicker } from \"./LocationPicker\";\nexport { createMarkerIcon, defaultMarkerIcon, ThemedTileLayer } from \"./map-utils\";\nexport { useReverseGeocoding } from \"./useReverseGeocoding\";\n"
  },
  {
    "path": "web/src/components/map/map-utils.tsx",
    "content": "import { DivIcon } from \"leaflet\";\nimport { MapPinIcon } from \"lucide-react\";\nimport { useMemo } from \"react\";\nimport ReactDOMServer from \"react-dom/server\";\nimport { TileLayer } from \"react-leaflet\";\nimport { useAuth } from \"@/contexts/AuthContext\";\nimport { resolveTheme } from \"@/utils/theme\";\n\nconst TILE_URLS = {\n  light: \"https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png\",\n  dark: \"https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png\",\n} as const;\n\nexport const ThemedTileLayer = () => {\n  const { userGeneralSetting } = useAuth();\n  const isDark = useMemo(() => resolveTheme(userGeneralSetting?.theme || \"system\").includes(\"dark\"), [userGeneralSetting?.theme]);\n  return <TileLayer url={isDark ? TILE_URLS.dark : TILE_URLS.light} />;\n};\n\ninterface MarkerIconOptions {\n  fill?: string;\n  size?: number;\n  className?: string;\n}\n\nexport const createMarkerIcon = (options?: MarkerIconOptions): DivIcon => {\n  const { fill = \"orange\", size = 28, className = \"\" } = options || {};\n  return new DivIcon({\n    className: \"relative border-none\",\n    html: ReactDOMServer.renderToString(\n      <MapPinIcon className={`absolute bottom-1/2 -left-1/2 ${className}`.trim()} fill={fill} size={size} />,\n    ),\n  });\n};\n\nexport const defaultMarkerIcon = createMarkerIcon();\n"
  },
  {
    "path": "web/src/components/map/useReverseGeocoding.ts",
    "content": "import { useQuery } from \"@tanstack/react-query\";\n\nconst GEOCODING = {\n  endpoint: \"https://nominatim.openstreetmap.org/reverse\",\n  userAgent: \"Memos/1.0 (https://github.com/usememos/memos)\",\n  format: \"json\",\n} as const;\n\nexport const useReverseGeocoding = (lat: number | undefined, lng: number | undefined) => {\n  return useQuery({\n    queryKey: [\"geocoding\", lat, lng],\n    queryFn: async () => {\n      const coordString = `${lat?.toFixed(6)}, ${lng?.toFixed(6)}`;\n      if (lat === undefined || lng === undefined) return \"\";\n\n      try {\n        const url = `${GEOCODING.endpoint}?lat=${lat}&lon=${lng}&format=${GEOCODING.format}`;\n        const response = await fetch(url, {\n          headers: {\n            \"User-Agent\": GEOCODING.userAgent,\n            Accept: \"application/json\",\n          },\n        });\n\n        if (!response.ok) {\n          throw new Error(`HTTP error! status: ${response.status}`);\n        }\n\n        const data = await response.json();\n        return (data?.display_name as string) || coordString;\n      } catch (error) {\n        console.error(\"Failed to fetch reverse geocoding data:\", error);\n        return coordString;\n      }\n    },\n    enabled: lat !== undefined && lng !== undefined,\n    staleTime: Infinity,\n  });\n};\n"
  },
  {
    "path": "web/src/components/ui/badge.tsx",
    "content": "import { Slot } from \"@radix-ui/react-slot\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nconst badgeVariants = cva(\n  \"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none transition-[color,box-shadow] overflow-hidden\",\n  {\n    variants: {\n      variant: {\n        default: \"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90\",\n        secondary: \"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90\",\n        destructive: \"border-transparent bg-destructive text-destructive-foreground [a&]:hover:bg-destructive/90\",\n        outline: \"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n    },\n  },\n);\n\nfunction Badge({\n  className,\n  variant,\n  asChild = false,\n  ...props\n}: React.ComponentProps<\"span\"> & VariantProps<typeof badgeVariants> & { asChild?: boolean }) {\n  const Comp = asChild ? Slot : \"span\";\n\n  return <Comp data-slot=\"badge\" className={cn(badgeVariants({ variant }), className)} {...props} />;\n}\n\nexport { Badge, badgeVariants };\n"
  },
  {
    "path": "web/src/components/ui/button.tsx",
    "content": "import { Slot } from \"@radix-ui/react-slot\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nconst buttonVariants = cva(\n  \"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0\",\n  {\n    variants: {\n      variant: {\n        default: \"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90\",\n        destructive: \"bg-destructive text-destructive-foreground shadow-xs hover:bg-destructive/90\",\n        outline: \"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground\",\n        secondary: \"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80\",\n        ghost: \"hover:bg-accent hover:text-accent-foreground\",\n        link: \"text-primary underline-offset-4 hover:underline\",\n      },\n      size: {\n        default: \"h-8 px-3\",\n        sm: \"h-7 rounded-md gap-1 px-2 has-[>svg]:px-2\",\n        lg: \"h-9 rounded-md px-4\",\n        icon: \"size-8\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n      size: \"default\",\n    },\n  },\n);\n\nconst Button = React.forwardRef<\n  HTMLButtonElement,\n  React.ComponentProps<\"button\"> &\n    VariantProps<typeof buttonVariants> & {\n      asChild?: boolean;\n    }\n>(({ className, variant, size, asChild = false, ...props }, ref) => {\n  const Comp = asChild ? Slot : \"button\";\n\n  return <Comp ref={ref} data-slot=\"button\" className={cn(buttonVariants({ variant, size, className }))} {...props} />;\n});\nButton.displayName = \"Button\";\n\nexport { Button, buttonVariants };\n"
  },
  {
    "path": "web/src/components/ui/checkbox.tsx",
    "content": "import * as CheckboxPrimitive from \"@radix-ui/react-checkbox\";\nimport { CheckIcon } from \"lucide-react\";\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nconst Checkbox = React.forwardRef<\n  React.ElementRef<typeof CheckboxPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>\n>(({ className, ...props }, ref) => {\n  return (\n    <CheckboxPrimitive.Root\n      ref={ref}\n      data-slot=\"checkbox\"\n      className={cn(\n        \"peer border-border data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground data-[state=checked]:border-primary size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow disabled:cursor-not-allowed disabled:opacity-50\",\n        className,\n      )}\n      {...props}\n    >\n      <CheckboxPrimitive.Indicator data-slot=\"checkbox-indicator\" className=\"flex items-center justify-center text-current transition-none\">\n        <CheckIcon className=\"size-3.5\" />\n      </CheckboxPrimitive.Indicator>\n    </CheckboxPrimitive.Root>\n  );\n});\nCheckbox.displayName = \"Checkbox\";\n\nexport { Checkbox };\n"
  },
  {
    "path": "web/src/components/ui/dialog.tsx",
    "content": "import * as DialogPrimitive from \"@radix-ui/react-dialog\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { XIcon } from \"lucide-react\";\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nconst Dialog = DialogPrimitive.Root;\n\nconst DialogTrigger = DialogPrimitive.Trigger;\n\nconst DialogPortal = DialogPrimitive.Portal;\n\nconst DialogClose = DialogPrimitive.Close;\n\nconst DialogOverlay = React.forwardRef<\n  React.ElementRef<typeof DialogPrimitive.Overlay>,\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>\n>(({ className, ...props }, ref) => (\n  <DialogPrimitive.Overlay\n    ref={ref}\n    data-slot=\"dialog-overlay\"\n    className={cn(\n      \"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-foreground/50\",\n      className,\n    )}\n    {...props}\n  />\n));\nDialogOverlay.displayName = DialogPrimitive.Overlay.displayName;\n\nconst dialogContentVariants = cva(\n  \"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 flex flex-col translate-x-[-50%] translate-y-[-50%] rounded-lg border shadow-lg duration-200 max-h-[calc(100vh-2rem)] sm:max-h-[calc(100vh-3rem)] md:max-h-[calc(100vh-4rem)]\",\n  {\n    variants: {\n      size: {\n        sm: \"w-[calc(100%-2rem)] max-w-[calc(100%-2rem)] p-4 sm:w-[calc(100%-3rem)] sm:max-w-[calc(100%-3rem)] sm:p-6 md:w-full md:max-w-sm\",\n        default:\n          \"w-[calc(100%-2rem)] max-w-[calc(100%-2rem)] p-4 sm:w-[calc(100%-3rem)] sm:max-w-[calc(100%-3rem)] sm:p-6 md:w-full md:max-w-md\",\n        lg: \"w-[calc(100%-2rem)] max-w-[calc(100%-2rem)] p-4 sm:w-[calc(100%-3rem)] sm:max-w-[calc(100%-3rem)] sm:p-6 md:w-full md:max-w-lg\",\n        xl: \"w-[calc(100%-2rem)] max-w-[calc(100%-2rem)] p-4 sm:w-[calc(100%-3rem)] sm:max-w-[calc(100%-3rem)] sm:p-6 md:w-full md:max-w-xl\",\n        \"2xl\":\n          \"w-[calc(100%-2rem)] max-w-[calc(100%-2rem)] p-4 sm:w-[calc(100%-3rem)] sm:max-w-[calc(100%-3rem)] sm:p-6 md:w-full md:max-w-2xl\",\n        full: \"w-[calc(100%-2rem)] max-w-[calc(100%-2rem)] p-4 sm:w-[calc(100%-3rem)] sm:max-w-[calc(100%-3rem)] sm:p-6 md:w-[calc(100%-2rem)] md:max-w-none\",\n      },\n    },\n    defaultVariants: {\n      size: \"default\",\n    },\n  },\n);\n\nconst DialogContent = React.forwardRef<\n  React.ElementRef<typeof DialogPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> &\n    VariantProps<typeof dialogContentVariants> & {\n      showCloseButton?: boolean;\n    }\n>(({ className, children, showCloseButton = true, size, ...props }, ref) => (\n  <DialogPortal>\n    <DialogOverlay />\n    <DialogPrimitive.Content\n      ref={ref}\n      className={cn(dialogContentVariants({ size }), className)}\n      onOpenAutoFocus={(e) => {\n        e.preventDefault();\n      }}\n      onCloseAutoFocus={(e) => {\n        e.preventDefault();\n        document.body.style.pointerEvents = \"auto\";\n      }}\n      {...props}\n    >\n      <div className=\"overflow-y-auto overflow-x-hidden flex-1 flex flex-col gap-4\">{children}</div>\n      {showCloseButton && (\n        <DialogPrimitive.Close className=\"ring-offset-background data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\">\n          <XIcon />\n          <span className=\"sr-only\">Close</span>\n        </DialogPrimitive.Close>\n      )}\n    </DialogPrimitive.Content>\n  </DialogPortal>\n));\nDialogContent.displayName = DialogPrimitive.Content.displayName;\n\nconst DialogHeader = React.forwardRef<React.ElementRef<\"div\">, React.ComponentPropsWithoutRef<\"div\">>(({ className, ...props }, ref) => (\n  <div ref={ref} className={cn(\"flex flex-col gap-2 text-center sm:text-left\", className)} {...props} />\n));\nDialogHeader.displayName = \"DialogHeader\";\n\nconst DialogFooter = React.forwardRef<React.ElementRef<\"div\">, React.ComponentPropsWithoutRef<\"div\">>(({ className, ...props }, ref) => (\n  <div ref={ref} className={cn(\"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end\", className)} {...props} />\n));\nDialogFooter.displayName = \"DialogFooter\";\n\nconst DialogTitle = React.forwardRef<\n  React.ElementRef<typeof DialogPrimitive.Title>,\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>\n>(({ className, ...props }, ref) => (\n  <DialogPrimitive.Title ref={ref} className={cn(\"text-lg leading-none font-semibold\", className)} {...props} />\n));\nDialogTitle.displayName = DialogPrimitive.Title.displayName;\n\nconst DialogDescription = React.forwardRef<\n  React.ElementRef<typeof DialogPrimitive.Description>,\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>\n>(({ className, ...props }, ref) => (\n  <DialogPrimitive.Description ref={ref} className={cn(\"text-muted-foreground text-sm\", className)} {...props} />\n));\nDialogDescription.displayName = DialogPrimitive.Description.displayName;\n\nexport {\n  Dialog,\n  DialogClose,\n  DialogContent,\n  DialogDescription,\n  DialogFooter,\n  DialogHeader,\n  DialogOverlay,\n  DialogPortal,\n  DialogTitle,\n  DialogTrigger,\n};\n"
  },
  {
    "path": "web/src/components/ui/dropdown-menu.tsx",
    "content": "import * as DropdownMenuPrimitive from \"@radix-ui/react-dropdown-menu\";\nimport { CheckIcon, ChevronRightIcon, CircleIcon } from \"lucide-react\";\nimport * as React from \"react\";\nimport { useEffect, useRef } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nconst DropdownMenu = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Root>\n  // eslint-disable-next-line @typescript-eslint/no-unused-vars\n>(({ ...props }, _ref) => {\n  return <DropdownMenuPrimitive.Root data-slot=\"dropdown-menu\" {...props} />;\n});\nDropdownMenu.displayName = \"DropdownMenu\";\n\nconst DropdownMenuPortal = ({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) => {\n  return <DropdownMenuPrimitive.Portal data-slot=\"dropdown-menu-portal\" {...props} />;\n};\n\nconst DropdownMenuTrigger = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.Trigger>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Trigger>\n>(({ ...props }, ref) => {\n  return <DropdownMenuPrimitive.Trigger ref={ref} data-slot=\"dropdown-menu-trigger\" {...props} />;\n});\nDropdownMenuTrigger.displayName = \"DropdownMenuTrigger\";\n\nconst DropdownMenuContent = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>\n>(({ className, sideOffset = 4, ...props }, ref) => {\n  return (\n    <DropdownMenuPrimitive.Portal>\n      <DropdownMenuPrimitive.Content\n        ref={ref}\n        data-slot=\"dropdown-menu-content\"\n        sideOffset={sideOffset}\n        className={cn(\n          \"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-[60] max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md\",\n          className,\n        )}\n        {...props}\n      />\n    </DropdownMenuPrimitive.Portal>\n  );\n});\nDropdownMenuContent.displayName = \"DropdownMenuContent\";\n\nfunction DropdownMenuGroup({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {\n  return <DropdownMenuPrimitive.Group data-slot=\"dropdown-menu-group\" {...props} />;\n}\n\nfunction DropdownMenuItem({\n  className,\n  inset,\n  variant = \"default\",\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {\n  inset?: boolean;\n  variant?: \"default\" | \"destructive\";\n}) {\n  return (\n    <DropdownMenuPrimitive.Item\n      data-slot=\"dropdown-menu-item\"\n      data-inset={inset}\n      data-variant={variant}\n      className={cn(\n        \"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction DropdownMenuCheckboxItem({\n  className,\n  children,\n  checked,\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {\n  return (\n    <DropdownMenuPrimitive.CheckboxItem\n      data-slot=\"dropdown-menu-checkbox-item\"\n      className={cn(\n        \"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n        className,\n      )}\n      checked={checked}\n      {...props}\n    >\n      <span className=\"pointer-events-none absolute left-2 flex size-3.5 items-center justify-center\">\n        <DropdownMenuPrimitive.ItemIndicator>\n          <CheckIcon className=\"size-4\" />\n        </DropdownMenuPrimitive.ItemIndicator>\n      </span>\n      {children}\n    </DropdownMenuPrimitive.CheckboxItem>\n  );\n}\n\nfunction DropdownMenuRadioGroup({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {\n  return <DropdownMenuPrimitive.RadioGroup data-slot=\"dropdown-menu-radio-group\" {...props} />;\n}\n\nfunction DropdownMenuRadioItem({ className, children, ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {\n  return (\n    <DropdownMenuPrimitive.RadioItem\n      data-slot=\"dropdown-menu-radio-item\"\n      className={cn(\n        \"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n        className,\n      )}\n      {...props}\n    >\n      <span className=\"pointer-events-none absolute left-2 flex size-3.5 items-center justify-center\">\n        <DropdownMenuPrimitive.ItemIndicator>\n          <CircleIcon className=\"size-2 fill-current\" />\n        </DropdownMenuPrimitive.ItemIndicator>\n      </span>\n      {children}\n    </DropdownMenuPrimitive.RadioItem>\n  );\n}\n\nfunction DropdownMenuLabel({\n  className,\n  inset,\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {\n  inset?: boolean;\n}) {\n  return (\n    <DropdownMenuPrimitive.Label\n      data-slot=\"dropdown-menu-label\"\n      data-inset={inset}\n      className={cn(\"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction DropdownMenuSeparator({ className, ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {\n  return (\n    <DropdownMenuPrimitive.Separator\n      data-slot=\"dropdown-menu-separator\"\n      className={cn(\"bg-border -mx-1 my-1 h-px\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction DropdownMenuShortcut({ className, ...props }: React.ComponentProps<\"span\">) {\n  return (\n    <span\n      data-slot=\"dropdown-menu-shortcut\"\n      className={cn(\"text-muted-foreground ml-auto text-xs tracking-widest\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction DropdownMenuSub({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {\n  return <DropdownMenuPrimitive.Sub data-slot=\"dropdown-menu-sub\" {...props} />;\n}\n\nfunction DropdownMenuSubTrigger({\n  className,\n  inset,\n  children,\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {\n  inset?: boolean;\n}) {\n  return (\n    <DropdownMenuPrimitive.SubTrigger\n      data-slot=\"dropdown-menu-sub-trigger\"\n      data-inset={inset}\n      className={cn(\n        \"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n      <ChevronRightIcon className=\"ml-auto size-4\" />\n    </DropdownMenuPrimitive.SubTrigger>\n  );\n}\n\nfunction DropdownMenuSubContent({ className, ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {\n  return (\n    <DropdownMenuPrimitive.SubContent\n      data-slot=\"dropdown-menu-sub-content\"\n      className={cn(\n        \"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-[60] min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\n/**\n * Hook for managing submenu hover behavior with delayed close.\n * Prevents accidental submenu closure on quick mouse movements.\n *\n * @param closeDelay - Delay in ms before closing submenu when leaving trigger/content\n * @param onOpenChange - Callback to update submenu open state\n * @returns Object with event handlers and state management utilities\n */\nfunction useDropdownMenuSubHoverDelay(closeDelay = 150, onOpenChange?: (open: boolean) => void) {\n  const closeTimeoutRef = useRef<number | null>(null);\n\n  const clearCloseTimeout = () => {\n    if (closeTimeoutRef.current) {\n      clearTimeout(closeTimeoutRef.current);\n      closeTimeoutRef.current = null;\n    }\n  };\n\n  const scheduleClose = (delay = 0) => {\n    clearCloseTimeout();\n    closeTimeoutRef.current = window.setTimeout(() => onOpenChange?.(false), delay);\n  };\n\n  const handleTriggerEnter = () => {\n    clearCloseTimeout();\n    onOpenChange?.(true);\n  };\n\n  const handleTriggerLeave = () => {\n    scheduleClose(closeDelay);\n  };\n\n  const handleContentEnter = () => {\n    clearCloseTimeout();\n  };\n\n  const handleContentLeave = () => {\n    scheduleClose();\n  };\n\n  useEffect(() => {\n    return () => clearCloseTimeout();\n  }, []);\n\n  return {\n    handleTriggerEnter,\n    handleTriggerLeave,\n    handleContentEnter,\n    handleContentLeave,\n  };\n}\n\nexport {\n  DropdownMenu,\n  DropdownMenuCheckboxItem,\n  DropdownMenuContent,\n  DropdownMenuGroup,\n  DropdownMenuItem,\n  DropdownMenuLabel,\n  DropdownMenuPortal,\n  DropdownMenuRadioGroup,\n  DropdownMenuRadioItem,\n  DropdownMenuSeparator,\n  DropdownMenuShortcut,\n  DropdownMenuSub,\n  DropdownMenuSubContent,\n  DropdownMenuSubTrigger,\n  DropdownMenuTrigger,\n  useDropdownMenuSubHoverDelay,\n};\n"
  },
  {
    "path": "web/src/components/ui/input.tsx",
    "content": "import * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nfunction Input({ className, type, ...props }: React.ComponentProps<\"input\">) {\n  return (\n    <input\n      type={type}\n      data-slot=\"input\"\n      className={cn(\n        \"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground border-border flex h-8 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport { Input };\n"
  },
  {
    "path": "web/src/components/ui/label.tsx",
    "content": "import * as LabelPrimitive from \"@radix-ui/react-label\";\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nfunction Label({ className, ...props }: React.ComponentProps<typeof LabelPrimitive.Root>) {\n  return (\n    <LabelPrimitive.Root\n      data-slot=\"label\"\n      className={cn(\n        \"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport { Label };\n"
  },
  {
    "path": "web/src/components/ui/popover.tsx",
    "content": "import * as PopoverPrimitive from \"@radix-ui/react-popover\";\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nconst Popover = React.forwardRef<\n  React.ElementRef<typeof PopoverPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Root>\n  // eslint-disable-next-line @typescript-eslint/no-unused-vars\n>(({ ...props }, _ref) => {\n  return <PopoverPrimitive.Root data-slot=\"popover\" {...props} />;\n});\nPopover.displayName = \"Popover\";\n\nconst PopoverTrigger = React.forwardRef<\n  React.ElementRef<typeof PopoverPrimitive.Trigger>,\n  React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Trigger>\n>(({ ...props }, ref) => {\n  return <PopoverPrimitive.Trigger ref={ref} data-slot=\"popover-trigger\" {...props} />;\n});\nPopoverTrigger.displayName = \"PopoverTrigger\";\n\nconst PopoverContent = React.forwardRef<\n  React.ElementRef<typeof PopoverPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>\n>(({ className, align = \"center\", sideOffset = 4, ...props }, ref) => {\n  return (\n    <PopoverPrimitive.Portal>\n      <PopoverPrimitive.Content\n        ref={ref}\n        data-slot=\"popover-content\"\n        align={align}\n        sideOffset={sideOffset}\n        className={cn(\n          \"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-[60] w-auto origin-(--radix-popover-content-transform-origin) rounded-md border p-1 shadow-md outline-hidden\",\n          className,\n        )}\n        {...props}\n      />\n    </PopoverPrimitive.Portal>\n  );\n});\nPopoverContent.displayName = \"PopoverContent\";\n\nconst PopoverAnchor = React.forwardRef<\n  React.ElementRef<typeof PopoverPrimitive.Anchor>,\n  React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Anchor>\n>(({ ...props }, ref) => {\n  return <PopoverPrimitive.Anchor ref={ref} data-slot=\"popover-anchor\" {...props} />;\n});\nPopoverAnchor.displayName = \"PopoverAnchor\";\n\nexport { Popover, PopoverAnchor, PopoverContent, PopoverTrigger };\n"
  },
  {
    "path": "web/src/components/ui/radio-group.tsx",
    "content": "import * as RadioGroupPrimitive from \"@radix-ui/react-radio-group\";\nimport { CircleIcon } from \"lucide-react\";\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nfunction RadioGroup({ className, ...props }: React.ComponentProps<typeof RadioGroupPrimitive.Root>) {\n  return <RadioGroupPrimitive.Root data-slot=\"radio-group\" className={cn(\"grid gap-3\", className)} {...props} />;\n}\n\nfunction RadioGroupItem({ className, ...props }: React.ComponentProps<typeof RadioGroupPrimitive.Item>) {\n  return (\n    <RadioGroupPrimitive.Item\n      data-slot=\"radio-group-item\"\n      className={cn(\n        \"border-border text-primary aspect-square size-4 shrink-0 rounded-full border shadow-xs transition-[color,box-shadow] disabled:cursor-not-allowed disabled:opacity-50\",\n        className,\n      )}\n      {...props}\n    >\n      <RadioGroupPrimitive.Indicator data-slot=\"radio-group-indicator\" className=\"relative flex items-center justify-center\">\n        <CircleIcon className=\"fill-primary absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2\" />\n      </RadioGroupPrimitive.Indicator>\n    </RadioGroupPrimitive.Item>\n  );\n}\n\nexport { RadioGroup, RadioGroupItem };\n"
  },
  {
    "path": "web/src/components/ui/select.tsx",
    "content": "import * as SelectPrimitive from \"@radix-ui/react-select\";\nimport { CheckIcon, ChevronDownIcon, ChevronUpIcon } from \"lucide-react\";\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nfunction Select({ ...props }: React.ComponentProps<typeof SelectPrimitive.Root>) {\n  return <SelectPrimitive.Root data-slot=\"select\" {...props} />;\n}\n\nfunction SelectGroup({ ...props }: React.ComponentProps<typeof SelectPrimitive.Group>) {\n  return <SelectPrimitive.Group data-slot=\"select-group\" {...props} />;\n}\n\nfunction SelectValue({ ...props }: React.ComponentProps<typeof SelectPrimitive.Value>) {\n  return <SelectPrimitive.Value data-slot=\"select-value\" {...props} />;\n}\n\nfunction SelectTrigger({\n  className,\n  size = \"default\",\n  children,\n  ...props\n}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {\n  size?: \"xs\" | \"sm\" | \"default\";\n}) {\n  return (\n    <SelectPrimitive.Trigger\n      data-slot=\"select-trigger\"\n      data-size={size}\n      className={cn(\n        \"border-border data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground flex w-fit items-center justify-between rounded-md border bg-transparent text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-8 data-[size=default]:gap-2 data-[size=default]:px-2 data-[size=default]:py-1 data-[size=sm]:h-7 data-[size=sm]:gap-2 data-[size=sm]:px-2 data-[size=sm]:py-1 data-[size=xs]:h-6 data-[size=xs]:gap-1 data-[size=xs]:px-1 data-[size=xs]:py-0.5 data-[size=xs]:text-xs *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n      <SelectPrimitive.Icon asChild>\n        <ChevronDownIcon className=\"size-4 opacity-50\" />\n      </SelectPrimitive.Icon>\n    </SelectPrimitive.Trigger>\n  );\n}\n\nfunction SelectContent({ className, children, position = \"popper\", ...props }: React.ComponentProps<typeof SelectPrimitive.Content>) {\n  return (\n    <SelectPrimitive.Portal>\n      <SelectPrimitive.Content\n        data-slot=\"select-content\"\n        className={cn(\n          \"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-[60] max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md\",\n          position === \"popper\" &&\n            \"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1\",\n          className,\n        )}\n        position={position}\n        {...props}\n      >\n        <SelectScrollUpButton />\n        <SelectPrimitive.Viewport\n          className={cn(\n            \"p-1\",\n            position === \"popper\" && \"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1\",\n          )}\n        >\n          {children}\n        </SelectPrimitive.Viewport>\n        <SelectScrollDownButton />\n      </SelectPrimitive.Content>\n    </SelectPrimitive.Portal>\n  );\n}\n\nfunction SelectLabel({ className, ...props }: React.ComponentProps<typeof SelectPrimitive.Label>) {\n  return (\n    <SelectPrimitive.Label\n      data-slot=\"select-label\"\n      className={cn(\"text-muted-foreground px-2 py-1 text-xs select-none\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction SelectItem({ className, children, ...props }: React.ComponentProps<typeof SelectPrimitive.Item>) {\n  return (\n    <SelectPrimitive.Item\n      data-slot=\"select-item\"\n      className={cn(\n        \"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2\",\n        className,\n      )}\n      {...props}\n    >\n      <span className=\"absolute right-2 flex size-3.5 items-center justify-center\">\n        <SelectPrimitive.ItemIndicator>\n          <CheckIcon className=\"size-4\" />\n        </SelectPrimitive.ItemIndicator>\n      </span>\n      <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>\n    </SelectPrimitive.Item>\n  );\n}\n\nfunction SelectSeparator({ className, ...props }: React.ComponentProps<typeof SelectPrimitive.Separator>) {\n  return (\n    <SelectPrimitive.Separator\n      data-slot=\"select-separator\"\n      className={cn(\"bg-border pointer-events-none -mx-1 my-1 h-px\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction SelectScrollUpButton({ className, ...props }: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {\n  return (\n    <SelectPrimitive.ScrollUpButton\n      data-slot=\"select-scroll-up-button\"\n      className={cn(\"flex cursor-default items-center justify-center py-1\", className)}\n      {...props}\n    >\n      <ChevronUpIcon className=\"size-4\" />\n    </SelectPrimitive.ScrollUpButton>\n  );\n}\n\nfunction SelectScrollDownButton({ className, ...props }: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {\n  return (\n    <SelectPrimitive.ScrollDownButton\n      data-slot=\"select-scroll-down-button\"\n      className={cn(\"flex cursor-default items-center justify-center py-1\", className)}\n      {...props}\n    >\n      <ChevronDownIcon className=\"size-4\" />\n    </SelectPrimitive.ScrollDownButton>\n  );\n}\n\nexport {\n  Select,\n  SelectContent,\n  SelectGroup,\n  SelectItem,\n  SelectLabel,\n  SelectScrollDownButton,\n  SelectScrollUpButton,\n  SelectSeparator,\n  SelectTrigger,\n  SelectValue,\n};\n"
  },
  {
    "path": "web/src/components/ui/separator.tsx",
    "content": "import * as SeparatorPrimitive from \"@radix-ui/react-separator\";\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nfunction Separator({\n  className,\n  orientation = \"horizontal\",\n  decorative = true,\n  ...props\n}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {\n  return (\n    <SeparatorPrimitive.Root\n      data-slot=\"separator\"\n      decorative={decorative}\n      orientation={orientation}\n      className={cn(\n        \"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport { Separator };\n"
  },
  {
    "path": "web/src/components/ui/sheet.tsx",
    "content": "import * as SheetPrimitive from \"@radix-ui/react-dialog\";\nimport { XIcon } from \"lucide-react\";\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nconst Sheet = React.forwardRef<React.ElementRef<typeof SheetPrimitive.Root>, React.ComponentPropsWithoutRef<typeof SheetPrimitive.Root>>(\n  // eslint-disable-next-line @typescript-eslint/no-unused-vars\n  ({ ...props }, _ref) => {\n    return <SheetPrimitive.Root data-slot=\"sheet\" {...props} />;\n  },\n);\nSheet.displayName = \"Sheet\";\n\nconst SheetTrigger = React.forwardRef<\n  React.ElementRef<typeof SheetPrimitive.Trigger>,\n  React.ComponentPropsWithoutRef<typeof SheetPrimitive.Trigger>\n>(({ ...props }, ref) => {\n  return <SheetPrimitive.Trigger ref={ref} data-slot=\"sheet-trigger\" {...props} />;\n});\nSheetTrigger.displayName = \"SheetTrigger\";\n\nconst SheetClose = React.forwardRef<\n  React.ElementRef<typeof SheetPrimitive.Close>,\n  React.ComponentPropsWithoutRef<typeof SheetPrimitive.Close>\n>(({ ...props }, ref) => {\n  return <SheetPrimitive.Close ref={ref} data-slot=\"sheet-close\" {...props} />;\n});\nSheetClose.displayName = \"SheetClose\";\n\nconst SheetPortal = ({ ...props }: React.ComponentProps<typeof SheetPrimitive.Portal>) => {\n  return <SheetPrimitive.Portal data-slot=\"sheet-portal\" {...props} />;\n};\n\nconst SheetOverlay = React.forwardRef<\n  React.ElementRef<typeof SheetPrimitive.Overlay>,\n  React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>\n>(({ className, ...props }, ref) => {\n  return (\n    <SheetPrimitive.Overlay\n      ref={ref}\n      data-slot=\"sheet-overlay\"\n      className={cn(\n        \"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-foreground/50\",\n        className,\n      )}\n      {...props}\n    />\n  );\n});\nSheetOverlay.displayName = \"SheetOverlay\";\n\nconst SheetContent = React.forwardRef<\n  React.ElementRef<typeof SheetPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content> & {\n    side?: \"top\" | \"right\" | \"bottom\" | \"left\";\n  }\n>(({ className, children, side = \"right\", ...props }, ref) => {\n  return (\n    <SheetPortal>\n      <SheetOverlay />\n      <SheetPrimitive.Content\n        ref={ref}\n        data-slot=\"sheet-content\"\n        className={cn(\n          \"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500\",\n          side === \"right\" &&\n            \"data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm\",\n          side === \"left\" &&\n            \"data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm\",\n          side === \"top\" && \"data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b\",\n          side === \"bottom\" &&\n            \"data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t\",\n          className,\n        )}\n        onOpenAutoFocus={(e) => {\n          e.preventDefault();\n        }}\n        onCloseAutoFocus={(e) => {\n          e.preventDefault();\n          document.body.style.pointerEvents = \"auto\";\n        }}\n        {...props}\n      >\n        {children}\n        <SheetPrimitive.Close className=\"ring-offset-background data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-60 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none\">\n          <XIcon className=\"size-5\" />\n          <span className=\"sr-only\">Close</span>\n        </SheetPrimitive.Close>\n      </SheetPrimitive.Content>\n    </SheetPortal>\n  );\n});\nSheetContent.displayName = \"SheetContent\";\n\nconst SheetHeader = React.forwardRef<React.ElementRef<\"div\">, React.ComponentPropsWithoutRef<\"div\">>(({ className, ...props }, ref) => {\n  return <div ref={ref} data-slot=\"sheet-header\" className={cn(\"flex flex-col gap-1.5 p-4\", className)} {...props} />;\n});\nSheetHeader.displayName = \"SheetHeader\";\n\nconst SheetFooter = React.forwardRef<React.ElementRef<\"div\">, React.ComponentPropsWithoutRef<\"div\">>(({ className, ...props }, ref) => {\n  return <div ref={ref} data-slot=\"sheet-footer\" className={cn(\"mt-auto flex flex-col gap-2 p-4\", className)} {...props} />;\n});\nSheetFooter.displayName = \"SheetFooter\";\n\nconst SheetTitle = React.forwardRef<\n  React.ElementRef<typeof SheetPrimitive.Title>,\n  React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>\n>(({ className, ...props }, ref) => {\n  return <SheetPrimitive.Title ref={ref} data-slot=\"sheet-title\" className={cn(\"text-foreground font-semibold\", className)} {...props} />;\n});\nSheetTitle.displayName = \"SheetTitle\";\n\nconst SheetDescription = React.forwardRef<\n  React.ElementRef<typeof SheetPrimitive.Description>,\n  React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>\n>(({ className, ...props }, ref) => {\n  return (\n    <SheetPrimitive.Description\n      ref={ref}\n      data-slot=\"sheet-description\"\n      className={cn(\"text-muted-foreground text-sm\", className)}\n      {...props}\n    />\n  );\n});\nSheetDescription.displayName = \"SheetDescription\";\n\nexport { Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle, SheetTrigger };\n"
  },
  {
    "path": "web/src/components/ui/switch.tsx",
    "content": "import * as SwitchPrimitive from \"@radix-ui/react-switch\";\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nfunction Switch({ className, ...props }: React.ComponentProps<typeof SwitchPrimitive.Root>) {\n  return (\n    <SwitchPrimitive.Root\n      data-slot=\"switch\"\n      className={cn(\n        \"peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all disabled:cursor-not-allowed disabled:opacity-50\",\n        className,\n      )}\n      {...props}\n    >\n      <SwitchPrimitive.Thumb\n        data-slot=\"switch-thumb\"\n        className={cn(\n          \"bg-background pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0\",\n        )}\n      />\n    </SwitchPrimitive.Root>\n  );\n}\n\nexport { Switch };\n"
  },
  {
    "path": "web/src/components/ui/textarea.tsx",
    "content": "import * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nfunction Textarea({ className, ...props }: React.ComponentProps<\"textarea\">) {\n  return (\n    <textarea\n      data-slot=\"textarea\"\n      className={cn(\n        \"border-border placeholder:text-muted-foreground flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport { Textarea };\n"
  },
  {
    "path": "web/src/components/ui/tooltip.tsx",
    "content": "import * as TooltipPrimitive from \"@radix-ui/react-tooltip\";\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nconst TooltipProvider = React.forwardRef<\n  React.ElementRef<typeof TooltipPrimitive.Provider>,\n  React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Provider>\n  // eslint-disable-next-line @typescript-eslint/no-unused-vars\n>(({ delayDuration = 0, ...props }, _ref) => {\n  return <TooltipPrimitive.Provider data-slot=\"tooltip-provider\" delayDuration={delayDuration} {...props} />;\n});\nTooltipProvider.displayName = \"TooltipProvider\";\n\nconst Tooltip = React.forwardRef<\n  React.ElementRef<typeof TooltipPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Root>\n  // eslint-disable-next-line @typescript-eslint/no-unused-vars\n>(({ ...props }, _ref) => {\n  return (\n    <TooltipProvider>\n      <TooltipPrimitive.Root data-slot=\"tooltip\" {...props} />\n    </TooltipProvider>\n  );\n});\nTooltip.displayName = \"Tooltip\";\n\nconst TooltipTrigger = React.forwardRef<\n  React.ElementRef<typeof TooltipPrimitive.Trigger>,\n  React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Trigger>\n>(({ ...props }, ref) => {\n  return <TooltipPrimitive.Trigger ref={ref} data-slot=\"tooltip-trigger\" {...props} />;\n});\nTooltipTrigger.displayName = \"TooltipTrigger\";\n\nconst TooltipContent = React.forwardRef<\n  React.ElementRef<typeof TooltipPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>\n>(({ className, sideOffset = 0, children, ...props }, ref) => {\n  return (\n    <TooltipPrimitive.Portal>\n      <TooltipPrimitive.Content\n        ref={ref}\n        data-slot=\"tooltip-content\"\n        sideOffset={sideOffset}\n        className={cn(\n          \"bg-primary text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-[70] w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance\",\n          className,\n        )}\n        {...props}\n      >\n        {children}\n        <TooltipPrimitive.Arrow className=\"bg-primary fill-primary z-[70] size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]\" />\n      </TooltipPrimitive.Content>\n    </TooltipPrimitive.Portal>\n  );\n});\nTooltipContent.displayName = \"TooltipContent\";\n\nexport { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger };\n"
  },
  {
    "path": "web/src/components/ui/visually-hidden.tsx",
    "content": "import * as React from \"react\";\n\nexport const VisuallyHidden = React.forwardRef<HTMLSpanElement, React.HTMLAttributes<HTMLSpanElement>>(({ children, ...props }, ref) => {\n  return (\n    <span\n      ref={ref}\n      className=\"absolute w-px h-px p-0 -m-px overflow-hidden whitespace-nowrap border-0\"\n      style={{\n        clip: \"rect(0, 0, 0, 0)\",\n        clipPath: \"inset(50%)\",\n      }}\n      {...props}\n    >\n      {children}\n    </span>\n  );\n});\n\nVisuallyHidden.displayName = \"VisuallyHidden\";\n"
  },
  {
    "path": "web/src/connect.ts",
    "content": "import { timestampDate } from \"@bufbuild/protobuf/wkt\";\nimport { Code, ConnectError, createClient, type Interceptor } from \"@connectrpc/connect\";\nimport { createConnectTransport } from \"@connectrpc/connect-web\";\nimport { getAccessToken, hasStoredToken, isTokenExpired, REQUEST_TOKEN_EXPIRY_BUFFER_MS, setAccessToken } from \"./auth-state\";\nimport { AttachmentService } from \"./types/proto/api/v1/attachment_service_pb\";\nimport { AuthService } from \"./types/proto/api/v1/auth_service_pb\";\nimport { IdentityProviderService } from \"./types/proto/api/v1/idp_service_pb\";\nimport { InstanceService } from \"./types/proto/api/v1/instance_service_pb\";\nimport { MemoService } from \"./types/proto/api/v1/memo_service_pb\";\nimport { ShortcutService } from \"./types/proto/api/v1/shortcut_service_pb\";\nimport { UserService } from \"./types/proto/api/v1/user_service_pb\";\nimport { redirectOnAuthFailure } from \"./utils/auth-redirect\";\n\ninterface RequestWithHeader {\n  header: Headers;\n}\n\n// ============================================================================\n// Constants\n// ============================================================================\n\nconst RETRY_HEADER = \"X-Retry\";\nconst RETRY_HEADER_VALUE = \"true\";\n\n// ============================================================================\n// Token Refresh State Management\n// ============================================================================\n\nconst createTokenRefreshManager = () => {\n  let isRefreshing = false;\n  let refreshPromise: Promise<void> | null = null;\n\n  return {\n    async refresh(refreshFn: () => Promise<void>): Promise<void> {\n      if (isRefreshing && refreshPromise) {\n        return refreshPromise;\n      }\n\n      isRefreshing = true;\n      refreshPromise = refreshFn().finally(() => {\n        isRefreshing = false;\n        refreshPromise = null;\n      });\n\n      return refreshPromise;\n    },\n  };\n};\n\nconst tokenRefreshManager = createTokenRefreshManager();\n\n// ============================================================================\n// Token Refresh\n// ============================================================================\n\nconst fetchWithCredentials: typeof globalThis.fetch = (input, init) => {\n  return globalThis.fetch(input, {\n    ...init,\n    credentials: \"include\",\n  });\n};\n\n// Separate transport without auth interceptor to prevent recursion\nconst refreshTransport = createConnectTransport({\n  baseUrl: window.location.origin,\n  useBinaryFormat: true,\n  fetch: fetchWithCredentials,\n  interceptors: [],\n});\n\nconst refreshAuthClient = createClient(AuthService, refreshTransport);\n\nasync function doRefreshAccessToken(): Promise<void> {\n  const response = await refreshAuthClient.refreshToken({});\n\n  if (!response.accessToken) {\n    throw new ConnectError(\"Refresh token response missing access token\", Code.Internal);\n  }\n\n  const expiresAt = response.expiresAt ? timestampDate(response.expiresAt) : undefined;\n  setAccessToken(response.accessToken, expiresAt);\n}\n\n// All callers go through the manager to deduplicate concurrent refresh requests.\n// This prevents race conditions between useTokenRefreshOnFocus (proactive refresh\n// on tab focus) and the auth interceptor (reactive refresh on 401), which could\n// otherwise send duplicate requests that conflict with server-side token rotation.\nexport async function refreshAccessToken(): Promise<void> {\n  return tokenRefreshManager.refresh(doRefreshAccessToken);\n}\n\n// ============================================================================\n// Authentication Interceptor Helpers\n// ============================================================================\n\nfunction setAuthorizationHeader(req: RequestWithHeader, token: string | null) {\n  if (!token) return;\n  req.header.set(\"Authorization\", `Bearer ${token}`);\n}\n\nfunction shouldHandleUnauthenticatedRetry(error: unknown, isRetryAttempt: boolean): boolean {\n  if (!(error instanceof ConnectError)) {\n    return false;\n  }\n  if (error.code !== Code.Unauthenticated) {\n    return false;\n  }\n  if (isRetryAttempt) {\n    return false;\n  }\n  return true;\n}\n\nasync function refreshAndGetAccessToken(): Promise<string> {\n  await refreshAccessToken();\n  const token = getAccessToken();\n  if (!token) {\n    throw new ConnectError(\"Token refresh succeeded but no token available\", Code.Internal);\n  }\n  return token;\n}\n\nasync function getRequestToken(): Promise<string | null> {\n  let token = getAccessToken();\n  if (!token) {\n    if (!hasStoredToken()) return null;\n    try {\n      token = await refreshAndGetAccessToken();\n    } catch {\n      return null;\n    }\n    return token;\n  }\n\n  // Preflight refresh: avoid sending requests with expired access tokens.\n  // This is especially important for public endpoints (e.g. ListMemos), where\n  // an expired token could otherwise be treated as anonymous and return\n  // guest-scoped data before the reactive 401 refresh path runs.\n  if (isTokenExpired(REQUEST_TOKEN_EXPIRY_BUFFER_MS)) {\n    try {\n      token = await refreshAndGetAccessToken();\n    } catch {\n      // Keep existing reactive 401 flow as fallback.\n      // Protected methods still trigger refresh/redirect in the catch block below.\n    }\n  }\n\n  return token;\n}\n\n// ============================================================================\n// Authentication Interceptor\n// ============================================================================\n\nconst authInterceptor: Interceptor = (next) => async (req) => {\n  const isRetryAttempt = req.header.get(RETRY_HEADER) === RETRY_HEADER_VALUE;\n  const token = await getRequestToken();\n  setAuthorizationHeader(req, token);\n\n  try {\n    return await next(req);\n  } catch (error) {\n    if (!shouldHandleUnauthenticatedRetry(error, isRetryAttempt)) {\n      throw error;\n    }\n\n    try {\n      const newToken = await refreshAndGetAccessToken();\n      setAuthorizationHeader(req, newToken);\n      req.header.set(RETRY_HEADER, RETRY_HEADER_VALUE);\n      return await next(req);\n    } catch (refreshError) {\n      redirectOnAuthFailure();\n      throw refreshError;\n    }\n  }\n};\n\n// ============================================================================\n// Transport & Service Clients\n// ============================================================================\n\nconst transport = createConnectTransport({\n  baseUrl: window.location.origin,\n  useBinaryFormat: true,\n  fetch: fetchWithCredentials,\n  interceptors: [authInterceptor],\n});\n\n// Core service clients\nexport const instanceServiceClient = createClient(InstanceService, transport);\nexport const authServiceClient = createClient(AuthService, transport);\nexport const userServiceClient = createClient(UserService, transport);\n\n// Content service clients\nexport const memoServiceClient = createClient(MemoService, transport);\nexport const attachmentServiceClient = createClient(AttachmentService, transport);\nexport const shortcutServiceClient = createClient(ShortcutService, transport);\n\n// Configuration service clients\nexport const identityProviderServiceClient = createClient(IdentityProviderService, transport);\n"
  },
  {
    "path": "web/src/contexts/AuthContext.tsx",
    "content": "import { useQueryClient } from \"@tanstack/react-query\";\nimport { createContext, type ReactNode, useCallback, useContext, useMemo, useState } from \"react\";\nimport { clearAccessToken, hasStoredToken } from \"@/auth-state\";\nimport { authServiceClient, shortcutServiceClient, userServiceClient } from \"@/connect\";\nimport { userKeys } from \"@/hooks/useUserQueries\";\nimport type { Shortcut } from \"@/types/proto/api/v1/shortcut_service_pb\";\nimport type { User, UserSetting_GeneralSetting, UserSetting_WebhooksSetting } from \"@/types/proto/api/v1/user_service_pb\";\n\ninterface AuthState {\n  currentUser: User | undefined;\n  userGeneralSetting: UserSetting_GeneralSetting | undefined;\n  userWebhooksSetting: UserSetting_WebhooksSetting | undefined;\n  shortcuts: Shortcut[];\n  isInitialized: boolean;\n  isLoading: boolean;\n}\n\ninterface AuthContextValue extends AuthState {\n  initialize: () => Promise<void>;\n  logout: () => Promise<void>;\n  refetchSettings: () => Promise<void>;\n}\n\nconst AuthContext = createContext<AuthContextValue | null>(null);\n\nexport function AuthProvider({ children }: { children: ReactNode }) {\n  const queryClient = useQueryClient();\n  const [state, setState] = useState<AuthState>({\n    currentUser: undefined,\n    userGeneralSetting: undefined,\n    userWebhooksSetting: undefined,\n    shortcuts: [],\n    isInitialized: false,\n    isLoading: true,\n  });\n\n  const fetchUserSettings = useCallback(async (userName: string) => {\n    const [{ settings }, { shortcuts }] = await Promise.all([\n      userServiceClient.listUserSettings({ parent: userName }),\n      shortcutServiceClient.listShortcuts({ parent: userName }),\n    ]);\n\n    const generalSetting = settings.find((s) => s.value.case === \"generalSetting\");\n    const webhooksSetting = settings.find((s) => s.value.case === \"webhooksSetting\");\n\n    return {\n      userGeneralSetting: generalSetting?.value.case === \"generalSetting\" ? generalSetting.value.value : undefined,\n      userWebhooksSetting: webhooksSetting?.value.case === \"webhooksSetting\" ? webhooksSetting.value.value : undefined,\n      shortcuts,\n    };\n  }, []);\n\n  const initialize = useCallback(async () => {\n    setState((prev) => ({ ...prev, isLoading: true }));\n\n    // If there is no stored token at all, the user is not authenticated.\n    // Skip the network call — there is nothing to refresh and no session to restore.\n    if (!hasStoredToken()) {\n      setState({\n        currentUser: undefined,\n        userGeneralSetting: undefined,\n        userWebhooksSetting: undefined,\n        shortcuts: [],\n        isInitialized: true,\n        isLoading: false,\n      });\n      return;\n    }\n\n    try {\n      const { user: currentUser } = await authServiceClient.getCurrentUser({});\n\n      if (!currentUser) {\n        clearAccessToken();\n        setState({\n          currentUser: undefined,\n          userGeneralSetting: undefined,\n          userWebhooksSetting: undefined,\n          shortcuts: [],\n          isInitialized: true,\n          isLoading: false,\n        });\n        return;\n      }\n\n      const settings = await fetchUserSettings(currentUser.name);\n\n      setState({\n        currentUser,\n        ...settings,\n        isInitialized: true,\n        isLoading: false,\n      });\n\n      // Pre-populate React Query cache\n      queryClient.setQueryData(userKeys.currentUser(), currentUser);\n      queryClient.setQueryData(userKeys.detail(currentUser.name), currentUser);\n    } catch (error) {\n      console.error(\"Failed to initialize auth:\", error);\n      clearAccessToken();\n      setState({\n        currentUser: undefined,\n        userGeneralSetting: undefined,\n        userWebhooksSetting: undefined,\n        shortcuts: [],\n        isInitialized: true,\n        isLoading: false,\n      });\n    }\n  }, [fetchUserSettings, queryClient]);\n\n  const logout = useCallback(async () => {\n    try {\n      await authServiceClient.signOut({});\n    } catch (error) {\n      console.error(\"[AuthContext] Failed to sign out:\", error);\n    } finally {\n      clearAccessToken();\n      setState({\n        currentUser: undefined,\n        userGeneralSetting: undefined,\n        userWebhooksSetting: undefined,\n        shortcuts: [],\n        isInitialized: true,\n        isLoading: false,\n      });\n      queryClient.clear();\n    }\n  }, [queryClient]);\n\n  const refetchSettings = useCallback(async () => {\n    // Use functional setState to get current user without including state in dependencies\n    setState((prev) => {\n      if (!prev.currentUser) return prev;\n\n      // Fetch settings asynchronously\n      fetchUserSettings(prev.currentUser.name).then((settings) => {\n        setState((current) => ({ ...current, ...settings }));\n      });\n\n      return prev;\n    });\n  }, [fetchUserSettings]);\n\n  // Memoize context value to prevent unnecessary re-renders of consumers\n  const value = useMemo(\n    () => ({\n      ...state,\n      initialize,\n      logout,\n      refetchSettings,\n    }),\n    [state, initialize, logout, refetchSettings],\n  );\n\n  return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;\n}\n\nexport function useAuth() {\n  const context = useContext(AuthContext);\n  if (!context) {\n    throw new Error(\"useAuth must be used within AuthProvider\");\n  }\n  return context;\n}\n\n// Convenience hook for just the current user\nexport function useCurrentUserFromAuth() {\n  const { currentUser } = useAuth();\n  return currentUser;\n}\n"
  },
  {
    "path": "web/src/contexts/InstanceContext.tsx",
    "content": "import { create } from \"@bufbuild/protobuf\";\nimport { createContext, type ReactNode, useCallback, useContext, useMemo, useState } from \"react\";\nimport { instanceServiceClient } from \"@/connect\";\nimport {\n  InstanceProfile,\n  InstanceProfileSchema,\n  InstanceSetting,\n  InstanceSetting_GeneralSetting,\n  InstanceSetting_GeneralSettingSchema,\n  InstanceSetting_Key,\n  InstanceSetting_MemoRelatedSetting,\n  InstanceSetting_MemoRelatedSettingSchema,\n  InstanceSetting_StorageSetting,\n  InstanceSetting_StorageSettingSchema,\n} from \"@/types/proto/api/v1/instance_service_pb\";\n\nconst instanceSettingNamePrefix = \"instance/settings/\";\n\nconst buildInstanceSettingName = (key: InstanceSetting_Key): string => {\n  const keyName = InstanceSetting_Key[key];\n  return `${instanceSettingNamePrefix}${keyName}`;\n};\n\ninterface InstanceState {\n  profile: InstanceProfile;\n  settings: InstanceSetting[];\n  isInitialized: boolean;\n  isLoading: boolean;\n  // True only when the profile was successfully fetched from the server.\n  // Remains false if initialization failed, so consumers can distinguish\n  // \"no admin exists\" from \"failed to load profile\".\n  profileLoaded: boolean;\n}\n\ninterface InstanceContextValue extends InstanceState {\n  generalSetting: InstanceSetting_GeneralSetting;\n  memoRelatedSetting: InstanceSetting_MemoRelatedSetting;\n  storageSetting: InstanceSetting_StorageSetting;\n  initialize: () => Promise<void>;\n  fetchSetting: (key: InstanceSetting_Key) => Promise<void>;\n  updateSetting: (setting: InstanceSetting) => Promise<void>;\n}\n\nconst InstanceContext = createContext<InstanceContextValue | null>(null);\n\nexport function InstanceProvider({ children }: { children: ReactNode }) {\n  const [state, setState] = useState<InstanceState>({\n    profile: create(InstanceProfileSchema, {}),\n    settings: [],\n    isInitialized: false,\n    isLoading: true,\n    profileLoaded: false,\n  });\n\n  // Memoize derived settings to prevent unnecessary recalculations\n  const generalSetting = useMemo((): InstanceSetting_GeneralSetting => {\n    const setting = state.settings.find((s) => s.name === `${instanceSettingNamePrefix}GENERAL`);\n    if (setting?.value.case === \"generalSetting\") {\n      return setting.value.value;\n    }\n    return create(InstanceSetting_GeneralSettingSchema, {});\n  }, [state.settings]);\n\n  const memoRelatedSetting = useMemo((): InstanceSetting_MemoRelatedSetting => {\n    const setting = state.settings.find((s) => s.name === `${instanceSettingNamePrefix}MEMO_RELATED`);\n    if (setting?.value.case === \"memoRelatedSetting\") {\n      return setting.value.value;\n    }\n    return create(InstanceSetting_MemoRelatedSettingSchema, {});\n  }, [state.settings]);\n\n  const storageSetting = useMemo((): InstanceSetting_StorageSetting => {\n    const setting = state.settings.find((s) => s.name === `${instanceSettingNamePrefix}STORAGE`);\n    if (setting?.value.case === \"storageSetting\") {\n      return setting.value.value;\n    }\n    return create(InstanceSetting_StorageSettingSchema, {});\n  }, [state.settings]);\n\n  const initialize = useCallback(async () => {\n    setState((prev) => ({ ...prev, isLoading: true }));\n    try {\n      const profile = await instanceServiceClient.getInstanceProfile({});\n\n      const [generalSetting, memoRelatedSettingResponse] = await Promise.all([\n        instanceServiceClient.getInstanceSetting({ name: buildInstanceSettingName(InstanceSetting_Key.GENERAL) }),\n        instanceServiceClient.getInstanceSetting({ name: buildInstanceSettingName(InstanceSetting_Key.MEMO_RELATED) }),\n      ]);\n\n      setState({\n        profile,\n        settings: [generalSetting, memoRelatedSettingResponse],\n        isInitialized: true,\n        isLoading: false,\n        profileLoaded: true,\n      });\n    } catch (error) {\n      console.error(\"Failed to initialize instance:\", error);\n      setState((prev) => ({\n        ...prev,\n        isInitialized: true,\n        isLoading: false,\n      }));\n    }\n  }, []);\n\n  const fetchSetting = useCallback(async (key: InstanceSetting_Key) => {\n    const setting = await instanceServiceClient.getInstanceSetting({\n      name: buildInstanceSettingName(key),\n    });\n    setState((prev) => ({\n      ...prev,\n      settings: [...prev.settings.filter((s) => s.name !== setting.name), setting],\n    }));\n  }, []);\n\n  const updateSetting = useCallback(async (setting: InstanceSetting) => {\n    await instanceServiceClient.updateInstanceSetting({ setting });\n    setState((prev) => ({\n      ...prev,\n      settings: [...prev.settings.filter((s) => s.name !== setting.name), setting],\n    }));\n  }, []);\n\n  // Memoize context value to prevent unnecessary re-renders of consumers\n  const value = useMemo(\n    () => ({\n      ...state,\n      generalSetting,\n      memoRelatedSetting,\n      storageSetting,\n      initialize,\n      fetchSetting,\n      updateSetting,\n    }),\n    [state, generalSetting, memoRelatedSetting, storageSetting, initialize, fetchSetting, updateSetting],\n  );\n\n  return <InstanceContext.Provider value={value}>{children}</InstanceContext.Provider>;\n}\n\nexport function useInstance() {\n  const context = useContext(InstanceContext);\n  if (!context) {\n    throw new Error(\"useInstance must be used within InstanceProvider\");\n  }\n  return context;\n}\n"
  },
  {
    "path": "web/src/contexts/MemoFilterContext.tsx",
    "content": "import { uniqBy } from \"lodash-es\";\nimport { createContext, type ReactNode, useCallback, useContext, useEffect, useRef, useState } from \"react\";\nimport { useSearchParams } from \"react-router-dom\";\n\nexport type FilterFactor =\n  | \"tagSearch\"\n  | \"visibility\"\n  | \"contentSearch\"\n  | \"displayTime\"\n  | \"pinned\"\n  | \"property.hasLink\"\n  | \"property.hasTaskList\"\n  | \"property.hasCode\";\n\nexport interface MemoFilter {\n  factor: FilterFactor;\n  value: string;\n}\n\nexport const getMemoFilterKey = (filter: MemoFilter): string => `${filter.factor}:${filter.value}`;\n\nexport const parseFilterQuery = (query: string | null): MemoFilter[] => {\n  if (!query) return [];\n  try {\n    return query.split(\",\").map((filterStr) => {\n      const [factor, value] = filterStr.split(\":\");\n      return {\n        factor: factor as FilterFactor,\n        value: decodeURIComponent(value || \"\"),\n      };\n    });\n  } catch {\n    return [];\n  }\n};\n\nexport const stringifyFilters = (filters: MemoFilter[]): string => {\n  return filters.map((filter) => `${filter.factor}:${encodeURIComponent(filter.value)}`).join(\",\");\n};\n\ninterface MemoFilterContextValue {\n  filters: MemoFilter[];\n  shortcut: string | undefined;\n  hasActiveFilters: boolean;\n  getFiltersByFactor: (factor: FilterFactor) => MemoFilter[];\n  setFilters: (filters: MemoFilter[]) => void;\n  addFilter: (filter: MemoFilter) => void;\n  removeFilter: (predicate: (f: MemoFilter) => boolean) => void;\n  removeFiltersByFactor: (factor: FilterFactor) => void;\n  clearAllFilters: () => void;\n  setShortcut: (shortcut?: string) => void;\n  hasFilter: (filter: MemoFilter) => boolean;\n}\n\nconst MemoFilterContext = createContext<MemoFilterContextValue | null>(null);\n\nexport function MemoFilterProvider({ children }: { children: ReactNode }) {\n  const [searchParams, setSearchParams] = useSearchParams();\n  const lastSyncedUrlRef = useRef(\"\");\n  const lastSyncedStoreRef = useRef(\"\");\n\n  // Initialize from URL\n  const [filters, setFiltersState] = useState<MemoFilter[]>(() => {\n    return parseFilterQuery(searchParams.get(\"filter\"));\n  });\n  const [shortcut, setShortcutState] = useState<string | undefined>(undefined);\n\n  // Sync URL to state when URL changes externally\n  useEffect(() => {\n    const filterParam = searchParams.get(\"filter\") || \"\";\n    if (filterParam !== lastSyncedUrlRef.current) {\n      lastSyncedUrlRef.current = filterParam;\n      const newFilters = parseFilterQuery(filterParam);\n      setFiltersState(newFilters);\n      lastSyncedStoreRef.current = stringifyFilters(newFilters);\n    }\n  }, [searchParams]);\n\n  // Sync state to URL when state changes\n  useEffect(() => {\n    const storeString = stringifyFilters(filters);\n    if (storeString !== lastSyncedStoreRef.current && storeString !== lastSyncedUrlRef.current) {\n      lastSyncedStoreRef.current = storeString;\n      const newParams = new URLSearchParams(searchParams);\n      if (filters.length > 0) {\n        newParams.set(\"filter\", storeString);\n      } else {\n        newParams.delete(\"filter\");\n      }\n      setSearchParams(newParams, { replace: true });\n      lastSyncedUrlRef.current = filters.length > 0 ? storeString : \"\";\n    }\n  }, [filters, searchParams, setSearchParams]);\n\n  const getFiltersByFactor = useCallback((factor: FilterFactor) => filters.filter((f) => f.factor === factor), [filters]);\n\n  const setFilters = useCallback((newFilters: MemoFilter[]) => {\n    setFiltersState(newFilters);\n  }, []);\n\n  const addFilter = useCallback((filter: MemoFilter) => {\n    setFiltersState((prev) => uniqBy([...prev, filter], getMemoFilterKey));\n  }, []);\n\n  const removeFilter = useCallback((predicate: (f: MemoFilter) => boolean) => {\n    setFiltersState((prev) => prev.filter((f) => !predicate(f)));\n  }, []);\n\n  const removeFiltersByFactor = useCallback((factor: FilterFactor) => {\n    setFiltersState((prev) => prev.filter((f) => f.factor !== factor));\n  }, []);\n\n  const clearAllFilters = useCallback(() => {\n    setFiltersState([]);\n    setShortcutState(undefined);\n  }, []);\n\n  const setShortcut = useCallback((newShortcut?: string) => {\n    setShortcutState(newShortcut);\n    // Clear content search filter when selecting a shortcut (issue #5462)\n    if (newShortcut !== undefined) {\n      setFiltersState((prev) => prev.filter((f) => f.factor !== \"contentSearch\"));\n    }\n  }, []);\n\n  const hasFilter = useCallback((filter: MemoFilter) => filters.some((f) => getMemoFilterKey(f) === getMemoFilterKey(filter)), [filters]);\n\n  const hasActiveFilters = filters.length > 0 || shortcut !== undefined;\n\n  return (\n    <MemoFilterContext.Provider\n      value={{\n        filters,\n        shortcut,\n        hasActiveFilters,\n        getFiltersByFactor,\n        setFilters,\n        addFilter,\n        removeFilter,\n        removeFiltersByFactor,\n        clearAllFilters,\n        setShortcut,\n        hasFilter,\n      }}\n    >\n      {children}\n    </MemoFilterContext.Provider>\n  );\n}\n\nexport function useMemoFilterContext() {\n  const context = useContext(MemoFilterContext);\n  if (!context) {\n    throw new Error(\"useMemoFilterContext must be used within MemoFilterProvider\");\n  }\n  return context;\n}\n\n// Alias for backwards compatibility during migration\nexport const useMemoFilter = useMemoFilterContext;\n"
  },
  {
    "path": "web/src/contexts/ViewContext.tsx",
    "content": "import { createContext, type ReactNode, useContext, useState } from \"react\";\n\ninterface ViewContextValue {\n  orderByTimeAsc: boolean;\n  toggleSortOrder: () => void;\n}\n\nconst ViewContext = createContext<ViewContextValue | null>(null);\n\nconst LOCAL_STORAGE_KEY = \"memos-view-setting\";\n\nexport function ViewProvider({ children }: { children: ReactNode }) {\n  const getInitialState = () => {\n    try {\n      const cached = localStorage.getItem(LOCAL_STORAGE_KEY);\n      if (cached) {\n        const data = JSON.parse(cached);\n        return {\n          orderByTimeAsc: Boolean(data.orderByTimeAsc ?? false),\n        };\n      }\n    } catch (error) {\n      console.warn(\"Failed to load view settings from localStorage:\", error);\n    }\n    return { orderByTimeAsc: false };\n  };\n\n  const [viewState, setViewState] = useState(getInitialState);\n\n  const persistToStorage = (newState: typeof viewState) => {\n    try {\n      localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(newState));\n    } catch (error) {\n      console.warn(\"Failed to persist view settings:\", error);\n    }\n  };\n\n  const toggleSortOrder = () => {\n    setViewState((prev) => {\n      const newState = { ...prev, orderByTimeAsc: !prev.orderByTimeAsc };\n      persistToStorage(newState);\n      return newState;\n    });\n  };\n\n  return (\n    <ViewContext.Provider\n      value={{\n        ...viewState,\n        toggleSortOrder,\n      }}\n    >\n      {children}\n    </ViewContext.Provider>\n  );\n}\n\nexport function useView() {\n  const context = useContext(ViewContext);\n  if (!context) {\n    throw new Error(\"useView must be used within ViewProvider\");\n  }\n  return context;\n}\n"
  },
  {
    "path": "web/src/helpers/consts.ts",
    "content": "// TAB_SPACE_WIDTH is the default tab space width.\nexport const TAB_SPACE_WIDTH = 2;\n\n// DEFAULT_LIST_MEMOS_PAGE_SIZE is the default page size for list memos request.\nexport const DEFAULT_LIST_MEMOS_PAGE_SIZE = 16;\n"
  },
  {
    "path": "web/src/helpers/resource-names.ts",
    "content": "import { InstanceSetting_Key } from \"@/types/proto/api/v1/instance_service_pb\";\nimport { Visibility } from \"@/types/proto/api/v1/memo_service_pb\";\nimport { UserSetting_Key } from \"@/types/proto/api/v1/user_service_pb\";\n\nexport const instanceSettingNamePrefix = \"instance/settings/\";\nexport const userNamePrefix = \"users/\";\nexport const memoNamePrefix = \"memos/\";\nexport const identityProviderNamePrefix = \"identity-providers/\";\n\nexport const extractUserIdFromName = (name: string) => {\n  return name.split(userNamePrefix).pop() || \"\";\n};\n\nexport const extractMemoIdFromName = (name: string) => {\n  return name.split(memoNamePrefix).pop() || \"\";\n};\n\nexport const extractIdentityProviderUidFromName = (name: string) => {\n  return name.split(identityProviderNamePrefix).pop() || \"\";\n};\n\n// Helper function to convert InstanceSetting_Key enum value to string name\nexport const getInstanceSettingKeyName = (key: InstanceSetting_Key): string => {\n  // TypeScript enum reverse mapping: converts numeric value to string name\n  return InstanceSetting_Key[key];\n};\n\n// Helper function to build instance setting name from key\nexport const buildInstanceSettingName = (key: InstanceSetting_Key): string => {\n  return `${instanceSettingNamePrefix}${getInstanceSettingKeyName(key)}`;\n};\n\n// Helper function to convert UserSetting_Key enum value to string name\nexport const getUserSettingKeyName = (key: UserSetting_Key): string => {\n  // TypeScript enum reverse mapping: converts numeric value to string name\n  return UserSetting_Key[key];\n};\n\n// Helper function to build user setting name from username and key\nexport const buildUserSettingName = (username: string, key: UserSetting_Key): string => {\n  return `${username}/settings/${getUserSettingKeyName(key)}`;\n};\n\n// Helper function to convert Visibility enum value to string name\n// Used when building filter expressions that require string enum names instead of numeric values\n// Example: visibility in [\"PUBLIC\", \"PROTECTED\"] instead of visibility in [\"3\", \"2\"]\nexport const getVisibilityName = (visibility: Visibility): string => {\n  // TypeScript enum reverse mapping: converts numeric value to string name\n  // e.g., Visibility.PUBLIC (3) -> \"PUBLIC\"\n  const name = Visibility[visibility];\n  if (!name) {\n    console.warn(`Invalid visibility value: ${visibility}, defaulting to PUBLIC`);\n    return \"PUBLIC\";\n  }\n  return name;\n};\n"
  },
  {
    "path": "web/src/helpers/utils.ts",
    "content": "export function absolutifyLink(rel: string): string {\n  const anchor = document.createElement(\"a\");\n  anchor.setAttribute(\"href\", rel);\n  return anchor.href;\n}\n\nexport function getSystemColorScheme() {\n  if (window.matchMedia && window.matchMedia(\"(prefers-color-scheme: dark)\").matches) {\n    return \"dark\";\n  } else {\n    return \"light\";\n  }\n}\n\nexport function convertFileToBase64(file: File): Promise<string> {\n  return new Promise<string>((resolve, reject) => {\n    const reader = new FileReader();\n    reader.readAsDataURL(file);\n    reader.onload = () => resolve(reader.result?.toString() || \"\");\n    reader.onerror = (error) => reject(error);\n  });\n}\n\nexport const isValidUrl = (url: string): boolean => {\n  try {\n    new URL(url);\n    return true;\n  } catch {\n    return false;\n  }\n};\n\nexport const downloadFileFromUrl = (url: string, filename: string) => {\n  const a = document.createElement(\"a\");\n  a.href = url;\n  a.download = filename;\n  a.click();\n  a.remove();\n};\n"
  },
  {
    "path": "web/src/hooks/index.ts",
    "content": "export * from \"./useAsyncEffect\";\nexport * from \"./useCurrentUser\";\nexport * from \"./useDateFilterNavigation\";\nexport * from \"./useFilteredMemoStats\";\nexport * from \"./useLoading\";\nexport * from \"./useMediaQuery\";\nexport * from \"./useMemoFilters\";\nexport * from \"./useMemoSorting\";\nexport * from \"./useNavigateTo\";\nexport * from \"./useUserLocale\";\nexport * from \"./useUserTheme\";\n"
  },
  {
    "path": "web/src/hooks/useAsyncEffect.ts",
    "content": "import { DependencyList, useEffect } from \"react\";\n\nconst useAsyncEffect = (effect: () => void | Promise<void>, deps?: DependencyList): void => {\n  useEffect(() => {\n    effect();\n  }, deps);\n};\n\nexport default useAsyncEffect;\n"
  },
  {
    "path": "web/src/hooks/useAttachmentQueries.ts",
    "content": "import { useMutation, useQuery, useQueryClient } from \"@tanstack/react-query\";\nimport { attachmentServiceClient } from \"@/connect\";\nimport type { Attachment, ListAttachmentsRequest } from \"@/types/proto/api/v1/attachment_service_pb\";\n\n// Query keys factory\nexport const attachmentKeys = {\n  all: [\"attachments\"] as const,\n  lists: () => [...attachmentKeys.all, \"list\"] as const,\n  list: (filters?: Partial<ListAttachmentsRequest>) => [...attachmentKeys.lists(), filters] as const,\n  details: () => [...attachmentKeys.all, \"detail\"] as const,\n  detail: (name: string) => [...attachmentKeys.details(), name] as const,\n};\n\n// Hook to fetch attachments\nexport function useAttachments() {\n  return useQuery({\n    queryKey: attachmentKeys.lists(),\n    queryFn: async () => {\n      const { attachments } = await attachmentServiceClient.listAttachments({});\n      return attachments;\n    },\n  });\n}\n\n// Hook to create/upload attachment\nexport function useCreateAttachment() {\n  const queryClient = useQueryClient();\n\n  return useMutation({\n    mutationFn: async (attachment: Attachment) => {\n      const result = await attachmentServiceClient.createAttachment({ attachment });\n      return result;\n    },\n    onSuccess: () => {\n      // Invalidate attachments list\n      queryClient.invalidateQueries({ queryKey: attachmentKeys.lists() });\n    },\n  });\n}\n\n// Hook to delete attachment\nexport function useDeleteAttachment() {\n  const queryClient = useQueryClient();\n\n  return useMutation({\n    mutationFn: async (name: string) => {\n      await attachmentServiceClient.deleteAttachment({ name });\n      return name;\n    },\n    onSuccess: (name) => {\n      // Remove from cache\n      queryClient.removeQueries({ queryKey: attachmentKeys.detail(name) });\n      // Invalidate lists\n      queryClient.invalidateQueries({ queryKey: attachmentKeys.lists() });\n    },\n  });\n}\n"
  },
  {
    "path": "web/src/hooks/useCurrentUser.ts",
    "content": "import { useAuth } from \"@/contexts/AuthContext\";\n\nconst useCurrentUser = () => {\n  const { currentUser } = useAuth();\n  return currentUser;\n};\n\nexport default useCurrentUser;\n"
  },
  {
    "path": "web/src/hooks/useDateFilterNavigation.ts",
    "content": "import { useCallback } from \"react\";\nimport { useLocation, useNavigate } from \"react-router-dom\";\nimport { stringifyFilters } from \"@/contexts/MemoFilterContext\";\n\nexport const useDateFilterNavigation = (targetPath?: string) => {\n  const navigate = useNavigate();\n  const location = useLocation();\n\n  const navigateToDateFilter = useCallback(\n    (date: string) => {\n      const filterQuery = stringifyFilters([{ factor: \"displayTime\", value: date }]);\n      const basePath = targetPath ?? location.pathname;\n      navigate(`${basePath}?filter=${filterQuery}`);\n    },\n    [navigate, location.pathname, targetPath],\n  );\n\n  return navigateToDateFilter;\n};\n"
  },
  {
    "path": "web/src/hooks/useDialog.ts",
    "content": "import { useCallback, useState } from \"react\";\n\nexport function useDialog(defaultOpen = false) {\n  const [isOpen, setIsOpen] = useState(defaultOpen);\n\n  const open = useCallback(() => setIsOpen(true), []);\n  const close = useCallback(() => setIsOpen(false), []);\n  const toggle = useCallback(() => setIsOpen((prev) => !prev), []);\n\n  return {\n    isOpen,\n    open,\n    close,\n    toggle,\n    setOpen: setIsOpen,\n  };\n}\n\nexport function useDialogs() {\n  const [openDialogs, setOpenDialogs] = useState<Set<string>>(new Set());\n\n  const isOpen = useCallback((key: string) => openDialogs.has(key), [openDialogs]);\n\n  const open = useCallback((key: string) => {\n    setOpenDialogs((prev) => new Set([...prev, key]));\n  }, []);\n\n  const close = useCallback((key: string) => {\n    setOpenDialogs((prev) => {\n      const next = new Set(prev);\n      next.delete(key);\n      return next;\n    });\n  }, []);\n\n  const toggle = useCallback((key: string) => {\n    setOpenDialogs((prev) => {\n      const next = new Set(prev);\n      if (next.has(key)) {\n        next.delete(key);\n      } else {\n        next.add(key);\n      }\n      return next;\n    });\n  }, []);\n\n  const setOpen = useCallback((key: string, open: boolean) => {\n    if (open) {\n      setOpenDialogs((prev) => new Set([...prev, key]));\n    } else {\n      setOpenDialogs((prev) => {\n        const next = new Set(prev);\n        next.delete(key);\n        return next;\n      });\n    }\n  }, []);\n\n  const closeAll = useCallback(() => {\n    setOpenDialogs(new Set());\n  }, []);\n\n  return {\n    isOpen,\n    open,\n    close,\n    toggle,\n    setOpen,\n    closeAll,\n    openDialogs: Array.from(openDialogs),\n  };\n}\n\nexport default useDialog;\n"
  },
  {
    "path": "web/src/hooks/useFilteredMemoStats.ts",
    "content": "import { timestampDate } from \"@bufbuild/protobuf/wkt\";\nimport dayjs from \"dayjs\";\nimport { countBy } from \"lodash-es\";\nimport { useMemo } from \"react\";\nimport type { MemoExplorerContext } from \"@/components/MemoExplorer\";\nimport useCurrentUser from \"@/hooks/useCurrentUser\";\nimport { useMemos } from \"@/hooks/useMemoQueries\";\nimport { useUserStats } from \"@/hooks/useUserQueries\";\nimport type { StatisticsData } from \"@/types/statistics\";\n\nexport interface FilteredMemoStats {\n  statistics: StatisticsData;\n  tags: Record<string, number>;\n  loading: boolean;\n}\n\nexport interface UseFilteredMemoStatsOptions {\n  userName?: string;\n  context?: MemoExplorerContext;\n}\n\nconst toDateString = (date: Date) => dayjs(date).format(\"YYYY-MM-DD\");\n\nexport const useFilteredMemoStats = (options: UseFilteredMemoStatsOptions = {}): FilteredMemoStats => {\n  const { userName, context } = options;\n  const currentUser = useCurrentUser();\n\n  // home/profile: use backend per-user stats (full tag set, not page-limited)\n  const { data: userStats, isLoading: isLoadingUserStats } = useUserStats(userName);\n\n  // explore: fetch memos with visibility filter to exclude private content.\n  // ListMemos AND's the request filter with the server's auth filter, so private\n  // memos are always excluded regardless of backend version.\n  // other contexts: fetch with default params for the fallback memo-based path.\n  const exploreVisibilityFilter = currentUser != null ? 'visibility in [\"PUBLIC\", \"PROTECTED\"]' : 'visibility in [\"PUBLIC\"]';\n  const memoQueryParams = context === \"explore\" ? { filter: exploreVisibilityFilter, pageSize: 1000 } : {};\n  const { data: memosResponse, isLoading: isLoadingMemos } = useMemos(memoQueryParams);\n\n  const data = useMemo(() => {\n    const loading = isLoadingUserStats || isLoadingMemos;\n    let activityStats: Record<string, number> = {};\n    let tagCount: Record<string, number> = {};\n\n    if (context === \"explore\") {\n      // Tags and activity stats from visibility-filtered memos (no private content).\n      for (const memo of memosResponse?.memos ?? []) {\n        for (const tag of memo.tags ?? []) {\n          tagCount[tag] = (tagCount[tag] ?? 0) + 1;\n        }\n      }\n      const displayDates = (memosResponse?.memos ?? [])\n        .map((memo) => (memo.displayTime ? timestampDate(memo.displayTime) : undefined))\n        .filter((date): date is Date => date !== undefined)\n        .map(toDateString);\n      activityStats = countBy(displayDates);\n    } else if (userName && userStats) {\n      // home/profile: use backend per-user stats\n      if (userStats.memoDisplayTimestamps && userStats.memoDisplayTimestamps.length > 0) {\n        activityStats = countBy(\n          userStats.memoDisplayTimestamps\n            .map((ts) => (ts ? timestampDate(ts) : undefined))\n            .filter((date): date is Date => date !== undefined)\n            .map(toDateString),\n        );\n      }\n      if (userStats.tagCount) {\n        tagCount = userStats.tagCount;\n      }\n    } else if (memosResponse?.memos) {\n      // archived/fallback: compute from cached memos\n      const displayDates = memosResponse.memos\n        .map((memo) => (memo.displayTime ? timestampDate(memo.displayTime) : undefined))\n        .filter((date): date is Date => date !== undefined)\n        .map(toDateString);\n      activityStats = countBy(displayDates);\n      for (const memo of memosResponse.memos) {\n        for (const tag of memo.tags ?? []) {\n          tagCount[tag] = (tagCount[tag] || 0) + 1;\n        }\n      }\n    }\n\n    return { statistics: { activityStats }, tags: tagCount, loading };\n  }, [context, userName, userStats, memosResponse, isLoadingUserStats, isLoadingMemos]);\n\n  return data;\n};\n"
  },
  {
    "path": "web/src/hooks/useInstanceQueries.ts",
    "content": "import { useMutation, useQuery, useQueryClient } from \"@tanstack/react-query\";\nimport { instanceServiceClient } from \"@/connect\";\nimport { InstanceSetting, InstanceSetting_Key } from \"@/types/proto/api/v1/instance_service_pb\";\n\n// Query keys factory\nexport const instanceKeys = {\n  all: [\"instance\"] as const,\n  profile: () => [...instanceKeys.all, \"profile\"] as const,\n  settings: () => [...instanceKeys.all, \"settings\"] as const,\n  setting: (key: InstanceSetting_Key) => [...instanceKeys.settings(), key] as const,\n};\n\n// Build setting name from key\nconst buildInstanceSettingName = (key: InstanceSetting_Key): string => {\n  const keyName = InstanceSetting_Key[key];\n  return `instance/settings/${keyName}`;\n};\n\n// Hook to fetch instance profile\nexport function useInstanceProfile() {\n  return useQuery({\n    queryKey: instanceKeys.profile(),\n    queryFn: async () => {\n      const profile = await instanceServiceClient.getInstanceProfile({});\n      return profile;\n    },\n    staleTime: 1000 * 60 * 10, // 10 minutes - instance profile rarely changes\n  });\n}\n\n// Hook to fetch a specific instance setting\nexport function useInstanceSetting(key: InstanceSetting_Key) {\n  return useQuery({\n    queryKey: instanceKeys.setting(key),\n    queryFn: async () => {\n      const setting = await instanceServiceClient.getInstanceSetting({\n        name: buildInstanceSettingName(key),\n      });\n      return setting;\n    },\n    staleTime: 1000 * 60 * 5, // 5 minutes\n  });\n}\n\n// Hook to update instance setting\nexport function useUpdateInstanceSetting() {\n  const queryClient = useQueryClient();\n\n  return useMutation({\n    mutationFn: async (setting: InstanceSetting) => {\n      await instanceServiceClient.updateInstanceSetting({ setting });\n      return setting;\n    },\n    onSuccess: (setting) => {\n      // Extract key from setting name and invalidate\n      const keyMatch = setting.name.match(/instance\\/settings\\/(\\w+)/);\n      if (keyMatch) {\n        const keyName = keyMatch[1] as keyof typeof InstanceSetting_Key;\n        const key = InstanceSetting_Key[keyName];\n        if (key !== undefined) {\n          queryClient.setQueryData(instanceKeys.setting(key), setting);\n        }\n      }\n      queryClient.invalidateQueries({ queryKey: instanceKeys.settings() });\n    },\n  });\n}\n\n// Derived hooks for common settings\nexport function useGeneralSetting() {\n  const { data: setting, ...rest } = useInstanceSetting(InstanceSetting_Key.GENERAL);\n  const generalSetting = setting?.value.case === \"generalSetting\" ? setting.value.value : undefined;\n  return { data: generalSetting, ...rest };\n}\n\nexport function useMemoRelatedSetting() {\n  const { data: setting, ...rest } = useInstanceSetting(InstanceSetting_Key.MEMO_RELATED);\n  const memoRelatedSetting = setting?.value.case === \"memoRelatedSetting\" ? setting.value.value : undefined;\n  return { data: memoRelatedSetting, ...rest };\n}\n"
  },
  {
    "path": "web/src/hooks/useLiveMemoRefresh.ts",
    "content": "import { useQueryClient } from \"@tanstack/react-query\";\nimport { useCallback, useEffect, useRef, useSyncExternalStore } from \"react\";\nimport { getAccessToken } from \"@/auth-state\";\nimport { useAuth } from \"@/contexts/AuthContext\";\nimport { memoKeys } from \"@/hooks/useMemoQueries\";\nimport { userKeys } from \"@/hooks/useUserQueries\";\n\n/**\n * Reconnection parameters for SSE connection.\n */\nconst INITIAL_RETRY_DELAY_MS = 1000;\nconst MAX_RETRY_DELAY_MS = 30000;\nconst RETRY_BACKOFF_MULTIPLIER = 2;\n\n// ---------------------------------------------------------------------------\n// Shared connection status store (singleton)\n// ---------------------------------------------------------------------------\n\nexport type SSEConnectionStatus = \"connected\" | \"disconnected\" | \"connecting\";\n\ntype Listener = () => void;\n\nlet _status: SSEConnectionStatus = \"disconnected\";\nconst _listeners = new Set<Listener>();\n\nfunction getSSEStatus(): SSEConnectionStatus {\n  return _status;\n}\n\nfunction setSSEStatus(s: SSEConnectionStatus) {\n  if (_status !== s) {\n    _status = s;\n    _listeners.forEach((l) => l());\n  }\n}\n\nfunction subscribeSSEStatus(listener: Listener): () => void {\n  _listeners.add(listener);\n  return () => _listeners.delete(listener);\n}\n\n/**\n * React hook that returns the current SSE connection status.\n * Re-renders the component whenever the status changes.\n */\nexport function useSSEConnectionStatus(): SSEConnectionStatus {\n  return useSyncExternalStore(subscribeSSEStatus, getSSEStatus, getSSEStatus);\n}\n\n// ---------------------------------------------------------------------------\n// Main hook\n// ---------------------------------------------------------------------------\n\n/**\n * useLiveMemoRefresh connects to the server's SSE endpoint and\n * invalidates relevant React Query caches when change events\n * (memos, reactions) are received.\n *\n * This enables real-time updates across all open instances of the app.\n */\nexport function useLiveMemoRefresh() {\n  const queryClient = useQueryClient();\n  const { currentUser } = useAuth();\n  const retryDelayRef = useRef(INITIAL_RETRY_DELAY_MS);\n  const abortControllerRef = useRef<AbortController | null>(null);\n\n  const currentUserName = currentUser?.name;\n  const handleEvent = useCallback((event: SSEChangeEvent) => handleSSEEvent(event, queryClient), [queryClient]);\n\n  useEffect(() => {\n    let mounted = true;\n    let retryTimeout: ReturnType<typeof setTimeout> | null = null;\n\n    const connect = async () => {\n      if (!mounted) return;\n\n      const token = getAccessToken();\n      if (!token) {\n        setSSEStatus(\"disconnected\");\n        // Not logged in; do not retry. Effect will re-run when currentUser is set (login).\n        return;\n      }\n\n      setSSEStatus(\"connecting\");\n      const abortController = new AbortController();\n      abortControllerRef.current = abortController;\n\n      try {\n        const response = await fetch(\"/api/v1/sse\", {\n          headers: {\n            Authorization: `Bearer ${token}`,\n          },\n          signal: abortController.signal,\n          credentials: \"include\",\n        });\n\n        if (!response.ok || !response.body) {\n          throw new Error(`SSE connection failed: ${response.status}`);\n        }\n\n        // Successfully connected - reset retry delay.\n        retryDelayRef.current = INITIAL_RETRY_DELAY_MS;\n        setSSEStatus(\"connected\");\n\n        const reader = response.body.getReader();\n        const decoder = new TextDecoder();\n        let buffer = \"\";\n\n        while (mounted) {\n          const { done, value } = await reader.read();\n          if (done) break;\n\n          buffer += decoder.decode(value, { stream: true });\n\n          // Process complete SSE messages (separated by double newlines).\n          const messages = buffer.split(\"\\n\\n\");\n          // Keep the last incomplete chunk in the buffer.\n          buffer = messages.pop() || \"\";\n\n          for (const message of messages) {\n            if (!message.trim()) continue;\n\n            // Parse SSE format: lines starting with \"data: \" contain JSON payload.\n            // Lines starting with \":\" are comments (heartbeats).\n            for (const line of message.split(\"\\n\")) {\n              if (line.startsWith(\"data: \")) {\n                const jsonStr = line.slice(6);\n                try {\n                  const event = JSON.parse(jsonStr) as SSEChangeEvent;\n                  handleEvent(event);\n                } catch {\n                  // Ignore malformed JSON.\n                }\n              }\n            }\n          }\n        }\n      } catch (err: unknown) {\n        if (err instanceof DOMException && err.name === \"AbortError\") {\n          // Intentional abort, don't reconnect.\n          setSSEStatus(\"disconnected\");\n          return;\n        }\n        // Connection lost or failed - reconnect with backoff.\n      }\n\n      setSSEStatus(\"disconnected\");\n\n      // Reconnect with exponential backoff.\n      if (mounted) {\n        const delay = retryDelayRef.current;\n        retryDelayRef.current = Math.min(delay * RETRY_BACKOFF_MULTIPLIER, MAX_RETRY_DELAY_MS);\n        retryTimeout = setTimeout(connect, delay);\n      }\n    };\n\n    connect();\n\n    return () => {\n      mounted = false;\n      setSSEStatus(\"disconnected\");\n      retryDelayRef.current = INITIAL_RETRY_DELAY_MS;\n      if (retryTimeout) {\n        clearTimeout(retryTimeout);\n      }\n      if (abortControllerRef.current) {\n        abortControllerRef.current.abort();\n      }\n    };\n  }, [handleEvent, currentUserName]);\n}\n\n// ---------------------------------------------------------------------------\n// Event handling\n// ---------------------------------------------------------------------------\n\ninterface SSEChangeEvent {\n  type: string;\n  name: string;\n}\n\nfunction handleSSEEvent(event: SSEChangeEvent, queryClient: ReturnType<typeof useQueryClient>) {\n  switch (event.type) {\n    case \"memo.created\":\n      queryClient.invalidateQueries({ queryKey: memoKeys.lists() });\n      queryClient.invalidateQueries({ queryKey: userKeys.stats() });\n      break;\n\n    case \"memo.updated\":\n      queryClient.invalidateQueries({ queryKey: memoKeys.detail(event.name) });\n      queryClient.invalidateQueries({ queryKey: memoKeys.lists() });\n      break;\n\n    case \"memo.deleted\":\n      queryClient.removeQueries({ queryKey: memoKeys.detail(event.name) });\n      queryClient.invalidateQueries({ queryKey: memoKeys.lists() });\n      queryClient.invalidateQueries({ queryKey: userKeys.stats() });\n      break;\n\n    case \"memo.comment.created\":\n      queryClient.invalidateQueries({ queryKey: memoKeys.comments(event.name) });\n      queryClient.invalidateQueries({ queryKey: memoKeys.detail(event.name) });\n      break;\n\n    case \"reaction.upserted\":\n    case \"reaction.deleted\":\n      queryClient.invalidateQueries({ queryKey: memoKeys.detail(event.name) });\n      queryClient.invalidateQueries({ queryKey: memoKeys.lists() });\n      break;\n  }\n}\n"
  },
  {
    "path": "web/src/hooks/useLoading.ts",
    "content": "import { useState } from \"react\";\n\nconst useLoading = (initialState = true) => {\n  const [state, setState] = useState({ isLoading: initialState, isFailed: false, isSucceed: false });\n\n  return {\n    ...state,\n    setLoading: () => {\n      setState({\n        ...state,\n        isLoading: true,\n        isFailed: false,\n        isSucceed: false,\n      });\n    },\n    setFinish: () => {\n      setState({\n        ...state,\n        isLoading: false,\n        isFailed: false,\n        isSucceed: true,\n      });\n    },\n    setError: () => {\n      setState({\n        ...state,\n        isLoading: false,\n        isFailed: true,\n        isSucceed: false,\n      });\n    },\n  };\n};\n\nexport default useLoading;\n"
  },
  {
    "path": "web/src/hooks/useMediaQuery.ts",
    "content": "import { useEffect, useState } from \"react\";\n\ntype Breakpoint = \"sm\" | \"md\" | \"lg\";\n\nconst BREAKPOINTS: Record<Breakpoint, number> = {\n  sm: 640,\n  md: 768,\n  lg: 1024,\n};\n\nconst useMediaQuery = (breakpoint: Breakpoint): boolean => {\n  const [matches, setMatches] = useState(() => {\n    if (typeof window === \"undefined\") return false;\n    return window.matchMedia(`(min-width: ${BREAKPOINTS[breakpoint]}px)`).matches;\n  });\n\n  useEffect(() => {\n    const mediaQuery = window.matchMedia(`(min-width: ${BREAKPOINTS[breakpoint]}px)`);\n\n    const handleChange = (e: MediaQueryListEvent) => {\n      setMatches(e.matches);\n    };\n\n    mediaQuery.addEventListener(\"change\", handleChange);\n\n    return () => {\n      mediaQuery.removeEventListener(\"change\", handleChange);\n    };\n  }, [breakpoint]);\n\n  return matches;\n};\n\nexport default useMediaQuery;\n"
  },
  {
    "path": "web/src/hooks/useMemoFilters.ts",
    "content": "import { useMemo } from \"react\";\nimport { useAuth } from \"@/contexts/AuthContext\";\nimport { useInstance } from \"@/contexts/InstanceContext\";\nimport { useMemoFilterContext } from \"@/contexts/MemoFilterContext\";\nimport { Visibility } from \"@/types/proto/api/v1/memo_service_pb\";\n\nconst extractUserIdFromName = (name: string): string => {\n  const match = name.match(/users\\/(\\d+)/);\n  return match ? match[1] : \"\";\n};\n\nconst getVisibilityName = (visibility: Visibility): string => {\n  switch (visibility) {\n    case Visibility.PUBLIC:\n      return \"PUBLIC\";\n    case Visibility.PROTECTED:\n      return \"PROTECTED\";\n    case Visibility.PRIVATE:\n      return \"PRIVATE\";\n    default:\n      return \"PRIVATE\";\n  }\n};\n\nconst getShortcutId = (name: string): string => {\n  const parts = name.split(\"/\");\n  return parts.length === 4 ? parts[3] : \"\";\n};\n\nexport interface UseMemoFiltersOptions {\n  creatorName?: string;\n  includeShortcuts?: boolean;\n  includePinned?: boolean;\n  visibilities?: Visibility[];\n}\n\nexport const useMemoFilters = (options: UseMemoFiltersOptions = {}): string | undefined => {\n  const { creatorName, includeShortcuts = false, includePinned = false, visibilities } = options;\n\n  const { shortcuts } = useAuth();\n  const { filters, shortcut: currentShortcut } = useMemoFilterContext();\n  const { memoRelatedSetting } = useInstance();\n\n  // Get selected shortcut if needed\n  const selectedShortcut = useMemo(() => {\n    if (!includeShortcuts) return undefined;\n    return shortcuts.find((shortcut) => getShortcutId(shortcut.name) === currentShortcut);\n  }, [includeShortcuts, currentShortcut, shortcuts]);\n\n  // Build filter\n  return useMemo(() => {\n    const conditions: string[] = [];\n\n    // Add creator filter if provided\n    if (creatorName) {\n      conditions.push(`creator_id == ${extractUserIdFromName(creatorName)}`);\n    }\n\n    // Add shortcut filter if enabled and selected\n    if (includeShortcuts && selectedShortcut?.filter) {\n      conditions.push(selectedShortcut.filter);\n    }\n\n    // Add active filters from context\n    for (const filter of filters) {\n      if (filter.factor === \"contentSearch\") {\n        conditions.push(`content.contains(\"${filter.value}\")`);\n      } else if (filter.factor === \"tagSearch\") {\n        conditions.push(`tag in [\"${filter.value}\"]`);\n      } else if (filter.factor === \"pinned\") {\n        if (includePinned) {\n          conditions.push(`pinned`);\n        }\n      } else if (filter.factor === \"property.hasLink\") {\n        conditions.push(`has_link`);\n      } else if (filter.factor === \"property.hasTaskList\") {\n        conditions.push(`has_task_list`);\n      } else if (filter.factor === \"property.hasCode\") {\n        conditions.push(`has_code`);\n      } else if (filter.factor === \"displayTime\") {\n        const displayWithUpdateTime = memoRelatedSetting?.displayWithUpdateTime ?? false;\n        const factor = displayWithUpdateTime ? \"updated_ts\" : \"created_ts\";\n\n        const filterDate = new Date(filter.value);\n        const filterUtcTimestamp = filterDate.getTime() + filterDate.getTimezoneOffset() * 60 * 1000;\n        const timestampAfter = filterUtcTimestamp / 1000;\n\n        conditions.push(`${factor} >= ${timestampAfter} && ${factor} < ${timestampAfter + 60 * 60 * 24}`);\n      }\n    }\n\n    // Add visibility filter if specified\n    if (visibilities && visibilities.length > 0) {\n      const visibilityValues = visibilities.map((v) => `\"${getVisibilityName(v)}\"`).join(\", \");\n      conditions.push(`visibility in [${visibilityValues}]`);\n    }\n\n    return conditions.length > 0 ? conditions.join(\" && \") : undefined;\n  }, [creatorName, includeShortcuts, includePinned, visibilities, selectedShortcut, filters, memoRelatedSetting]);\n};\n"
  },
  {
    "path": "web/src/hooks/useMemoQueries.ts",
    "content": "import { create } from \"@bufbuild/protobuf\";\nimport { FieldMaskSchema } from \"@bufbuild/protobuf/wkt\";\nimport { useInfiniteQuery, useMutation, useQuery, useQueryClient } from \"@tanstack/react-query\";\nimport { memoServiceClient } from \"@/connect\";\nimport { userKeys } from \"@/hooks/useUserQueries\";\nimport type { ListMemosRequest, Memo } from \"@/types/proto/api/v1/memo_service_pb\";\nimport { ListMemosRequestSchema, MemoSchema } from \"@/types/proto/api/v1/memo_service_pb\";\n\n// Query keys factory for consistent cache management\nexport const memoKeys = {\n  all: [\"memos\"] as const,\n  lists: () => [...memoKeys.all, \"list\"] as const,\n  list: (filters: Partial<ListMemosRequest>) => [...memoKeys.lists(), filters] as const,\n  details: () => [...memoKeys.all, \"detail\"] as const,\n  detail: (name: string) => [...memoKeys.details(), name] as const,\n  comments: (name: string) => [...memoKeys.all, \"comments\", name] as const,\n};\n\nexport function useMemos(request: Partial<ListMemosRequest> = {}) {\n  return useQuery({\n    queryKey: memoKeys.list(request),\n    queryFn: async () => {\n      const response = await memoServiceClient.listMemos(create(ListMemosRequestSchema, request as Record<string, unknown>));\n      return response;\n    },\n  });\n}\n\nexport function useInfiniteMemos(request: Partial<ListMemosRequest> = {}, options?: { enabled?: boolean }) {\n  return useInfiniteQuery({\n    queryKey: memoKeys.list(request),\n    queryFn: async ({ pageParam }) => {\n      const response = await memoServiceClient.listMemos(\n        create(ListMemosRequestSchema, {\n          ...request,\n          pageToken: pageParam || \"\",\n        } as Record<string, unknown>),\n      );\n      return response;\n    },\n    initialPageParam: \"\",\n    getNextPageParam: (lastPage) => lastPage.nextPageToken || undefined,\n    staleTime: 1000 * 60,\n    gcTime: 1000 * 60 * 5,\n    enabled: options?.enabled ?? true,\n  });\n}\n\nexport function useMemo(name: string, options?: { enabled?: boolean }) {\n  return useQuery({\n    queryKey: memoKeys.detail(name),\n    queryFn: async () => {\n      const memo = await memoServiceClient.getMemo({ name });\n      return memo;\n    },\n    enabled: options?.enabled ?? true,\n    staleTime: 1000 * 10, // 10 seconds - reduced to prevent stale data in collaborative editing\n  });\n}\n\nexport function useCreateMemo() {\n  const queryClient = useQueryClient();\n\n  return useMutation({\n    mutationFn: async (memoToCreate: Memo) => {\n      const memo = await memoServiceClient.createMemo({ memo: memoToCreate });\n      return memo;\n    },\n    onSuccess: (newMemo) => {\n      // Invalidate memo lists to refetch\n      queryClient.invalidateQueries({ queryKey: memoKeys.lists() });\n      // Add new memo to cache\n      queryClient.setQueryData(memoKeys.detail(newMemo.name), newMemo);\n      // Invalidate user stats\n      queryClient.invalidateQueries({ queryKey: userKeys.stats() });\n    },\n  });\n}\n\nexport function useUpdateMemo() {\n  const queryClient = useQueryClient();\n\n  return useMutation({\n    mutationFn: async ({ update, updateMask }: { update: Partial<Memo>; updateMask: string[] }) => {\n      const memo = await memoServiceClient.updateMemo({\n        memo: create(MemoSchema, update as Record<string, unknown>),\n        updateMask: create(FieldMaskSchema, { paths: updateMask }),\n      });\n      return memo;\n    },\n    onMutate: async ({ update }) => {\n      if (!update.name) {\n        return { previousMemo: undefined };\n      }\n\n      // Cancel outgoing refetches to prevent race conditions\n      await queryClient.cancelQueries({ queryKey: memoKeys.detail(update.name) });\n\n      // Snapshot previous value for rollback on error\n      const previousMemo = queryClient.getQueryData<Memo>(memoKeys.detail(update.name));\n\n      // Optimistically update the cache\n      if (previousMemo) {\n        queryClient.setQueryData(memoKeys.detail(update.name), { ...previousMemo, ...update });\n      }\n\n      return { previousMemo };\n    },\n    onError: (_err, { update }, context) => {\n      // Rollback on error\n      if (context?.previousMemo && update.name) {\n        queryClient.setQueryData(memoKeys.detail(update.name), context.previousMemo);\n      }\n    },\n    onSuccess: (updatedMemo) => {\n      // Update cache with server response\n      queryClient.setQueryData(memoKeys.detail(updatedMemo.name), updatedMemo);\n      // Invalidate lists to refresh\n      queryClient.invalidateQueries({ queryKey: memoKeys.lists() });\n      // Invalidate user stats\n      queryClient.invalidateQueries({ queryKey: userKeys.stats() });\n    },\n  });\n}\n\nexport function useDeleteMemo() {\n  const queryClient = useQueryClient();\n\n  return useMutation({\n    mutationFn: async (name: string) => {\n      await memoServiceClient.deleteMemo({ name });\n      return name;\n    },\n    onSuccess: (name) => {\n      // Remove from cache\n      queryClient.removeQueries({ queryKey: memoKeys.detail(name) });\n      // Invalidate lists\n      queryClient.invalidateQueries({ queryKey: memoKeys.lists() });\n      // Invalidate user stats\n      queryClient.invalidateQueries({ queryKey: userKeys.stats() });\n    },\n  });\n}\n\nexport function useMemoComments(name: string, options?: { enabled?: boolean }) {\n  return useQuery({\n    queryKey: memoKeys.comments(name),\n    queryFn: async () => {\n      const response = await memoServiceClient.listMemoComments({ name });\n      return response;\n    },\n    enabled: options?.enabled ?? true,\n    staleTime: 1000 * 60, // 1 minute\n  });\n}\n"
  },
  {
    "path": "web/src/hooks/useMemoShareQueries.ts",
    "content": "import { create } from \"@bufbuild/protobuf\";\nimport { timestampFromDate } from \"@bufbuild/protobuf/wkt\";\nimport { useMutation, useQuery, useQueryClient } from \"@tanstack/react-query\";\nimport { memoServiceClient } from \"@/connect\";\nimport type { MemoShare } from \"@/types/proto/api/v1/memo_service_pb\";\nimport {\n  CreateMemoShareRequestSchema,\n  DeleteMemoShareRequestSchema,\n  GetMemoByShareRequestSchema,\n  ListMemoSharesRequestSchema,\n  MemoShareSchema,\n} from \"@/types/proto/api/v1/memo_service_pb\";\n\n// Query keys factory for share-related cache management.\nexport const memoShareKeys = {\n  all: [\"memo-shares\"] as const,\n  list: (memoName: string) => [...memoShareKeys.all, \"list\", memoName] as const,\n  byShare: (shareId: string) => [...memoShareKeys.all, \"by-share\", shareId] as const,\n};\n\n/** Lists all active share links for a memo (creator-only). */\nexport function useMemoShares(memoName: string, options?: { enabled?: boolean }) {\n  return useQuery({\n    queryKey: memoShareKeys.list(memoName),\n    queryFn: async () => {\n      const response = await memoServiceClient.listMemoShares(create(ListMemoSharesRequestSchema, { parent: memoName }));\n      return response.memoShares;\n    },\n    enabled: options?.enabled ?? !!memoName,\n  });\n}\n\n/** Creates a new share link for a memo. */\nexport function useCreateMemoShare() {\n  const queryClient = useQueryClient();\n  return useMutation({\n    mutationFn: async ({ memoName, expireTime }: { memoName: string; expireTime?: Date }) => {\n      const memoShare = create(MemoShareSchema, {\n        expireTime: expireTime ? timestampFromDate(expireTime) : undefined,\n      });\n      const response = await memoServiceClient.createMemoShare(create(CreateMemoShareRequestSchema, { parent: memoName, memoShare }));\n      return response;\n    },\n    onSuccess: (_data, variables) => {\n      queryClient.invalidateQueries({ queryKey: memoShareKeys.list(variables.memoName) });\n    },\n  });\n}\n\n/** Revokes (deletes) a share link. */\nexport function useDeleteMemoShare() {\n  const queryClient = useQueryClient();\n  return useMutation({\n    mutationFn: async ({ name, memoName }: { name: string; memoName: string }) => {\n      await memoServiceClient.deleteMemoShare(create(DeleteMemoShareRequestSchema, { name }));\n      return memoName;\n    },\n    onSuccess: (_data, variables) => {\n      queryClient.invalidateQueries({ queryKey: memoShareKeys.list(variables.memoName) });\n    },\n  });\n}\n\n/** Resolves a share token to its memo. Used by the public SharedMemo page. */\nexport function useSharedMemo(shareId: string, options?: { enabled?: boolean }) {\n  return useQuery({\n    queryKey: memoShareKeys.byShare(shareId),\n    queryFn: async () => {\n      const memo = await memoServiceClient.getMemoByShare(create(GetMemoByShareRequestSchema, { shareId }));\n      return memo;\n    },\n    enabled: options?.enabled ?? !!shareId,\n    retry: false, // Don't retry NOT_FOUND — the link is invalid or expired\n  });\n}\n\n/**\n * Returns the share URL for a MemoShare resource.\n * The token is the last path segment of the share name (memos/{uid}/shares/{token}).\n */\nexport function getShareUrl(share: MemoShare): string {\n  const token = share.name.split(\"/\").pop() ?? \"\";\n  return `${window.location.origin}/memos/shares/${token}`;\n}\n\n/**\n * Returns the token portion of a MemoShare resource name.\n * Format: memos/{memo}/shares/{token}\n */\nexport function getShareToken(share: MemoShare): string {\n  return share.name.split(\"/\").pop() ?? \"\";\n}\n"
  },
  {
    "path": "web/src/hooks/useMemoSorting.ts",
    "content": "import { timestampDate } from \"@bufbuild/protobuf/wkt\";\nimport dayjs from \"dayjs\";\nimport { useMemo } from \"react\";\nimport { useView } from \"@/contexts/ViewContext\";\nimport { State } from \"@/types/proto/api/v1/common_pb\";\nimport { Memo } from \"@/types/proto/api/v1/memo_service_pb\";\n\nexport interface UseMemoSortingOptions {\n  pinnedFirst?: boolean;\n  state?: State;\n}\n\nexport interface UseMemoSortingResult {\n  listSort: (memos: Memo[]) => Memo[];\n  orderBy: string;\n}\n\nexport const useMemoSorting = (options: UseMemoSortingOptions = {}): UseMemoSortingResult => {\n  const { pinnedFirst = false, state = State.NORMAL } = options;\n  const { orderByTimeAsc } = useView();\n\n  // Generate orderBy string for API\n  const orderBy = useMemo(() => {\n    const timeOrder = orderByTimeAsc ? \"display_time asc\" : \"display_time desc\";\n    return pinnedFirst ? `pinned desc, ${timeOrder}` : timeOrder;\n  }, [pinnedFirst, orderByTimeAsc]);\n\n  // Generate listSort function for client-side sorting\n  const listSort = useMemo(() => {\n    return (memos: Memo[]): Memo[] => {\n      return memos\n        .filter((memo) => memo.state === state)\n        .sort((a, b) => {\n          // First, sort by pinned status if enabled\n          if (pinnedFirst && a.pinned !== b.pinned) {\n            return b.pinned ? 1 : -1;\n          }\n\n          // Then sort by display time\n          const aTime = a.displayTime ? timestampDate(a.displayTime) : undefined;\n          const bTime = b.displayTime ? timestampDate(b.displayTime) : undefined;\n          return orderByTimeAsc ? dayjs(aTime).unix() - dayjs(bTime).unix() : dayjs(bTime).unix() - dayjs(aTime).unix();\n        });\n    };\n  }, [pinnedFirst, state, orderByTimeAsc]);\n\n  return { listSort, orderBy };\n};\n"
  },
  {
    "path": "web/src/hooks/useNavigateTo.ts",
    "content": "import { useCallback } from \"react\";\nimport { NavigateOptions, useNavigate } from \"react-router-dom\";\n\nconst useNavigateTo = () => {\n  const navigateTo = useNavigate();\n\n  const navigateToWithViewTransition = useCallback(\n    (to: string, options?: NavigateOptions) => {\n      const doc = window.document as unknown as Document & { startViewTransition?: (callback: () => void) => void };\n      if (!doc.startViewTransition) {\n        navigateTo(to, options);\n      } else {\n        document.startViewTransition(() => {\n          navigateTo(to, options);\n        });\n      }\n    },\n    [navigateTo],\n  );\n\n  return navigateToWithViewTransition;\n};\n\nexport default useNavigateTo;\n"
  },
  {
    "path": "web/src/hooks/useTokenRefreshOnFocus.ts",
    "content": "import { useEffect } from \"react\";\nimport { FOCUS_TOKEN_EXPIRY_BUFFER_MS, hasStoredToken, isTokenExpired } from \"@/auth-state\";\n\n/**\n * Hook that proactively refreshes the access token when the tab becomes visible\n * and the token is expired or expiring soon.\n *\n * This prevents React Query's automatic refetch-on-window-focus from triggering\n * multiple 401 errors when the user returns to the tab after the token has expired.\n *\n * Related issue: https://github.com/usememos/memos/issues/5589\n */\nexport function useTokenRefreshOnFocus(refreshFn: () => Promise<void>, enabled: boolean = true) {\n  useEffect(() => {\n    if (!enabled) return;\n\n    const handleVisibilityChange = async () => {\n      // Only act when tab becomes visible\n      if (document.visibilityState !== \"visible\") {\n        return;\n      }\n\n      // Only refresh if the user has logged in before (token in localStorage)\n      if (!hasStoredToken()) {\n        return;\n      }\n\n      // Check if token is expired or expiring soon (within 2 minutes)\n      // Use a longer buffer than normal requests to be proactive\n      if (isTokenExpired(FOCUS_TOKEN_EXPIRY_BUFFER_MS)) {\n        try {\n          console.debug(\"[useTokenRefreshOnFocus] Token expired/expiring, refreshing before queries refetch\");\n          await refreshFn();\n          console.debug(\"[useTokenRefreshOnFocus] Token refreshed successfully\");\n        } catch (error) {\n          // Don't block - let the normal auth interceptor handle it\n          // The user will be redirected if refresh fails\n          console.error(\"[useTokenRefreshOnFocus] Failed to refresh token:\", error);\n        }\n      }\n    };\n\n    document.addEventListener(\"visibilitychange\", handleVisibilityChange);\n\n    return () => {\n      document.removeEventListener(\"visibilitychange\", handleVisibilityChange);\n    };\n  }, [refreshFn, enabled]);\n}\n"
  },
  {
    "path": "web/src/hooks/useUserLocale.ts",
    "content": "import { useEffect } from \"react\";\nimport { useTranslation } from \"react-i18next\";\nimport { useAuth } from \"@/contexts/AuthContext\";\nimport { getLocaleWithFallback, loadLocale } from \"@/utils/i18n\";\n\n/**\n * Hook that reactively applies user locale preference.\n * Priority: User setting → localStorage → browser language\n */\nexport const useUserLocale = () => {\n  const { i18n } = useTranslation();\n  const { userGeneralSetting } = useAuth();\n\n  // Apply locale when user setting changes or user logs in\n  useEffect(() => {\n    if (!userGeneralSetting) {\n      return;\n    }\n    const locale = getLocaleWithFallback(userGeneralSetting.locale);\n    loadLocale(locale);\n  }, [userGeneralSetting?.locale]);\n\n  // Update HTML lang and dir attributes based on current locale\n  useEffect(() => {\n    const currentLocale = i18n.language;\n    document.documentElement.setAttribute(\"lang\", currentLocale);\n\n    // RTL languages\n    if ([\"ar\", \"fa\"].includes(currentLocale)) {\n      document.documentElement.setAttribute(\"dir\", \"rtl\");\n    } else {\n      document.documentElement.setAttribute(\"dir\", \"ltr\");\n    }\n  }, [i18n.language]);\n};\n"
  },
  {
    "path": "web/src/hooks/useUserQueries.ts",
    "content": "import { create } from \"@bufbuild/protobuf\";\nimport { FieldMaskSchema } from \"@bufbuild/protobuf/wkt\";\nimport { useMutation, useQuery, useQueryClient } from \"@tanstack/react-query\";\nimport { shortcutServiceClient, userServiceClient } from \"@/connect\";\nimport { buildUserSettingName } from \"@/helpers/resource-names\";\nimport useCurrentUser from \"@/hooks/useCurrentUser\";\nimport { User, UserSetting, UserSetting_GeneralSetting, UserSetting_Key, UserSettingSchema } from \"@/types/proto/api/v1/user_service_pb\";\n\n// Query keys factory\nexport const userKeys = {\n  all: [\"users\"] as const,\n  details: () => [...userKeys.all, \"detail\"] as const,\n  detail: (name: string) => [...userKeys.details(), name] as const,\n  stats: () => [...userKeys.all, \"stats\"] as const,\n  userStats: (name: string) => [...userKeys.stats(), name] as const,\n  currentUser: () => [...userKeys.all, \"current\"] as const,\n  shortcuts: () => [...userKeys.all, \"shortcuts\"] as const,\n  notifications: () => [...userKeys.all, \"notifications\"] as const,\n  byNames: (names: string[]) => [...userKeys.all, \"byNames\", ...names.sort()] as const,\n};\n\nexport function useUser(name: string, options?: { enabled?: boolean }) {\n  return useQuery({\n    queryKey: userKeys.detail(name),\n    queryFn: async () => {\n      const user = await userServiceClient.getUser({ name });\n      return user;\n    },\n    enabled: options?.enabled ?? true,\n    staleTime: 1000 * 60 * 5, // 5 minutes - user profiles don't change often\n  });\n}\n\nexport function useUserStats(username?: string) {\n  return useQuery({\n    queryKey: username ? userKeys.userStats(username) : userKeys.stats(),\n    queryFn: async () => {\n      if (!username) {\n        throw new Error(\"Username is required\");\n      }\n      const stats = await userServiceClient.getUserStats({ name: username });\n      return stats;\n    },\n    enabled: !!username,\n  });\n}\n\nexport function useShortcuts() {\n  return useQuery({\n    queryKey: userKeys.shortcuts(),\n    queryFn: async () => {\n      const { shortcuts } = await shortcutServiceClient.listShortcuts({});\n      return shortcuts;\n    },\n  });\n}\n\nexport function useNotifications() {\n  const currentUser = useCurrentUser();\n\n  return useQuery({\n    queryKey: userKeys.notifications(),\n    queryFn: async () => {\n      if (!currentUser?.name) {\n        return [];\n      }\n      const { notifications } = await userServiceClient.listUserNotifications({ parent: currentUser.name });\n      return notifications;\n    },\n    enabled: !!currentUser?.name,\n    staleTime: 1000 * 30, // 30 seconds - notifications should update frequently\n  });\n}\n\nexport function useTagCounts(forCurrentUser = false) {\n  const currentUser = useCurrentUser();\n\n  return useQuery({\n    queryKey: forCurrentUser ? [...userKeys.stats(), \"tagCounts\", \"current\"] : [...userKeys.stats(), \"tagCounts\", \"all\"],\n    queryFn: async () => {\n      if (forCurrentUser) {\n        // Fetch current user stats only\n        if (!currentUser?.name) {\n          return {};\n        }\n        const stats = await userServiceClient.getUserStats({ name: currentUser.name });\n        return stats.tagCount || {};\n      } else {\n        // Fetch all user stats\n        const { stats } = await userServiceClient.listAllUserStats({});\n\n        // Aggregate tag counts from all users\n        const tagCount: Record<string, number> = {};\n        for (const userStats of stats) {\n          if (userStats.tagCount) {\n            for (const [tag, count] of Object.entries(userStats.tagCount)) {\n              tagCount[tag] = (tagCount[tag] || 0) + count;\n            }\n          }\n        }\n        return tagCount;\n      }\n    },\n    enabled: !forCurrentUser || !!currentUser?.name,\n    staleTime: 1000 * 60 * 2, // 2 minutes - tags don't change frequently\n  });\n}\n\nexport function useUpdateUser() {\n  const queryClient = useQueryClient();\n\n  return useMutation({\n    mutationFn: async ({ user, updateMask }: { user: Partial<User>; updateMask: string[] }) => {\n      const updatedUser = await userServiceClient.updateUser({\n        user: user as User,\n        updateMask: create(FieldMaskSchema, { paths: updateMask }),\n      });\n      return updatedUser;\n    },\n    onSuccess: (updatedUser) => {\n      queryClient.setQueryData(userKeys.detail(updatedUser.name), updatedUser);\n      queryClient.invalidateQueries({ queryKey: userKeys.currentUser() });\n    },\n  });\n}\n\nexport function useDeleteUser() {\n  const queryClient = useQueryClient();\n\n  return useMutation({\n    mutationFn: async (name: string) => {\n      await userServiceClient.deleteUser({ name });\n      return name;\n    },\n    onSuccess: (name) => {\n      queryClient.removeQueries({ queryKey: userKeys.detail(name) });\n      queryClient.invalidateQueries({ queryKey: userKeys.all });\n    },\n  });\n}\n\n// Hook to fetch user settings\nexport function useUserSettings(parent?: string) {\n  return useQuery({\n    queryKey: [...userKeys.all, \"settings\", parent],\n    queryFn: async () => {\n      if (!parent) return { settings: [], shortcuts: [] };\n      const [{ settings }, { shortcuts }] = await Promise.all([\n        userServiceClient.listUserSettings({ parent }),\n        shortcutServiceClient.listShortcuts({ parent }),\n      ]);\n      return { settings, shortcuts };\n    },\n    enabled: !!parent,\n  });\n}\n\n// Hook to update user setting\nexport function useUpdateUserSetting() {\n  const queryClient = useQueryClient();\n\n  return useMutation({\n    mutationFn: async ({ setting, updateMask }: { setting: UserSetting; updateMask: string[] }) => {\n      const updatedSetting = await userServiceClient.updateUserSetting({\n        setting,\n        updateMask: create(FieldMaskSchema, { paths: updateMask }),\n      });\n      return updatedSetting;\n    },\n    onSuccess: () => {\n      queryClient.invalidateQueries({ queryKey: [...userKeys.all, \"settings\"] });\n    },\n  });\n}\n\n// Hook to list all users\nexport function useListUsers() {\n  return useQuery({\n    queryKey: userKeys.all,\n    queryFn: async () => {\n      const { users } = await userServiceClient.listUsers({});\n      return users;\n    },\n  });\n}\n\n// Hook to update user general setting (convenience wrapper)\nexport function useUpdateUserGeneralSetting(currentUserName?: string) {\n  const queryClient = useQueryClient();\n\n  return useMutation({\n    mutationFn: async ({ generalSetting, updateMask }: { generalSetting: Partial<UserSetting_GeneralSetting>; updateMask: string[] }) => {\n      if (!currentUserName) {\n        throw new Error(\"No current user\");\n      }\n\n      const settingName = buildUserSettingName(currentUserName, UserSetting_Key.GENERAL);\n      const userSetting = create(UserSettingSchema, {\n        name: settingName,\n        value: {\n          case: \"generalSetting\",\n          value: generalSetting as UserSetting_GeneralSetting,\n        },\n      });\n\n      const updatedSetting = await userServiceClient.updateUserSetting({\n        setting: userSetting,\n        updateMask: create(FieldMaskSchema, { paths: updateMask }),\n      });\n      return updatedSetting;\n    },\n    onSuccess: () => {\n      queryClient.invalidateQueries({ queryKey: [...userKeys.all, \"settings\"] });\n    },\n  });\n}\n\n// Hook to fetch multiple users by names (returns Map<name, User>)\nexport function useUsersByNames(names: string[]) {\n  const enabled = names.length > 0;\n  const uniqueNames = Array.from(new Set(names));\n\n  return useQuery({\n    queryKey: userKeys.byNames(uniqueNames),\n    queryFn: async () => {\n      const users = await Promise.all(\n        uniqueNames.map(async (name) => {\n          try {\n            const user = await userServiceClient.getUser({ name });\n            return { name, user };\n          } catch {\n            return { name, user: undefined };\n          }\n        }),\n      );\n\n      const userMap = new Map<string, User | undefined>();\n      for (const { name, user } of users) {\n        userMap.set(name, user);\n      }\n      return userMap;\n    },\n    enabled,\n    staleTime: 1000 * 60 * 5, // 5 minutes - user profiles don't change often\n  });\n}\n"
  },
  {
    "path": "web/src/hooks/useUserTheme.ts",
    "content": "import { useEffect } from \"react\";\nimport { useAuth } from \"@/contexts/AuthContext\";\nimport { getThemeWithFallback, loadTheme, setupSystemThemeListener } from \"@/utils/theme\";\n\n/**\n * Hook that reactively applies user theme preference.\n * Priority: User setting → localStorage → system preference\n */\nexport const useUserTheme = () => {\n  const { userGeneralSetting } = useAuth();\n\n  // Apply theme when user setting changes or user logs in\n  useEffect(() => {\n    if (!userGeneralSetting) {\n      return;\n    }\n    const theme = getThemeWithFallback(userGeneralSetting.theme);\n    loadTheme(theme);\n  }, [userGeneralSetting?.theme]);\n\n  // Listen for system theme changes when using \"system\" theme\n  useEffect(() => {\n    const theme = getThemeWithFallback(userGeneralSetting?.theme);\n\n    // Only set up listener if theme is \"system\"\n    if (theme !== \"system\") {\n      return;\n    }\n\n    // Set up listener for OS theme preference changes\n    const cleanup = setupSystemThemeListener(() => {\n      loadTheme(theme);\n    });\n\n    return cleanup;\n  }, [userGeneralSetting?.theme]);\n};\n"
  },
  {
    "path": "web/src/i18n.ts",
    "content": "import i18n, { BackendModule, FallbackLng, FallbackLngObjList } from \"i18next\";\nimport { orderBy } from \"lodash-es\";\nimport { initReactI18next } from \"react-i18next\";\nimport { findNearestMatchedLanguage } from \"./utils/i18n\";\n\nexport const locales = orderBy([\n  \"ar\",\n  \"ca\",\n  \"cs\",\n  \"de\",\n  \"en\",\n  \"en-GB\",\n  \"es\",\n  \"fa\",\n  \"fr\",\n  \"gl\",\n  \"hi\",\n  \"hr\",\n  \"hu\",\n  \"id\",\n  \"it\",\n  \"ja\",\n  \"ka-GE\",\n  \"ko\",\n  \"mr\",\n  \"nb\",\n  \"nl\",\n  \"pl\",\n  \"pt-PT\",\n  \"pt-BR\",\n  \"ru\",\n  \"sl\",\n  \"sv\",\n  \"th\",\n  \"tr\",\n  \"uk\",\n  \"vi\",\n  \"zh-Hans\",\n  \"zh-Hant\",\n]);\n\nconst fallbacks = {\n  \"zh-HK\": [\"zh-Hant\", \"en\"],\n  \"zh-TW\": [\"zh-Hant\", \"en\"],\n  zh: [\"zh-Hans\", \"en\"],\n} as FallbackLngObjList;\n\nconst LazyImportPlugin: BackendModule = {\n  type: \"backend\",\n  init: function () {},\n  read: function (language, _, callback) {\n    const matchedLanguage = findNearestMatchedLanguage(language);\n    import(`./locales/${matchedLanguage}.json`)\n      .then((translationModule: Record<string, unknown>) => {\n        callback(null, (translationModule.default as Record<string, unknown>) ?? translationModule);\n      })\n      .catch(() => {\n        import(\"./locales/en.json\")\n          .then((translationModule: Record<string, unknown>) => {\n            callback(null, (translationModule.default as Record<string, unknown>) ?? translationModule);\n          })\n          .catch((error: unknown) => {\n            callback(error as Error, false);\n          });\n      });\n  },\n};\n\ni18n\n  .use(LazyImportPlugin)\n  .use(initReactI18next)\n  .init({\n    detection: {\n      order: [\"navigator\"],\n    },\n    interpolation: {\n      escapeValue: false,\n    },\n    fallbackLng: {\n      ...fallbacks,\n      ...{ default: [\"en\"] },\n    } as FallbackLng,\n  });\n\nexport default i18n;\nexport type TLocale = (typeof locales)[number];\n"
  },
  {
    "path": "web/src/index.css",
    "content": "@import \"tailwindcss\";\n@import \"tw-animate-css\";\n@import \"./themes/default.css\";\n\n@theme {\n  --default-transition-duration: 150ms;\n}\n\n@layer base {\n  * {\n    @apply border-border outline-none ring-0;\n  }\n  body {\n    @apply bg-background text-foreground;\n  }\n\n  /* ========================================\n   * Embedded Content\n   * ======================================== */\n\n  /* iframes (e.g., YouTube embeds, maps) */\n  iframe {\n    max-width: 100%;\n    border-radius: 0.5rem;\n    border: 1px solid var(--border);\n  }\n\n  /* KaTeX math rendering */\n  .katex-display {\n    overflow-x: auto;\n    overflow-y: hidden;\n    max-width: 100%;\n  }\n\n  /* Leaflet Popup Overrides */\n  .leaflet-popup-content-wrapper {\n    border-radius: 0.5rem !important;\n    border: 1px solid var(--border) !important;\n    background-color: var(--background) !important;\n    box-shadow: var(--shadow-lg) !important;\n  }\n\n  .leaflet-popup-content {\n    margin: 4px !important;\n    line-height: inherit !important;\n    font-size: inherit !important;\n  }\n\n  .leaflet-popup-tip {\n    background-color: var(--background) !important;\n  }\n}\n"
  },
  {
    "path": "web/src/layouts/MainLayout.tsx",
    "content": "import { useEffect, useMemo, useState } from \"react\";\nimport { matchPath, Outlet, useLocation } from \"react-router-dom\";\nimport type { MemoExplorerContext } from \"@/components/MemoExplorer\";\nimport { MemoExplorer, MemoExplorerDrawer } from \"@/components/MemoExplorer\";\nimport MobileHeader from \"@/components/MobileHeader\";\nimport { userServiceClient } from \"@/connect\";\nimport useCurrentUser from \"@/hooks/useCurrentUser\";\nimport { useFilteredMemoStats } from \"@/hooks/useFilteredMemoStats\";\nimport useMediaQuery from \"@/hooks/useMediaQuery\";\nimport { cn } from \"@/lib/utils\";\nimport { Routes } from \"@/router\";\n\nconst MainLayout = () => {\n  const md = useMediaQuery(\"md\");\n  const lg = useMediaQuery(\"lg\");\n  const location = useLocation();\n  const currentUser = useCurrentUser();\n  const [profileUserName, setProfileUserName] = useState<string | undefined>();\n\n  // Determine context based on current route\n  const context: MemoExplorerContext = useMemo(() => {\n    if (location.pathname === Routes.ROOT) return \"home\";\n    if (location.pathname === Routes.EXPLORE) return \"explore\";\n    if (matchPath(\"/archived\", location.pathname)) return \"archived\";\n    if (matchPath(\"/u/:username\", location.pathname)) return \"profile\";\n    return \"home\"; // fallback\n  }, [location.pathname]);\n\n  // Extract username from URL for profile context\n  useEffect(() => {\n    const match = matchPath(\"/u/:username\", location.pathname);\n    if (match && context === \"profile\") {\n      const username = match.params.username;\n      if (username) {\n        // Fetch or get user to obtain user name (e.g., \"users/123\")\n        // Note: User stats will be fetched by useFilteredMemoStats\n        userServiceClient\n          .getUser({ name: `users/${username}` })\n          .then((user) => {\n            setProfileUserName(user.name);\n          })\n          .catch((error) => {\n            console.error(\"Failed to fetch profile user:\", error);\n            setProfileUserName(undefined);\n          });\n      }\n    } else {\n      setProfileUserName(undefined);\n    }\n  }, [location.pathname, context]);\n\n  // Determine which user name to use for per-user stats.\n  // - home: current user's stats\n  // - profile: viewed user's stats\n  // - archived/explore: no user scope (each handled differently inside the hook)\n  const statsUserName = useMemo(() => {\n    if (context === \"home\") return currentUser?.name;\n    if (context === \"profile\") return profileUserName;\n    return undefined;\n  }, [context, currentUser, profileUserName]);\n\n  const { statistics, tags } = useFilteredMemoStats({ userName: statsUserName, context });\n\n  return (\n    <section className=\"@container w-full min-h-full flex flex-col justify-start items-center\">\n      {!md && (\n        <MobileHeader>\n          <MemoExplorerDrawer context={context} statisticsData={statistics} tagCount={tags} />\n        </MobileHeader>\n      )}\n      {md && (\n        <div className={cn(\"fixed top-0 left-16 shrink-0 h-svh transition-all\", \"border-r border-border\", lg ? \"w-72\" : \"w-56\")}>\n          <MemoExplorer className={cn(\"px-3 py-6\")} context={context} statisticsData={statistics} tagCount={tags} />\n        </div>\n      )}\n      <div className={cn(\"w-full min-h-full\", lg ? \"pl-72\" : md ? \"pl-56\" : \"\")}>\n        <div className={cn(\"w-full mx-auto px-4 sm:px-6 md:pt-6 pb-8\")}>\n          <Outlet />\n        </div>\n      </div>\n    </section>\n  );\n};\n\nexport default MainLayout;\n"
  },
  {
    "path": "web/src/layouts/RootLayout.tsx",
    "content": "import { useEffect, useMemo } from \"react\";\nimport { Outlet, useLocation, useSearchParams } from \"react-router-dom\";\nimport usePrevious from \"react-use/lib/usePrevious\";\nimport Navigation from \"@/components/Navigation\";\nimport { useMemoFilterContext } from \"@/contexts/MemoFilterContext\";\nimport useCurrentUser from \"@/hooks/useCurrentUser\";\nimport useMediaQuery from \"@/hooks/useMediaQuery\";\nimport useNavigateTo from \"@/hooks/useNavigateTo\";\nimport { cn } from \"@/lib/utils\";\nimport { ROUTES } from \"@/router/routes\";\nimport { redirectOnAuthFailure } from \"@/utils/auth-redirect\";\n\nconst RootLayout = () => {\n  const location = useLocation();\n  const [searchParams] = useSearchParams();\n  const sm = useMediaQuery(\"sm\");\n  const currentUser = useCurrentUser();\n  const navigateTo = useNavigateTo();\n  const { removeFilter } = useMemoFilterContext();\n  const pathname = useMemo(() => location.pathname, [location.pathname]);\n  const prevPathname = usePrevious(pathname);\n\n  useEffect(() => {\n    if (!currentUser) {\n      if (pathname === ROUTES.ROOT) {\n        navigateTo(ROUTES.EXPLORE);\n      } else {\n        redirectOnAuthFailure();\n      }\n    }\n  }, [currentUser, pathname, navigateTo]);\n\n  useEffect(() => {\n    // When the route changes and there is no filter in the search params, remove all filters\n    if (prevPathname !== pathname && !searchParams.has(\"filter\")) {\n      removeFilter(() => true);\n    }\n  }, [prevPathname, pathname, searchParams, removeFilter]);\n\n  return (\n    <div className=\"w-full min-h-full flex flex-row justify-center items-start sm:pl-16\">\n      {sm && (\n        <div\n          className={cn(\n            \"group flex flex-col justify-start items-start fixed top-0 left-0 select-none h-full bg-sidebar\",\n            \"w-16 px-2\",\n            \"border-r border-border\",\n          )}\n        >\n          <Navigation className=\"py-4 md:pt-6\" collapsed={true} />\n        </div>\n      )}\n      <main className=\"w-full h-auto grow shrink flex flex-col justify-start items-center\">\n        <Outlet />\n      </main>\n    </div>\n  );\n};\n\nexport default RootLayout;\n"
  },
  {
    "path": "web/src/lib/calendar-utils.ts",
    "content": "import dayjs from \"dayjs\";\n\nexport const MONTH_DATE_FORMAT = \"YYYY-MM\" as const;\n\nexport const formatMonth = (date: Date | string): string => {\n  return dayjs(date).format(MONTH_DATE_FORMAT);\n};\n\nexport const getYearFromDate = (date: Date | string): number => {\n  return dayjs(date).year();\n};\n\nexport const getMonthFromDate = (date: Date | string): number => {\n  return dayjs(date).month();\n};\n\nexport const addMonths = (date: Date | string, count: number): string => {\n  return dayjs(date).add(count, \"month\").format(MONTH_DATE_FORMAT);\n};\n\nexport const setYearAndMonth = (year: number, month: number): string => {\n  return dayjs().year(year).month(month).format(MONTH_DATE_FORMAT);\n};\n"
  },
  {
    "path": "web/src/lib/error.ts",
    "content": "export function getErrorMessage(error: unknown, fallback = \"Unknown error\"): string {\n  if (error instanceof Error) {\n    return error.message;\n  }\n\n  if (typeof error === \"string\") {\n    return error;\n  }\n\n  if (error && typeof error === \"object\" && \"message\" in error) {\n    return String(error.message);\n  }\n\n  return fallback;\n}\n\nexport function handleError(\n  error: unknown,\n  toast: (message: string) => void,\n  options?: {\n    context?: string;\n    fallbackMessage?: string;\n    onError?: (error: unknown) => void;\n  },\n): void {\n  const contextPrefix = options?.context ? `${options.context}: ` : \"\";\n  const fallback = options?.fallbackMessage;\n\n  const errorMessage = options?.context ? `${contextPrefix}${getErrorMessage(error, fallback)}` : getErrorMessage(error, fallback);\n\n  console.error(error);\n  toast(errorMessage);\n  options?.onError?.(error);\n}\n\nexport function isError(value: unknown): value is Error {\n  return value instanceof Error;\n}\n"
  },
  {
    "path": "web/src/lib/query-client.ts",
    "content": "import { Code, ConnectError } from \"@connectrpc/connect\";\nimport { QueryClient } from \"@tanstack/react-query\";\n\n// Don't retry requests that failed due to authentication errors.\n// The auth interceptor in connect.ts already handles token refresh and request retry.\n// If the interceptor still throws Unauthenticated, the session is truly gone and the\n// user will be redirected to /auth. A React Query retry would only fire a second\n// failed refresh attempt and a second redirect call while navigation is already in progress.\nconst shouldRetry = (failureCount: number, error: unknown): boolean => {\n  if (error instanceof ConnectError && error.code === Code.Unauthenticated) return false;\n  return failureCount < 1;\n};\n\nexport const queryClient = new QueryClient({\n  defaultOptions: {\n    queries: {\n      // Balanced approach: Fresh enough for collaboration, but reduces unnecessary refetches\n      // Individual queries can override with shorter staleTime if needed (e.g., notifications)\n      staleTime: 1000 * 30, // 30 seconds (increased from 10s for better performance)\n      gcTime: 1000 * 60 * 5, // 5 minutes (formerly cacheTime)\n      retry: shouldRetry,\n      refetchOnWindowFocus: true, // Refetch when user returns to tab\n      refetchOnReconnect: true, // Refetch when network reconnects\n    },\n    mutations: {\n      retry: shouldRetry,\n    },\n  },\n});\n"
  },
  {
    "path": "web/src/lib/utils.ts",
    "content": "import { type ClassValue, clsx } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}\n"
  },
  {
    "path": "web/src/locales/ar.json",
    "content": "{\n  \"about\": {\n    \"blogs\": \"مدونات\",\n    \"description\": \"خدمة تدوين ملاحظات خفيفة الوزن وتركز على الخصوصية. التقط وشارك أفكارك الرائعة بسهولة.\",\n    \"documents\": \"مستندات\",\n    \"github-repository\": \"مستودع GitHub\",\n    \"official-website\": \"الموقع الرسمي\"\n  },\n  \"auth\": {\n    \"create-your-account\": \"أنشئ حسابك\",\n    \"host-tip\": \"أنت تسجل في الموقع كمضيف\",\n    \"new-password\": \"كلمة المرور الجديدة\",\n    \"repeat-new-password\": \"أعد كلمة المرور الجديدة\",\n    \"sign-in-tip\": \"هل لديك حساب بالفعل؟\",\n    \"sign-up-tip\": \"ليس لديك حساب بعد؟\"\n  },\n  \"common\": {\n    \"about\": \"حول\",\n    \"add\": \"إضافة\",\n    \"admin\": \"مدير\",\n    \"all\": \"الكل\",\n    \"archive\": \"الأرشيف\",\n    \"archived\": \"المؤرشفة\",\n    \"attachments\": \"مرفقات\",\n    \"auto-expand\": \"توسيع تلقائي\",\n    \"avatar\": \"الصورة الشخصية\",\n    \"basic\": \"بسيط\",\n    \"beta\": \"تجريبي\",\n    \"calendar\": \"التقويم\",\n    \"cancel\": \"ألغِ\",\n    \"change\": \"تغيير\",\n    \"clear\": \"مسح\",\n    \"close\": \"أغلق\",\n    \"collapse\": \"طي\",\n    \"confirm\": \"تأكيد\",\n    \"copy\": \"نسخ\",\n    \"create\": \"إنشاء\",\n    \"created-at\": \"تاريخ الإنشاء\",\n    \"database\": \"قاعدة البيانات\",\n    \"day\": \"يوم\",\n    \"days\": {\n      \"fri\": \"جمعة\",\n      \"mon\": \"إثنين\",\n      \"sat\": \"سبت\",\n      \"sun\": \"أحد\",\n      \"thu\": \"خميس\",\n      \"tue\": \"ثلاثاء\",\n      \"wed\": \"أربعاء\"\n    },\n    \"delete\": \"حذف\",\n    \"description\": \"الوصف\",\n    \"edit\": \"تعديل\",\n    \"email\": \"البريد الإلكتروني\",\n    \"expand\": \"توسيع\",\n    \"explore\": \"استكشف\",\n    \"file\": \"ملف\",\n    \"filter\": \"تنقيح\",\n    \"home\": \"الصفحة الرئيسية\",\n    \"image\": \"صورة\",\n    \"in\": \"في\",\n    \"inbox\": \"الوارد\",\n    \"input\": \"إدخال\",\n    \"language\": \"اللغة\",\n    \"last-updated-at\": \"آخر تحديث في\",\n    \"learn-more\": \"تعلم المزيد\",\n    \"link\": \"الرابط\",\n    \"map\": \"خريطة\",\n    \"mark\": \"تعليم\",\n    \"memo\": \"مذكرة\",\n    \"memos\": \"مذكرات\",\n    \"more\": \"المزيد\",\n    \"name\": \"الاسم\",\n    \"new\": \"جديد\",\n    \"nickname\": \"الاسم المستعار\",\n    \"null\": \"فارغ\",\n    \"or\": \"أو\",\n    \"password\": \"كلمة المرور\",\n    \"pin\": \"ثبّت\",\n    \"pinned\": \"مثبت\",\n    \"preview\": \"معاينة\",\n    \"profile\": \"الملف الشخصي\",\n    \"properties\": \"خصائص\",\n    \"referenced-by\": \"مشار إليه بواسطة\",\n    \"referencing\": \"يشير إلى\",\n    \"relations\": \"العلاقات\",\n    \"remember-me\": \"تذكرني\",\n    \"rename\": \"إعادة التسمية\",\n    \"reset\": \"إعادة\",\n    \"resources\": \"المصادر\",\n    \"restore\": \"استعادة\",\n    \"role\": \"الدور\",\n    \"save\": \"احفظ\",\n    \"search\": \"بحث\",\n    \"select\": \"تحديد\",\n    \"settings\": \"الإعدادات\",\n    \"share\": \"مشاركة\",\n    \"shortcut-filter\": \"تصفية الاختصارات\",\n    \"shortcuts\": \"اختصارات\",\n    \"sign-in\": \"تسجيل الدخول\",\n    \"sign-in-with\": \"تسجيل دخول بواسطة {{provider}}\",\n    \"sign-out\": \"تسجيل خروج\",\n    \"sign-up\": \"تسجيل جديد\",\n    \"statistics\": \"إحصائيات\",\n    \"tags\": \"العلامات\",\n    \"title\": \"العنوان\",\n    \"today\": \"اليوم\",\n    \"tree-mode\": \"وضع الشجرة\",\n    \"type\": \"النوع\",\n    \"unpin\": \"إلغِ التثبيت\",\n    \"update\": \"تحديث\",\n    \"upload\": \"رفع\",\n    \"user\": \"مستخدم\",\n    \"username\": \"اسم المستخدم\",\n    \"version\": \"الإصدار\",\n    \"visibility\": \"الرؤية\",\n    \"yourself\": \"نفسك\"\n  },\n  \"editor\": {\n    \"add-your-comment-here\": \"أضف تعليقك هنا...\",\n    \"any-thoughts\": \"أية أفكار...\",\n    \"exit-focus-mode\": \"الخروج من وضع التركيز\",\n    \"focus-mode\": \"وضع التركيز\",\n    \"no-changes-detected\": \"لم يتم الكشف عن تغييرات\",\n    \"save\": \"حفظ\",\n    \"saving\": \"جارِ الحفظ...\",\n    \"slash-commands\": \"اكتب `/` للأوامر\"\n  },\n  \"inbox\": {\n    \"failed-to-load\": \"فشل تحميل عنصر صندوق الوارد\",\n    \"memo-comment\": \"{{user}} علق على {{memo}} الخاص بك.\",\n    \"no-archived\": \"لا توجد إشعارات مؤرشفة\",\n    \"no-unread\": \"لا توجد إشعارات غير مقروءة\",\n    \"unread\": \"غير مقروء\"\n  },\n  \"markdown\": {\n    \"checkbox\": \"مربع اختيار\",\n    \"code-block\": \"كتلة كود\",\n    \"content-syntax\": \"صياغة المحتوى\"\n  },\n  \"memo\": {\n    \"archived-at\": \"تاريخ الأرشفة\",\n    \"click-to-hide-nsfw-content\": \"انقر لإخفاء المحتوى الحساس\",\n    \"click-to-show-nsfw-content\": \"انقر لإظهار المحتوى الحساس\",\n    \"code\": \"كود\",\n    \"comment\": {\n      \"self\": \"التعليقات\",\n      \"write-a-comment\": \"اكتب تعليقاً\"\n    },\n    \"copy-content\": \"نسخ المحتوى\",\n    \"copy-link\": \"نسخ الرابط\",\n    \"count-memos-in-date\": \"{{count}} {{memos}} في {{date}}\",\n    \"delete-confirm\": \"هل أنت متأكد أنك تريد حذف هذه المذكرة؟\\n\\nهذا الإجراء لا رجعة فيه\",\n    \"delete-confirm-description\": \"هذا الإجراء لا رجعة فيه. ستتم إزالة المرفقات والروابط والمراجع أيضًا.\",\n    \"direction\": \"الاتجاه\",\n    \"direction-asc\": \"تصاعدي\",\n    \"direction-desc\": \"تنازلي\",\n    \"display-time\": \"عرض الوقت\",\n    \"filters\": {\n      \"has-code\": \"يحتوي على كود\",\n      \"has-link\": \"يحتوي على رابط\",\n      \"has-task-list\": \"يحتوي على قائمة مهام\"\n    },\n    \"links\": \"روابط\",\n    \"load-more\": \"تحميل المزيد\",\n    \"no-archived-memos\": \"لا يوجد مذكرات مؤرشفة\",\n    \"no-memos\": \"لا يوجد مذكرات.\",\n    \"order-by\": \"ترتيب حسب\",\n    \"search-placeholder\": \"ابحث عن مذكرة...\",\n    \"show-less\": \"عرض أقل\",\n    \"show-more\": \"عرض المزيد\",\n    \"to-do\": \"مهام\",\n    \"view-detail\": \"عرض التفاصيل\",\n    \"visibility\": {\n      \"disabled\": \"المذكرات العامة معطلة\",\n      \"private\": \"خاص\",\n      \"protected\": \"محمي\",\n      \"public\": \"عام\"\n    }\n  },\n  \"message\": {\n    \"archived-successfully\": \"تمت الأرشفة بنجاح\",\n    \"change-memo-created-time\": \"تغيير وقت إنشاء المذكرة\",\n    \"copied\": \"تم النسخ\",\n    \"deleted-successfully\": \"تم الحذف بنجاح\",\n    \"description-is-required\": \"الوصف مطلوب\",\n    \"failed-to-embed-memo\": \"فشل تضمين المذكرة\",\n    \"fill-all\": \"يرجى ملء جميع الحقول.\",\n    \"fill-all-required-fields\": \"يرجى ملء جميع الحقول المطلوبة\",\n    \"maximum-upload-size-is\": \"الحد الأقصى المسموح به للرفع هو {{size}} ميغابايت\",\n    \"memo-not-found\": \"المذكرة غير موجودة.\",\n    \"new-password-not-match\": \"كلمات المرور الجديدة غير متطابقة.\",\n    \"no-data\": \"لا توجد بيانات.\",\n    \"password-changed\": \"تم تغيير كلمة المرور\",\n    \"password-not-match\": \"كلمات المرور غير متطابقة.\",\n    \"restored-successfully\": \"تمت الاستعادة بنجاح\",\n    \"succeed-copy-content\": \"تم نسخ المحتوى بنجاح.\",\n    \"succeed-copy-link\": \"تم نسخ الرابط بنجاح.\",\n    \"update-succeed\": \"تم التحديث بنجاح\",\n    \"user-not-found\": \"المستخدم غير موجود\"\n  },\n  \"reference\": {\n    \"add-references\": \"إضافة مراجع\",\n    \"embedded-usage\": \"الاستخدام كمحتوى مضمّن\",\n    \"no-memos-found\": \"لم يتم العثور على مذكرات\",\n    \"search-placeholder\": \"ابحث في المحتوى\"\n  },\n  \"resource\": {\n    \"clear\": \"مسح\",\n    \"copy-link\": \"نسخ الرابط\",\n    \"create-dialog\": {\n      \"external-link\": {\n        \"file-name\": \"اسم الملف\",\n        \"file-name-placeholder\": \"اسم الملف\",\n        \"link\": \"رابط\",\n        \"link-placeholder\": \"https://the.link.to/your/resource\",\n        \"option\": \"رابط خارجي\",\n        \"type\": \"النوع\",\n        \"type-placeholder\": \"نوع الملف\"\n      },\n      \"local-file\": {\n        \"choose\": \"اختر ملف…\",\n        \"option\": \"ملف محلي\"\n      },\n      \"title\": \"إنشاء مصدر\",\n      \"upload-method\": \"طريقة الرفع\"\n    },\n    \"delete-all-unused\": \"حذف جميع الموارد غير المستخدمة\",\n    \"delete-all-unused-confirm\": \"هل أنت متأكد من حذف جميع الموارد غير المستخدمة؟ هذا الإجراء لا رجعة فيه\",\n    \"delete-all-unused-error\": \"فشل حذف الموارد غير المستخدمة\",\n    \"delete-all-unused-success\": \"تم حذف الموارد بنجاح\",\n    \"delete-resource\": \"حذف المصدر\",\n    \"delete-selected-resources\": \"حذف المصادر المحددة\",\n    \"fetching-data\": \"جلب البيانات…\",\n    \"file-drag-drop-prompt\": \"قم بسحب وإسقاط ملفك هنا لرفع الملف\",\n    \"linked-amount\": \"الكمية المرتبطة\",\n    \"no-files-selected\": \"لا يوجد ملفات محددة\",\n    \"no-resources\": \"لا يوجد مصادر.\",\n    \"no-unused-resources\": \"لا يوجد مصادر غير مستخدمة\",\n    \"reset-link\": \"إعادة تعيين الرابط\",\n    \"reset-link-prompt\": \"هل أنت متأكد من إعادة ضبط الرابط؟ سيؤدي هذا إلى قطع جميع استخدامات الارتباط الحالية.\\n\\nهذا الإجراء لا رجعة فيه\",\n    \"reset-resource-link\": \"إعادة تعيين رابط المصدر\",\n    \"unused-resources\": \"مصادر غير مستخدمة\"\n  },\n  \"router\": {\n    \"back-to-top\": \"اذهب إلى الأعلى\",\n    \"go-to-home\": \"اذهب إلى الصفحة الرئيسية\"\n  },\n  \"setting\": {\n    \"member\": {\n      \"admin\": \"مدير\",\n      \"archive-member\": \"أرشفة العضو\",\n      \"archive-success\": \"تم أرشفة {{username}} بنجاح\",\n      \"archive-warning\": \"هل أنت متأكد من أرشفة {{username}}؟\",\n      \"archive-warning-description\": \"الأرشفة تعطل الحساب. يمكنك استعادته أو حذفه لاحقًا.\",\n      \"create-a-member\": \"إنشاء عضو\",\n      \"delete-member\": \"حذف العضو\",\n      \"delete-success\": \"تم حذف {{username}} بنجاح\",\n      \"delete-warning\": \"هل أنت متأكد من حذف {{username}}؟ هذا الإجراء لا رجعة فيه\",\n      \"delete-warning-description\": \"هذا الإجراء لا رجعة فيه\",\n      \"restore-success\": \"تم استعادة {{username}} بنجاح\",\n      \"user\": \"مستخدم\",\n      \"label\": \"عضو\",\n      \"list-title\": \"قائمة الأعضاء\"\n    },\n    \"my-account\": {\n      \"label\": \"حسابي\"\n    },\n    \"preference\": {\n      \"default-memo-sort-option\": \"وقت عرض المذكرة\",\n      \"default-memo-visibility\": \"رؤية المذكرة الافتراضية\",\n      \"theme\": \"السمة\",\n      \"label\": \"التفضيلات\"\n    },\n    \"shortcut\": {\n      \"delete-confirm\": \"هل أنت متأكد من حذف الاختصار `{{title}}`؟\",\n      \"delete-success\": \"تم حذف الاختصار `{{title}}` بنجاح\"\n    },\n    \"sso\": {\n      \"authorization-endpoint\": \"نقطة نهاية التفويض\",\n      \"client-id\": \"معرف العميل\",\n      \"client-secret\": \"سر العميل\",\n      \"confirm-delete\": \"هل أنت متأكد من حذف إعداد SSO \\\"{{name}}\\\"؟ هذا الإجراء لا رجعة فيه\",\n      \"create-sso\": \"إنشاء SSO\",\n      \"custom\": \"مخصص\",\n      \"delete-sso\": \"تأكيد الحذف\",\n      \"disabled-password-login-warning\": \"تسجيل الدخول بكلمة المرور معطل، كن حذرًا عند إزالة مزودي الهوية\",\n      \"display-name\": \"اسم العرض\",\n      \"identifier\": \"المعرف\",\n      \"identifier-filter\": \"تصفية المعرف\",\n      \"no-sso-found\": \"لم يتم العثور على SSO.\",\n      \"redirect-url\": \"رابط إعادة التوجيه\",\n      \"scopes\": \"النطاقات\",\n      \"single-sign-on\": \"إعداد تسجيل الدخول الموحد (SSO) للمصادقة\",\n      \"sso-created\": \"تم إنشاء SSO {{name}}\",\n      \"sso-list\": \"قائمة SSO\",\n      \"sso-updated\": \"تم تحديث SSO {{name}}\",\n      \"template\": \"قالب\",\n      \"token-endpoint\": \"نقطة نهاية الرمز\",\n      \"update-sso\": \"تحديث SSO\",\n      \"user-endpoint\": \"نقطة نهاية المستخدم\",\n      \"label\": \"تسجيل موحد\"\n    },\n    \"storage\": {\n      \"accesskey\": \"مفتاح الوصول\",\n      \"accesskey-placeholder\": \"مفتاح الوصول / معرف الوصول\",\n      \"bucket\": \"دلو التخزين\",\n      \"bucket-placeholder\": \"اسم الدلو\",\n      \"create-a-service\": \"إنشاء خدمة\",\n      \"create-storage\": \"إنشاء تخزين\",\n      \"current-storage\": \"تخزين الكائنات الحالي\",\n      \"delete-storage\": \"حذف التخزين\",\n      \"endpoint\": \"نقطة النهاية\",\n      \"filepath-template\": \"قالب مسار الملف\",\n      \"local-storage-path\": \"مسار التخزين المحلي\",\n      \"path\": \"مسار التخزين\",\n      \"path-description\": \"يمكنك استخدام نفس المتغيرات الديناميكية من التخزين المحلي، مثل {filename}\",\n      \"path-placeholder\": \"مسار/مخصص\",\n      \"presign-placeholder\": \"رابط توقيع مسبق، اختياري\",\n      \"region\": \"المنطقة\",\n      \"region-placeholder\": \"اسم المنطقة\",\n      \"s3-compatible-url\": \"رابط متوافق مع S3\",\n      \"secretkey\": \"المفتاح السري\",\n      \"secretkey-placeholder\": \"المفتاح السري / مفتاح الوصول\",\n      \"storage-services\": \"خدمات التخزين\",\n      \"type-database\": \"قاعدة بيانات\",\n      \"type-local\": \"نظام ملفات محلي\",\n      \"update-a-service\": \"تحديث خدمة\",\n      \"update-local-path\": \"تحديث المسار المحلي\",\n      \"update-local-path-description\": \"مسار التخزين المحلي هو مسار نسبي لملف قاعدة البيانات الخاص بك\",\n      \"update-storage\": \"تحديث التخزين\",\n      \"url-prefix\": \"بادئة الرابط\",\n      \"url-prefix-placeholder\": \"بادئة رابط مخصصة، اختياري\",\n      \"url-suffix\": \"لاحقة الرابط\",\n      \"url-suffix-placeholder\": \"لاحقة رابط مخصصة، اختياري\",\n      \"warning-text\": \"هل أنت متأكد من حذف خدمة التخزين \\\"{{name}}\\\"؟ هذا الإجراء لا رجعة فيه\",\n      \"label\": \"المساحة التخزينية\"\n    },\n    \"system\": {\n      \"additional-script\": \"سكريبت إضافي\",\n      \"additional-script-placeholder\": \"كود JavaScript إضافي\",\n      \"additional-style\": \"نمط إضافي\",\n      \"additional-style-placeholder\": \"كود CSS إضافي\",\n      \"allow-user-signup\": \"السماح بتسجيل المستخدمين\",\n      \"customize-server\": {\n        \"description\": \"الوصف\",\n        \"icon-url\": \"رابط الأيقونة\",\n        \"locale\": \"لغة الخادم\",\n        \"title\": \"تخصيص الخادم\"\n      },\n      \"disable-password-login\": \"تعطيل تسجيل الدخول بكلمة المرور\",\n      \"disable-password-login-final-warning\": \"يرجى كتابة \\\"CONFIRM\\\" إذا كنت متأكدًا مما تفعله.\",\n      \"disable-password-login-warning\": \"سيؤدي ذلك إلى تعطيل تسجيل الدخول بكلمة المرور لجميع المستخدمين. لا يمكن تسجيل الدخول بدون التراجع عن هذا الإعداد في قاعدة البيانات إذا فشل مزودو الهوية. يجب أن تكون حذرًا عند إزالة مزود هوية\",\n      \"display-with-updated-time\": \"العرض مع وقت التحديث\",\n      \"enable-auto-compact\": \"تمكين الضغط التلقائي\",\n      \"enable-double-click-to-edit\": \"تمكين النقر المزدوج للتحرير\",\n      \"enable-password-login\": \"تمكين تسجيل الدخول بكلمة المرور\",\n      \"enable-password-login-warning\": \"سيؤدي ذلك إلى تمكين تسجيل الدخول بكلمة المرور لجميع المستخدمين. تابع فقط إذا كنت تريد أن يتمكن المستخدمون من تسجيل الدخول باستخدام كل من SSO وكلمة المرور\",\n      \"max-upload-size\": \"الحد الأقصى لحجم الرفع (ميغابايت)\",\n      \"max-upload-size-hint\": \"القيمة الموصى بها هي 32 ميغابايت.\",\n      \"removed-completed-task-list-items\": \"تمكين إزالة عناصر قائمة المهام المكتملة\",\n      \"server-name\": \"اسم الخادم\",\n      \"title\": \"عام\",\n      \"label\": \"النظام\"\n    },\n    \"version\": \"الإصدار\",\n    \"access-token\": {\n      \"access-token-copied-to-clipboard\": \"تم نسخ رمز الوصول إلى الحافظة\",\n      \"access-token-deleted\": \"تم حذف رمز الوصول `{{description}}`\",\n      \"access-token-deletion\": \"هل أنت متأكد من حذف رمز الوصول `{{description}}`؟\",\n      \"access-token-deletion-description\": \"هذا الإجراء لا رجعة فيه. ستحتاج إلى تحديث أي خدمات تستخدم هذا الرمز لاستخدام رمز جديد.\",\n      \"create-dialog\": {\n        \"access-token-created\": \"تم إنشاء رمز الوصول `{{description}}`\",\n        \"create-access-token\": \"إنشاء رمز وصول\",\n        \"created-at\": \"تاريخ الإنشاء\",\n        \"description\": \"الوصف\",\n        \"duration-1m\": \"شهر واحد\",\n        \"duration-8h\": \"8 ساعات\",\n        \"duration-never\": \"أبدًا\",\n        \"expiration\": \"انتهاء الصلاحية\",\n        \"expires-at\": \"ينتهي في\",\n        \"some-description\": \"بعض الوصف...\"\n      },\n      \"description\": \"قائمة بجميع رموز الوصول لحسابك.\",\n      \"title\": \"رموز الوصول\",\n      \"token\": \"رمز\"\n    },\n    \"account\": {\n      \"change-password\": \"تغيير كلمة المرور\",\n      \"email-note\": \"اختياري\",\n      \"export-memos\": \"تصدير المذكرات\",\n      \"nickname-note\": \"معروض في اللافتة\",\n      \"openapi-reset\": \"إعادة ضبط مفتاح OpenAPI\",\n      \"openapi-sample-post\": \"مرحبًا #مذكراتي من {{url}}\",\n      \"openapi-title\": \"OpenAPI\",\n      \"reset-api\": \"إعادة ضبط الـ API\",\n      \"title\": \"معلومات الحساب\",\n      \"update-information\": \"تحديث المعلومات\",\n      \"username-note\": \"تستخدم لتسجيل الدخول\"\n    },\n    \"instance\": {\n      \"disallow-change-nickname\": \"عدم السماح بتغيير الاسم المستعار\",\n      \"disallow-change-username\": \"عدم السماح بتغيير اسم المستخدم\",\n      \"disallow-password-auth\": \"عدم السماح بالمصادقة بكلمة المرور\",\n      \"disallow-user-registration\": \"عدم السماح بتسجيل المستخدمين\",\n      \"monday\": \"الإثنين\",\n      \"saturday\": \"السبت\",\n      \"sunday\": \"الأحد\",\n      \"week-start-day\": \"بداية الأسبوع\"\n    },\n    \"memo\": {\n      \"content-length-limit\": \"حد طول المحتوى (بايت)\",\n      \"enable-blur-nsfw-content\": \"تمكين طمس المحتوى الحساس (NSFW)\",\n      \"enable-memo-comments\": \"تمكين تعليقات المذكرة\",\n      \"enable-memo-location\": \"تمكين موقع المذكرة\",\n      \"reactions\": \"تفاعلات\",\n      \"title\": \"إعدادات المذكرة\",\n      \"label\": \"مذكرة\"\n    },\n    \"webhook\": {\n      \"create-dialog\": {\n        \"an-easy-to-remember-name\": \"اسم سهل التذكر\",\n        \"create-webhook\": \"إنشاء Webhook\",\n        \"create-webhook-success\": \"تم إنشاء Webhook `{{name}}`\",\n        \"edit-webhook\": \"تعديل Webhook\",\n        \"payload-url\": \"رابط الحمولة\",\n        \"title\": \"العنوان\",\n        \"url-example-post-receive\": \"https://example.com/postreceive\"\n      },\n      \"delete-dialog\": {\n        \"delete-webhook-description\": \"هذا الإجراء لا رجعة فيه.\",\n        \"delete-webhook-success\": \"تم حذف Webhook `{{name}}` بنجاح\",\n        \"delete-webhook-title\": \"هل أنت متأكد من حذف Webhook `{{name}}`؟\"\n      },\n      \"no-webhooks-found\": \"لم يتم العثور على Webhooks.\",\n      \"title\": \"Webhooks\",\n      \"url\": \"الرابط\"\n    }\n  },\n  \"tag\": {\n    \"all-tags\": \"جميع العلامات\",\n    \"create-tag\": \"إنشاء علامة\",\n    \"create-tags-guide\": \"يمكنك إنشاء علامات بإدخال `#tag`.\",\n    \"delete-confirm\": \"هل أنت متأكد من حذف هذه العلامة؟ سيتم أرشفة جميع المذكرات المتعلقة.\",\n    \"delete-success\": \"تم حذف العلامة بنجاح\",\n    \"delete-tag\": \"حذف العلامة\",\n    \"new-name\": \"اسم جديد\",\n    \"no-tag-found\": \"لم يتم العثور على علامات\",\n    \"old-name\": \"الاسم القديم\",\n    \"rename-error-empty\": \"لا يمكن أن يكون اسم العلامة فارغًا أو يحتوي على مسافات\",\n    \"rename-error-repeat\": \"لا يمكن أن يكون الاسم الجديد هو نفسه الاسم القديم\",\n    \"rename-success\": \"تمت إعادة تسمية العلامة بنجاح\",\n    \"rename-tag\": \"إعادة تسمية العلامة\",\n    \"rename-tip\": \"سيتم تحديث جميع مذكراتك بهذه العلامة.\"\n  },\n  \"tooltip\": {\n    \"link-memo\": \"ربط مذكرة\",\n    \"markdown-menu\": \"Markdown\",\n    \"select-location\": \"الموقع\",\n    \"select-visibility\": \"الرؤية\",\n    \"tags\": \"العلامات\",\n    \"upload-attachment\": \"رفع المرفق\"\n  }\n}\n"
  },
  {
    "path": "web/src/locales/ca.json",
    "content": "{\n  \"about\": {\n    \"blogs\": \"Blocs\",\n    \"description\": \"Un servei de presa de notes lleuger i centrat en la privadesa. Captura i comparteix fàcilment les teves grans idees.\",\n    \"documents\": \"Documents\",\n    \"github-repository\": \"Repositori GitHub\",\n    \"official-website\": \"Lloc web oficial\"\n  },\n  \"auth\": {\n    \"create-your-account\": \"Crea el teu compte\",\n    \"host-tip\": \"T'estàs registrant com a amfitrió.\",\n    \"new-password\": \"Nova contrasenya\",\n    \"repeat-new-password\": \"Repeteix la contrasenya\",\n    \"sign-in-tip\": \"Ja tens un compte?\",\n    \"sign-up-tip\": \"Encara no tens un compte?\"\n  },\n  \"common\": {\n    \"about\": \"Quant a\",\n    \"add\": \"Afegeix\",\n    \"admin\": \"Administrador\",\n    \"all\": \"Tots\",\n    \"archive\": \"Arxivar\",\n    \"archived\": \"Arxivat\",\n    \"attachments\": \"Adjunts\",\n    \"auto-expand\": \"Expansió automàtica\",\n    \"avatar\": \"Avatar\",\n    \"basic\": \"Bàsic\",\n    \"beta\": \"Beta\",\n    \"calendar\": \"Calendari\",\n    \"cancel\": \"Cancel·lar\",\n    \"change\": \"Canviar\",\n    \"clear\": \"Netejar\",\n    \"close\": \"Tanca\",\n    \"collapse\": \"Redueix\",\n    \"confirm\": \"Confirmar\",\n    \"copy\": \"Copia\",\n    \"create\": \"Crea\",\n    \"created-at\": \"Creat el\",\n    \"database\": \"Base de dades\",\n    \"day\": \"Dia\",\n    \"days\": {\n      \"fri\": \"Dv\",\n      \"mon\": \"Dl\",\n      \"sat\": \"Ds\",\n      \"sun\": \"Dg\",\n      \"thu\": \"Dj\",\n      \"tue\": \"Dt\",\n      \"wed\": \"Dc\"\n    },\n    \"delete\": \"Esborra\",\n    \"description\": \"Descripció\",\n    \"edit\": \"Edita\",\n    \"email\": \"Correu electrònic\",\n    \"expand\": \"Expandeix\",\n    \"explore\": \"Explora\",\n    \"file\": \"Fitxer\",\n    \"filter\": \"Filtra\",\n    \"home\": \"Inici\",\n    \"image\": \"Imatge\",\n    \"in\": \"A\",\n    \"inbox\": \"Bústia d'entrada\",\n    \"input\": \"Entrada\",\n    \"language\": \"Llenguatge\",\n    \"last-updated-at\": \"Darrera actualització el\",\n    \"learn-more\": \"Saber-ne més\",\n    \"link\": \"Enllaç\",\n    \"map\": \"Mapa\",\n    \"mark\": \"Marca\",\n    \"memo\": \"Memo\",\n    \"memos\": \"Memos\",\n    \"more\": \"Més\",\n    \"name\": \"Nom\",\n    \"new\": \"Nou\",\n    \"nickname\": \"Sobrenom\",\n    \"null\": \"Nul\",\n    \"or\": \"o\",\n    \"password\": \"Contrasenya\",\n    \"pin\": \"Enganxa\",\n    \"pinned\": \"Enganxat\",\n    \"preview\": \"Previsualitza\",\n    \"profile\": \"Perfil\",\n    \"properties\": \"Propietats\",\n    \"referenced-by\": \"Referenciat per\",\n    \"referencing\": \"Referenciant\",\n    \"relations\": \"Relacions\",\n    \"remember-me\": \"Recorda'm\",\n    \"rename\": \"Reanomena\",\n    \"reset\": \"Reinicia\",\n    \"resources\": \"Recursos\",\n    \"restore\": \"Restaura\",\n    \"role\": \"Rol\",\n    \"save\": \"Guarda\",\n    \"search\": \"Cerca\",\n    \"select\": \"Selecciona\",\n    \"settings\": \"Configuració\",\n    \"share\": \"Comparteix\",\n    \"shortcut-filter\": \"Filtre de dreceres\",\n    \"shortcuts\": \"Dreceres\",\n    \"sign-in\": \"Identifica't\",\n    \"sign-in-with\": \"Identifica't via {{provider}}\",\n    \"sign-out\": \"Surt\",\n    \"sign-up\": \"Registra't\",\n    \"statistics\": \"Estadístiques\",\n    \"tags\": \"Etiquetes\",\n    \"title\": \"Títol\",\n    \"today\": \"Avui\",\n    \"tree-mode\": \"Mode arbre\",\n    \"type\": \"Tipus\",\n    \"unpin\": \"Desenganxa\",\n    \"update\": \"Actualitza\",\n    \"upload\": \"Puja\",\n    \"user\": \"Usuari\",\n    \"username\": \"Nom d'usuari\",\n    \"version\": \"Versió\",\n    \"visibility\": \"Visibilitat\",\n    \"yourself\": \"Tu mateix\"\n  },\n  \"editor\": {\n    \"add-your-comment-here\": \"Afegeix el teu comentari aquí...\",\n    \"any-thoughts\": \"Alguna idea...\",\n    \"exit-focus-mode\": \"Surt del mode focus\",\n    \"focus-mode\": \"Mode focus\",\n    \"no-changes-detected\": \"No s'han detectat canvis\",\n    \"save\": \"Guarda\",\n    \"saving\": \"Guardant...\",\n    \"slash-commands\": \"Escriu `/` per a comandaments\"\n  },\n  \"inbox\": {\n    \"failed-to-load\": \"Error en carregar la bústia\",\n    \"memo-comment\": \"{{user}} ha comentat la teva {{memo}}.\",\n    \"no-archived\": \"No hi ha notificacions arxivades\",\n    \"no-unread\": \"No hi ha notificacions no llegides\",\n    \"unread\": \"No llegit\"\n  },\n  \"markdown\": {\n    \"checkbox\": \"Casella de selecció\",\n    \"code-block\": \"Bloc de codi\",\n    \"content-syntax\": \"Sintaxi de contingut\"\n  },\n  \"memo\": {\n    \"archived-at\": \"Arxivat el\",\n    \"click-to-hide-nsfw-content\": \"Fes clic per ocultar contingut sensible\",\n    \"click-to-show-nsfw-content\": \"Fes clic per mostrar contingut sensible\",\n    \"code\": \"Codi\",\n    \"comment\": {\n      \"self\": \"Comentaris\",\n      \"write-a-comment\": \"Escriu un comentari\"\n    },\n    \"copy-content\": \"Copia el contingut\",\n    \"copy-link\": \"Copia l'enllaç\",\n    \"count-memos-in-date\": \"{{count}} {{memos}} el {{date}}\",\n    \"delete-confirm\": \"Estàs segur que vols esborrar aquesta nota? AQUESTA ACCIÓ ÉS IRREVERSIBLE\",\n    \"delete-confirm-description\": \"Aquesta acció és irreversible. Els adjunts, enllaços i referències també s'eliminaran.\",\n    \"direction\": \"Direcció\",\n    \"direction-asc\": \"Ascendent\",\n    \"direction-desc\": \"Descendent\",\n    \"display-time\": \"Mostra l'hora\",\n    \"filters\": {\n      \"has-code\": \"téCodi\",\n      \"has-link\": \"téEnllaç\",\n      \"has-task-list\": \"téLlistaTasques\"\n    },\n    \"links\": \"Enllaços\",\n    \"load-more\": \"Carrega més\",\n    \"no-archived-memos\": \"No hi ha notes arxivades.\",\n    \"no-memos\": \"No hi ha notes.\",\n    \"order-by\": \"Ordena per\",\n    \"search-placeholder\": \"Cerca notes...\",\n    \"show-less\": \"Mostra menys\",\n    \"show-more\": \"Mostra més\",\n    \"to-do\": \"Tasques\",\n    \"view-detail\": \"Veure detall\",\n    \"visibility\": {\n      \"disabled\": \"Les notes públiques estan desactivades\",\n      \"private\": \"Privat\",\n      \"protected\": \"Espai de treball\",\n      \"public\": \"Públic\"\n    }\n  },\n  \"message\": {\n    \"archived-successfully\": \"Arxivat correctament\",\n    \"change-memo-created-time\": \"Canvia l'hora de creació de la nota\",\n    \"copied\": \"Copiat\",\n    \"deleted-successfully\": \"Eliminat correctament\",\n    \"description-is-required\": \"La descripció és obligatòria\",\n    \"failed-to-embed-memo\": \"No s'ha pogut incrustar la nota\",\n    \"fill-all\": \"Si us plau, omple tots els camps.\",\n    \"fill-all-required-fields\": \"Si us plau, omple tots els camps obligatoris\",\n    \"maximum-upload-size-is\": \"La mida màxima permesa de pujada és {{size}} MiB\",\n    \"memo-not-found\": \"Nota no trobada.\",\n    \"new-password-not-match\": \"Les noves contrasenyes no coincideixen.\",\n    \"no-data\": \"No s'han trobat dades.\",\n    \"password-changed\": \"Contrasenya canviada\",\n    \"password-not-match\": \"Les contrasenyes no coincideixen.\",\n    \"restored-successfully\": \"Restaurat correctament\",\n    \"succeed-copy-content\": \"Contingut copiat correctament.\",\n    \"succeed-copy-link\": \"Enllaç copiat correctament.\",\n    \"update-succeed\": \"Actualització correcta\",\n    \"user-not-found\": \"Usuari no trobat\"\n  },\n  \"reference\": {\n    \"add-references\": \"Afegeix referències\",\n    \"embedded-usage\": \"Ús com a contingut incrustat\",\n    \"no-memos-found\": \"No s'han trobat notes\",\n    \"search-placeholder\": \"Cerca contingut\"\n  },\n  \"resource\": {\n    \"clear\": \"Netejar\",\n    \"copy-link\": \"Copia l'enllaç\",\n    \"create-dialog\": {\n      \"external-link\": {\n        \"file-name\": \"Nom del fitxer\",\n        \"file-name-placeholder\": \"Nom del fitxer\",\n        \"link\": \"Enllaç\",\n        \"link-placeholder\": \"https://the.link.to/your/resource\",\n        \"option\": \"Enllaç extern\",\n        \"type\": \"Tipus\",\n        \"type-placeholder\": \"Tipus de fitxer\"\n      },\n      \"local-file\": {\n        \"choose\": \"Tria un fitxer…\",\n        \"option\": \"Fitxer local\"\n      },\n      \"title\": \"Crea recurs\",\n      \"upload-method\": \"Mètode de pujada\"\n    },\n    \"delete-all-unused\": \"Elimina tots els sense ús\",\n    \"delete-all-unused-confirm\": \"Estàs segur que vols eliminar tots els recursos sense ús? AQUESTA ACCIÓ ÉS IRREVERSIBLE\",\n    \"delete-all-unused-error\": \"Error en eliminar els recursos sense ús\",\n    \"delete-all-unused-success\": \"Recursos eliminats correctament\",\n    \"delete-resource\": \"Elimina recurs\",\n    \"delete-selected-resources\": \"Elimina recursos seleccionats\",\n    \"fetching-data\": \"Carregant dades…\",\n    \"file-drag-drop-prompt\": \"Arrossega i deixa anar el teu fitxer aquí per pujar-lo\",\n    \"linked-amount\": \"Quantitat enllaçada\",\n    \"no-files-selected\": \"No s'ha seleccionat cap fitxer\",\n    \"no-resources\": \"No hi ha recursos.\",\n    \"no-unused-resources\": \"No hi ha recursos sense ús\",\n    \"reset-link\": \"Reinicia l'enllaç\",\n    \"reset-link-prompt\": \"Estàs segur que vols reiniciar l'enllaç? Això trencarà tots els usos actuals de l'enllaç. AQUESTA ACCIÓ ÉS IRREVERSIBLE\",\n    \"reset-resource-link\": \"Reinicia l'enllaç del recurs\",\n    \"unused-resources\": \"Recursos sense ús\"\n  },\n  \"router\": {\n    \"back-to-top\": \"Torna a dalt\",\n    \"go-to-home\": \"Vés a l'inici\"\n  },\n  \"setting\": {\n    \"member\": {\n      \"admin\": \"Administrador\",\n      \"archive-member\": \"Arxiva el membre\",\n      \"archive-success\": \"{{username}} arxivat correctament\",\n      \"archive-warning\": \"Estàs segur que vols arxivar {{username}}?\",\n      \"archive-warning-description\": \"Arxivar desactiva el compte. Pots restaurar-lo o eliminar-lo més tard.\",\n      \"create-a-member\": \"Crea un membre\",\n      \"delete-member\": \"Elimina el membre\",\n      \"delete-success\": \"{{username}} eliminat correctament\",\n      \"delete-warning\": \"Estàs segur que vols eliminar {{username}}?\",\n      \"delete-warning-description\": \"AQUESTA ACció ÉS IRREVERSIBLE\",\n      \"restore-success\": \"{{username}} restaurat correctament\",\n      \"user\": \"Usuari\",\n      \"label\": \"Membre\",\n      \"list-title\": \"Llista de membres\"\n    },\n    \"my-account\": {\n      \"label\": \"El meu compte\"\n    },\n    \"preference\": {\n      \"default-memo-sort-option\": \"Hora de visualització de la nota\",\n      \"default-memo-visibility\": \"Visibilitat per defecte de la nota\",\n      \"theme\": \"Tema\",\n      \"label\": \"Preferències\"\n    },\n    \"shortcut\": {\n      \"delete-confirm\": \"Estàs segur que vols eliminar la drecera `{{title}}`?\",\n      \"delete-success\": \"Drecera `{{title}}` eliminada correctament\"\n    },\n    \"sso\": {\n      \"authorization-endpoint\": \"Punt final d'autorització\",\n      \"client-id\": \"ID de client\",\n      \"client-secret\": \"Secret de client\",\n      \"confirm-delete\": \"Estàs segur que vols eliminar la configuració SSO \\\"{{name}}\\\"? AQUESTA ACCIÓ ÉS IRREVERSIBLE\",\n      \"create-sso\": \"Crea SSO\",\n      \"custom\": \"Personalitzat\",\n      \"delete-sso\": \"Confirma l'eliminació\",\n      \"disabled-password-login-warning\": \"L'inici de sessió amb contrasenya està desactivat, vés amb compte en eliminar proveïdors d'identitat\",\n      \"display-name\": \"Nom a mostrar\",\n      \"identifier\": \"Identificador\",\n      \"identifier-filter\": \"Filtre d'identificador\",\n      \"no-sso-found\": \"No s'ha trobat cap SSO.\",\n      \"redirect-url\": \"URL de redirecció\",\n      \"scopes\": \"Àmbits\",\n      \"single-sign-on\": \"Configuració de Single Sign-On (SSO) per a autenticació\",\n      \"sso-created\": \"SSO {{name}} creat\",\n      \"sso-list\": \"Llista SSO\",\n      \"sso-updated\": \"SSO {{name}} actualitzat\",\n      \"template\": \"Plantilla\",\n      \"token-endpoint\": \"Punt final de token\",\n      \"update-sso\": \"Actualitza SSO\",\n      \"user-endpoint\": \"Punt final d'usuari\",\n      \"label\": \"SSO\"\n    },\n    \"storage\": {\n      \"accesskey\": \"Clau d'accés\",\n      \"accesskey-placeholder\": \"Clau d'accés / ID d'accés\",\n      \"bucket\": \"Bucket\",\n      \"bucket-placeholder\": \"Nom del bucket\",\n      \"create-a-service\": \"Crea un servei\",\n      \"create-storage\": \"Crea emmagatzematge\",\n      \"current-storage\": \"Emmagatzematge d'objectes actual\",\n      \"delete-storage\": \"Elimina emmagatzematge\",\n      \"endpoint\": \"Punt final\",\n      \"filepath-template\": \"Plantilla de ruta de fitxer\",\n      \"local-storage-path\": \"Ruta d'emmagatzematge local\",\n      \"path\": \"Ruta d'emmagatzematge\",\n      \"path-description\": \"Pots utilitzar les mateixes variables dinàmiques que a l'emmagatzematge local, com {filename}\",\n      \"path-placeholder\": \"ruta/personalitzada\",\n      \"presign-placeholder\": \"URL pre-signada, opcional\",\n      \"region\": \"Regió\",\n      \"region-placeholder\": \"Nom de la regió\",\n      \"s3-compatible-url\": \"URL compatible amb S3\",\n      \"secretkey\": \"Clau secreta\",\n      \"secretkey-placeholder\": \"Clau secreta / Clau d'accés\",\n      \"storage-services\": \"Serveis d'emmagatzematge\",\n      \"type-database\": \"Base de dades\",\n      \"type-local\": \"Sistema de fitxers local\",\n      \"update-a-service\": \"Actualitza un servei\",\n      \"update-local-path\": \"Actualitza la ruta local\",\n      \"update-local-path-description\": \"La ruta d'emmagatzematge local és una ruta relativa al teu fitxer de base de dades\",\n      \"update-storage\": \"Actualitza l'emmagatzematge\",\n      \"url-prefix\": \"Prefix d'URL\",\n      \"url-prefix-placeholder\": \"Prefix d'URL personalitzat, opcional\",\n      \"url-suffix\": \"Sufix d'URL\",\n      \"url-suffix-placeholder\": \"Sufix d'URL personalitzat, opcional\",\n      \"warning-text\": \"Estàs segur que vols eliminar el servei d'emmagatzematge \\\"{{name}}\\\"? AQUESTA ACCIÓ ÉS IRREVERSIBLE\",\n      \"label\": \"Emmagatzematge\"\n    },\n    \"system\": {\n      \"additional-script\": \"Script addicional\",\n      \"additional-script-placeholder\": \"Codi JavaScript addicional\",\n      \"additional-style\": \"Estil addicional\",\n      \"additional-style-placeholder\": \"Codi CSS addicional\",\n      \"allow-user-signup\": \"Permet el registre d'usuaris\",\n      \"customize-server\": {\n        \"description\": \"Descripció\",\n        \"icon-url\": \"URL de la icona\",\n        \"locale\": \"Llengua del servidor\",\n        \"title\": \"Personalitza el servidor\"\n      },\n      \"disable-password-login\": \"Desactiva l'inici de sessió amb contrasenya\",\n      \"disable-password-login-final-warning\": \"Escriu \\\"CONFIRM\\\" si saps el que estàs fent.\",\n      \"disable-password-login-warning\": \"Això desactivarà l'inici de sessió amb contrasenya per a tots els usuaris. No serà possible iniciar sessió sense revertir aquesta configuració a la base de dades si els proveïdors d'identitat fallen. Vés amb compte en eliminar un proveïdor d'identitat\",\n      \"display-with-updated-time\": \"Mostra amb hora d'actualització\",\n      \"enable-auto-compact\": \"Habilita la compactació automàtica\",\n      \"enable-double-click-to-edit\": \"Habilita doble clic per editar\",\n      \"enable-password-login\": \"Habilita l'inici de sessió amb contrasenya\",\n      \"enable-password-login-warning\": \"Això habilitarà l'inici de sessió amb contrasenya per a tots els usuaris. Continua només si vols que els usuaris puguin iniciar sessió amb SSO i contrasenya\",\n      \"max-upload-size\": \"Mida màxima de pujada (MiB)\",\n      \"max-upload-size-hint\": \"El valor recomanat és 32 MiB.\",\n      \"removed-completed-task-list-items\": \"Habilita l'eliminació de tasques completades\",\n      \"server-name\": \"Nom del servidor\",\n      \"title\": \"General\",\n      \"label\": \"Sistema\"\n    },\n    \"version\": \"Versió\",\n    \"access-token\": {\n      \"access-token-copied-to-clipboard\": \"Token d'accés copiat al porta-retalls\",\n      \"access-token-deleted\": \"Token d'accés `{{description}}` eliminat\",\n      \"access-token-deletion\": \"Estàs segur que vols eliminar el token d'accés `{{description}}`?\",\n      \"access-token-deletion-description\": \"Aquesta acció és irreversible. Hauràs d'actualitzar qualsevol servei que utilitzi aquest token per utilitzar un de nou.\",\n      \"create-dialog\": {\n        \"access-token-created\": \"Token d'accés `{{description}}` creat\",\n        \"create-access-token\": \"Crea token d'accés\",\n        \"created-at\": \"Creat el\",\n        \"description\": \"Descripció\",\n        \"duration-1m\": \"1 mes\",\n        \"duration-8h\": \"8 hores\",\n        \"duration-never\": \"Mai\",\n        \"expiration\": \"Caducitat\",\n        \"expires-at\": \"Caduca el\",\n        \"some-description\": \"Alguna descripció...\"\n      },\n      \"description\": \"Llista de tots els tokens d'accés del teu compte.\",\n      \"title\": \"Tokens d'accés\",\n      \"token\": \"Token\"\n    },\n    \"account\": {\n      \"change-password\": \"Canvia la contrasenya\",\n      \"email-note\": \"Opcional\",\n      \"export-memos\": \"Exporta notes\",\n      \"nickname-note\": \"Mostrat a la capçalera\",\n      \"openapi-reset\": \"Reinicia la clau OpenAPI\",\n      \"openapi-sample-post\": \"Hola #memos des de {{url}}\",\n      \"openapi-title\": \"OpenAPI\",\n      \"reset-api\": \"Reinicia l'API\",\n      \"title\": \"Informació del compte\",\n      \"update-information\": \"Actualitza la informació\",\n      \"username-note\": \"Utilitzat per iniciar sessió\"\n    },\n    \"instance\": {\n      \"disallow-change-nickname\": \"No permetis canviar el sobrenom\",\n      \"disallow-change-username\": \"No permetis canviar el nom d'usuari\",\n      \"disallow-password-auth\": \"No permetis autenticació per contrasenya\",\n      \"disallow-user-registration\": \"No permetis el registre d'usuaris\",\n      \"monday\": \"Dilluns\",\n      \"saturday\": \"Dissabte\",\n      \"sunday\": \"Diumenge\",\n      \"week-start-day\": \"Dia d'inici de setmana\"\n    },\n    \"memo\": {\n      \"content-length-limit\": \"Límit de longitud del contingut (Bytes)\",\n      \"enable-blur-nsfw-content\": \"Habilita el difuminat de contingut sensible (NSFW)\",\n      \"enable-memo-comments\": \"Habilita els comentaris a les notes\",\n      \"enable-memo-location\": \"Habilita la ubicació de la nota\",\n      \"reactions\": \"Reaccions\",\n      \"title\": \"Configuració de notes\",\n      \"label\": \"Nota\"\n    },\n    \"webhook\": {\n      \"create-dialog\": {\n        \"an-easy-to-remember-name\": \"Un nom fàcil de recordar\",\n        \"create-webhook\": \"Crea webhook\",\n        \"create-webhook-success\": \"Webhook `{{name}}` creat\",\n        \"edit-webhook\": \"Edita webhook\",\n        \"payload-url\": \"URL de càrrega\",\n        \"title\": \"Títol\",\n        \"url-example-post-receive\": \"https://example.com/postreceive\"\n      },\n      \"delete-dialog\": {\n        \"delete-webhook-description\": \"Aquesta acció és irreversible.\",\n        \"delete-webhook-success\": \"Webhook `{{name}}` eliminat correctament\",\n        \"delete-webhook-title\": \"Estàs segur que vols eliminar el webhook `{{name}}`?\"\n      },\n      \"no-webhooks-found\": \"No s'han trobat webhooks.\",\n      \"title\": \"Webhooks\",\n      \"url\": \"URL\"\n    }\n  },\n  \"tag\": {\n    \"all-tags\": \"Totes les etiquetes\",\n    \"create-tag\": \"Crea etiqueta\",\n    \"create-tags-guide\": \"Pots crear etiquetes escrivint `#etiqueta`.\",\n    \"delete-confirm\": \"Estàs segur que vols eliminar aquesta etiqueta? Totes les notes relacionades s'arxivaran.\",\n    \"delete-success\": \"Etiqueta eliminada correctament\",\n    \"delete-tag\": \"Elimina etiqueta\",\n    \"new-name\": \"Nom nou\",\n    \"no-tag-found\": \"No s'ha trobat cap etiqueta\",\n    \"old-name\": \"Nom antic\",\n    \"rename-error-empty\": \"El nom de l'etiqueta no pot estar buit ni contenir espais\",\n    \"rename-error-repeat\": \"El nom nou no pot ser igual que l'antic\",\n    \"rename-success\": \"Etiqueta reanomenada correctament\",\n    \"rename-tag\": \"Reanomena etiqueta\",\n    \"rename-tip\": \"Totes les teves notes amb aquesta etiqueta s'actualitzaran.\"\n  },\n  \"tooltip\": {\n    \"link-memo\": \"Enllaça nota\",\n    \"markdown-menu\": \"Markdown\",\n    \"select-location\": \"Ubicació\",\n    \"select-visibility\": \"Visibilitat\",\n    \"tags\": \"Etiquetes\",\n    \"upload-attachment\": \"Puja adjunt(s)\"\n  }\n}\n"
  },
  {
    "path": "web/src/locales/cs.json",
    "content": "{\n  \"about\": {\n    \"blogs\": \"Blogy\",\n    \"description\": \"Jednoduchá služba pro psaní poznámek, která dbá především na soukromí. Snadno zaznamenávejte a sdílejte své skvělé myšlenky.\",\n    \"documents\": \"Dokumenty\",\n    \"github-repository\": \"GitHub Repo\",\n    \"official-website\": \"Oficiální web\"\n  },\n  \"auth\": {\n    \"create-your-account\": \"Založit nový účet\",\n    \"host-tip\": \"Registrujete se jako Hostitel stránek.\",\n    \"new-password\": \"Nové heslo\",\n    \"repeat-new-password\": \"Zopakujte nové heslo\",\n    \"sign-in-tip\": \"Už máte účet?\",\n    \"sign-up-tip\": \"Ještě nemáte účet?\"\n  },\n  \"common\": {\n    \"about\": \"O aplikaci\",\n    \"add\": \"Přidat\",\n    \"admin\": \"Admin\",\n    \"all\": \"Vše\",\n    \"archive\": \"Archivovat\",\n    \"archived\": \"Archivované\",\n    \"attachments\": \"Přílohy\",\n    \"auto-expand\": \"Automaticky rozbalit\",\n    \"avatar\": \"Avatar\",\n    \"basic\": \"Základní\",\n    \"beta\": \"Beta\",\n    \"calendar\": \"Kalendář\",\n    \"cancel\": \"Zrušit\",\n    \"change\": \"Změnit\",\n    \"clear\": \"Vymazat\",\n    \"close\": \"Zavřít\",\n    \"collapse\": \"Sbalit\",\n    \"confirm\": \"Potvrdit\",\n    \"copy\": \"Kopírovat\",\n    \"create\": \"Vytvořit\",\n    \"created-at\": \"Vytvořeno\",\n    \"database\": \"Databáze\",\n    \"day\": \"Den\",\n    \"days\": {\n      \"fri\": \"Pá\",\n      \"mon\": \"Po\",\n      \"sat\": \"So\",\n      \"sun\": \"Ne\",\n      \"thu\": \"Čt\",\n      \"tue\": \"Út\",\n      \"wed\": \"St\"\n    },\n    \"delete\": \"Smazat\",\n    \"description\": \"Popis\",\n    \"edit\": \"Upravit\",\n    \"email\": \"E-mail\",\n    \"expand\": \"Rozbalit\",\n    \"explore\": \"Prozkoumat\",\n    \"file\": \"Soubor\",\n    \"filter\": \"Filtr\",\n    \"home\": \"Domů\",\n    \"image\": \"Obrázek\",\n    \"in\": \"V\",\n    \"inbox\": \"Došlá pošta\",\n    \"input\": \"Vstup\",\n    \"language\": \"Jazyk\",\n    \"last-updated-at\": \"Změněno\",\n    \"learn-more\": \"Více informací\",\n    \"link\": \"Odkaz\",\n    \"map\": \"Mapa\",\n    \"mark\": \"Značka\",\n    \"memo\": \"Poznámka\",\n    \"memos\": \"Poznámky\",\n    \"more\": \"Více\",\n    \"name\": \"Jméno\",\n    \"new\": \"Nový\",\n    \"nickname\": \"Přezdívka\",\n    \"null\": \"Prázdné\",\n    \"or\": \"nebo\",\n    \"password\": \"Heslo\",\n    \"pin\": \"Připnout\",\n    \"pinned\": \"Připnutá\",\n    \"preview\": \"Náhled\",\n    \"profile\": \"Profil\",\n    \"properties\": \"Vlastnosti\",\n    \"referenced-by\": \"Odkazováno\",\n    \"referencing\": \"Odkazuje na\",\n    \"relations\": \"Souvislosti\",\n    \"remember-me\": \"Zapamatovat si mě\",\n    \"rename\": \"Přejmenovat\",\n    \"reset\": \"Resetovat\",\n    \"resources\": \"Zdroje\",\n    \"restore\": \"Obnovit\",\n    \"role\": \"Role\",\n    \"save\": \"Uložit\",\n    \"search\": \"Hledat\",\n    \"select\": \"Vybrat\",\n    \"settings\": \"Nastavení\",\n    \"share\": \"Sdílet\",\n    \"shortcut-filter\": \"Filtr zkratek\",\n    \"shortcuts\": \"Zkratky\",\n    \"sign-in\": \"Přihlásit se\",\n    \"sign-in-with\": \"Přihlásit se pomocí {{provider}}\",\n    \"sign-out\": \"Odhlásit se\",\n    \"sign-up\": \"Zaregistrovat se\",\n    \"statistics\": \"Statistiky\",\n    \"tags\": \"Štítky\",\n    \"title\": \"Název\",\n    \"today\": \"Dnes\",\n    \"tree-mode\": \"Stromový pohled\",\n    \"type\": \"Typ\",\n    \"unpin\": \"Odepnout\",\n    \"update\": \"Aktualizovat\",\n    \"upload\": \"Nahrát\",\n    \"user\": \"Uživatel\",\n    \"username\": \"Uživatelské jméno\",\n    \"version\": \"Verze\",\n    \"visibility\": \"Viditelnost\",\n    \"yourself\": \"Vy sami\"\n  },\n  \"editor\": {\n    \"add-your-comment-here\": \"Přidejte sem svůj komentář...\",\n    \"any-thoughts\": \"Jakékoli myšlenky...\",\n    \"exit-focus-mode\": \"Ukončit režim soustředění\",\n    \"focus-mode\": \"Režim soustředění\",\n    \"no-changes-detected\": \"Nebyly zjištěny žádné změny\",\n    \"save\": \"Uložit\",\n    \"saving\": \"Ukládání...\",\n    \"slash-commands\": \"Pro příkazy zadejte `/`\"\n  },\n  \"inbox\": {\n    \"failed-to-load\": \"Nepodařilo se načíst položku doručené pošty\",\n    \"memo-comment\": \"{{user}} komentoval k vaší {{memo}}.\",\n    \"no-archived\": \"Žádná archivovaná oznámení\",\n    \"no-unread\": \"Žádná nepřečtená oznámení\",\n    \"unread\": \"Nepřečtené\"\n  },\n  \"markdown\": {\n    \"checkbox\": \"Zaškrtávací pole\",\n    \"code-block\": \"Blok kódu\",\n    \"content-syntax\": \"Syntaxe obsahu\"\n  },\n  \"memo\": {\n    \"archived-at\": \"Archivováno na\",\n    \"click-to-hide-nsfw-content\": \"Klikněte pro skrytí citlivého obsahu\",\n    \"click-to-show-nsfw-content\": \"Klikněte pro zobrazení citlivého obsahu\",\n    \"code\": \"Kód\",\n    \"comment\": {\n      \"self\": \"Komentáře\",\n      \"write-a-comment\": \"Napsat komentář\"\n    },\n    \"copy-content\": \"Kopírovat obsah\",\n    \"copy-link\": \"Kopírovat odkaz\",\n    \"count-memos-in-date\": \"{{count}} {{memos}} v {{date}}\",\n    \"delete-confirm\": \"Opravdu chcete smazat tuto poznámku?\",\n    \"delete-confirm-description\": \"Tato akce je nevratná. Přílohy, odkazy a reference budou také odstraněny.\",\n    \"direction\": \"Směr\",\n    \"direction-asc\": \"Vzestupně\",\n    \"direction-desc\": \"Sestupně\",\n    \"display-time\": \"Doba zobrazení\",\n    \"filters\": {\n      \"has-code\": \"maKod\",\n      \"has-link\": \"maOdkaz\",\n      \"has-task-list\": \"maSeznamUkolu\"\n    },\n    \"links\": \"Odkazy\",\n    \"load-more\": \"Načíst více\",\n    \"no-archived-memos\": \"Žádné archivované poznámky.\",\n    \"no-memos\": \"Žádné poznámky.\",\n    \"order-by\": \"Řadit dle\",\n    \"search-placeholder\": \"Hledat poznámky...\",\n    \"show-less\": \"Zobrazit méně\",\n    \"show-more\": \"Zobrazit více\",\n    \"to-do\": \"Úkoly\",\n    \"view-detail\": \"Zobrazit podrobnosti\",\n    \"visibility\": {\n      \"disabled\": \"Veřejné poznámky nejsou povoleny\",\n      \"private\": \"Soukromá\",\n      \"protected\": \"Chráněná\",\n      \"public\": \"Veřejná\"\n    }\n  },\n  \"message\": {\n    \"archived-successfully\": \"Úspěšně archivováno\",\n    \"change-memo-created-time\": \"Změnit čas vytvoření poznámky\",\n    \"copied\": \"Zkopírováno\",\n    \"deleted-successfully\": \"Úspěšně smazáno\",\n    \"description-is-required\": \"Popis je vyžadován\",\n    \"failed-to-embed-memo\": \"Nepodařilo se vložit poznámku\",\n    \"fill-all\": \"Vyplňte prosím všechna pole\",\n    \"fill-all-required-fields\": \"Vyplňte prosím všechny požadované údaje\",\n    \"maximum-upload-size-is\": \"Maximální povolená velikost souboru je {{size}} MiB\",\n    \"memo-not-found\": \"Poznámka nenalezena.\",\n    \"new-password-not-match\": \"Nová hesla nesouhlasí.\",\n    \"no-data\": \"Nebyly nalezeny žádné údaje.\",\n    \"password-changed\": \"Heslo změněno\",\n    \"password-not-match\": \"Hesla nesouhlasí.\",\n    \"restored-successfully\": \"Úspěšně obnoveno\",\n    \"succeed-copy-content\": \"Obsah úspěšně zkopírován.\",\n    \"succeed-copy-link\": \"Odkaz byl úspěšně zkopírován.\",\n    \"update-succeed\": \"Aktualizace proběhla úspěšně\",\n    \"user-not-found\": \"Uživatel nenalezen\"\n  },\n  \"reference\": {\n    \"add-references\": \"Přidat odkazy\",\n    \"embedded-usage\": \"Použít jako vložený obsah\",\n    \"no-memos-found\": \"Žádné poznámky nenalezeny\",\n    \"search-placeholder\": \"Vyhledat obsah\"\n  },\n  \"resource\": {\n    \"clear\": \"Vymazat\",\n    \"copy-link\": \"Kopírovat odkaz\",\n    \"create-dialog\": {\n      \"external-link\": {\n        \"file-name\": \"Název souboru\",\n        \"file-name-placeholder\": \"Název souboru\",\n        \"link\": \"Odkaz\",\n        \"link-placeholder\": \"https://odkaz.na/zdroj\",\n        \"option\": \"Externí odkaz\",\n        \"type\": \"Typ\",\n        \"type-placeholder\": \"Typ souboru\"\n      },\n      \"local-file\": {\n        \"choose\": \"Vyberte soubor…\",\n        \"option\": \"Místní soubor\"\n      },\n      \"title\": \"Vytvořit zdroj\",\n      \"upload-method\": \"Způsob nahrávání\"\n    },\n    \"delete-all-unused\": \"Odstranit všechny nepoužité\",\n    \"delete-all-unused-confirm\": \"Jste si jisti, že chcete odstranit všechny nepoužité zdroje? TATO AKCE JE NEVRATNÁ\",\n    \"delete-all-unused-error\": \"Nepodařilo se odstranit nepoužité zdroje\",\n    \"delete-all-unused-success\": \"Zdroje úspěšně odstraněny\",\n    \"delete-resource\": \"Odstranit zdroj\",\n    \"delete-selected-resources\": \"Odstranit vybraný zdroj\",\n    \"fetching-data\": \"Načítání dat…\",\n    \"file-drag-drop-prompt\": \"Přetáhněte sem svůj soubor a nahrejte jej\",\n    \"linked-amount\": \"Počet odkazů\",\n    \"no-files-selected\": \"Nebyly vybrány žádné soubory\",\n    \"no-resources\": \"Žádné zdroje.\",\n    \"no-unused-resources\": \"Žádné nepoužité zdroje\",\n    \"reset-link\": \"Resetovat odkaz\",\n    \"reset-link-prompt\": \"Jste si jisti, že chcete odkaz resetovat? Tím se přeruší všechna stávající použití odkazu. TATO AKCE JE NEVRATNÁ\",\n    \"reset-resource-link\": \"Resetovat odkaz na zdroj\",\n    \"unused-resources\": \"Nepoužité zdroje\"\n  },\n  \"router\": {\n    \"back-to-top\": \"Zpět na začátek\",\n    \"go-to-home\": \"Přejít na úvod\"\n  },\n  \"setting\": {\n    \"member\": {\n      \"admin\": \"Admin\",\n      \"archive-member\": \"Archivovat uživatele\",\n      \"archive-success\": \"{{username}} úspěšně archivován\",\n      \"archive-warning\": \"Jste si jisti, že chcete archivovat uživatele {{username}}?\",\n      \"archive-warning-description\": \"Archivace deaktivuje účet. Můžete jej později obnovit nebo odstranit.\",\n      \"create-a-member\": \"Založit uživatele\",\n      \"delete-member\": \"Odstranit uživatele\",\n      \"delete-success\": \"{{username}} úspěšně odstraněn\",\n      \"delete-warning\": \"Jste si jisti, že chcete odstranit uživatele {{username}}?\",\n      \"delete-warning-description\": \"TATO AKCE JE NEVRATNÁ\",\n      \"restore-success\": \"{{username}} úspěšně obnoven\",\n      \"user\": \"Uživatel\",\n      \"label\": \"Uživatel\",\n      \"list-title\": \"Seznam uživatelů\"\n    },\n    \"my-account\": {\n      \"label\": \"Můj účet\"\n    },\n    \"preference\": {\n      \"default-memo-sort-option\": \"Čas zobrazení poznámky\",\n      \"default-memo-visibility\": \"Výchozí viditelnost poznámky\",\n      \"theme\": \"Motiv\",\n      \"label\": \"Předvolby\"\n    },\n    \"shortcut\": {\n      \"delete-confirm\": \"Jste si jisti, že chcete odstranit zkratku `{{title}}`?\",\n      \"delete-success\": \"Zkratka `{{title}}` úspěšně odstraněna\"\n    },\n    \"sso\": {\n      \"authorization-endpoint\": \"Autorizační koncový bod\",\n      \"client-id\": \"ID klienta\",\n      \"client-secret\": \"Klientské tajemství\",\n      \"confirm-delete\": \"Jste si jisti, že chcete odstranit SSO konfiguraci \\\"{{name}}\\\"? TATO AKCE JE NEVRATNÁ\",\n      \"create-sso\": \"Vytvořit SSO\",\n      \"custom\": \"Vlastní\",\n      \"delete-sso\": \"Potvrdit odstranění\",\n      \"disabled-password-login-warning\": \"Přihlášení pomocí hesla není povoleno, buďte velmi obezřetní při odstranění poskytovatele identity\",\n      \"display-name\": \"Zobrazovaný název\",\n      \"identifier\": \"Identifikátor\",\n      \"identifier-filter\": \"Filtr identifikátoru\",\n      \"no-sso-found\": \"Žádné SSO nenalezeno.\",\n      \"redirect-url\": \"URL pro přesměrování\",\n      \"scopes\": \"Rozsahy\",\n      \"single-sign-on\": \"Nastavení SSO pro autentizaci\",\n      \"sso-created\": \"SSO {{name}} vytvořeno\",\n      \"sso-list\": \"Seznam SSO\",\n      \"sso-updated\": \"SSO {{name}} aktualizováno\",\n      \"template\": \"Šablona\",\n      \"token-endpoint\": \"Koncový bod tokenu\",\n      \"update-sso\": \"Aktualizovat SSO\",\n      \"user-endpoint\": \"Koncový bod uživatele\",\n      \"label\": \"SSO\"\n    },\n    \"storage\": {\n      \"accesskey\": \"Přístupový klíč\",\n      \"accesskey-placeholder\": \"Přístupový klíč / ID\",\n      \"bucket\": \"Bucket\",\n      \"bucket-placeholder\": \"Název bucketu\",\n      \"create-a-service\": \"Vytvořit službu\",\n      \"create-storage\": \"Vytvořit úložiště\",\n      \"current-storage\": \"Aktuální úložiště objektů\",\n      \"delete-storage\": \"Odstranit úložiště\",\n      \"endpoint\": \"Koncový bod\",\n      \"filepath-template\": \"Šablona cesty k souboru\",\n      \"local-storage-path\": \"Cesta k místnímu úložišti\",\n      \"path\": \"Cesta k úložišti\",\n      \"path-description\": \"Můžete použít stejné dynamické proměnné z místního úložiště, například {filename}\",\n      \"path-placeholder\": \"vlastní/cesta\",\n      \"presign-placeholder\": \"Předběžná adresa URL, nepovinná\",\n      \"region\": \"Region\",\n      \"region-placeholder\": \"Název regionu\",\n      \"s3-compatible-url\": \"URL adresa kompatibilní se S3\",\n      \"secretkey\": \"Tajný klíč\",\n      \"secretkey-placeholder\": \"Tajný klíč / Přístupový klíč\",\n      \"storage-services\": \"Služby úložiště\",\n      \"type-database\": \"Databáze\",\n      \"type-local\": \"Místní souborový systém\",\n      \"update-a-service\": \"Aktualizovat službu\",\n      \"update-local-path\": \"Aktualizovat cestu k místnímu úložišti\",\n      \"update-local-path-description\": \"Cesta k místnímu úložišti je relativní k umístění souboru databáze.\",\n      \"update-storage\": \"Aktualizovat úložiště\",\n      \"url-prefix\": \"Předpona URL\",\n      \"url-prefix-placeholder\": \"Vlastní předpona URL, nepovinná\",\n      \"url-suffix\": \"Přípona URL\",\n      \"url-suffix-placeholder\": \"Vlastní přípona URL, nepovinná\",\n      \"warning-text\": \"Jste si jistí, že chcete odstranit službu úložiště \\\"{{name}}\\\"? TATO AKCE JE NEVRATNÁ\",\n      \"label\": \"Úložiště\"\n    },\n    \"system\": {\n      \"additional-script\": \"Vlastní rozšíření skriptu\",\n      \"additional-script-placeholder\": \"Vlastní rozšíření kódu JavaScript\",\n      \"additional-style\": \"Vlastní rozšíření stylu\",\n      \"additional-style-placeholder\": \"Vlastní rozšíření kódu CSS\",\n      \"allow-user-signup\": \"Povolit registraci uživatele\",\n      \"customize-server\": {\n        \"description\": \"Popis\",\n        \"icon-url\": \"URL ikony\",\n        \"locale\": \"Lokalizace serveru\",\n        \"title\": \"Přizpůsobení serveru\"\n      },\n      \"disable-password-login\": \"Zakázat přihlašování heslem\",\n      \"disable-password-login-final-warning\": \"Pokud víte, co děláte, zadejte \\\"CONFIRM\\\".\",\n      \"disable-password-login-warning\": \"Tímto zakážete přihlašování heslem pro všechny uživatele. Bez obnovení tohoto nastavení v databázi není možné se přihlásit, pokud selžou vámi nakonfigurovaní poskytovatelé identit. Při odebírání poskytovatele identit budete muset být také velmi opatrní.\",\n      \"display-with-updated-time\": \"Zobrazení s časem aktualizace\",\n      \"enable-auto-compact\": \"Povolit automatické kompaktní zobrazení\",\n      \"enable-double-click-to-edit\": \"Povolit dvojklik pro editaci\",\n      \"enable-password-login\": \"Povolit přihlašování heslem\",\n      \"enable-password-login-warning\": \"Tímto se povolí přihlašování heslem pro všechny uživatele. Pokračujte pouze v případě, že chcete, aby se uživatelé mohli přihlašovat pomocí SSO i hesla.\",\n      \"max-upload-size\": \"Maximální velikost nahrávaného souboru (MiB)\",\n      \"max-upload-size-hint\": \"Doporučená hodnota je 32 MiB.\",\n      \"removed-completed-task-list-items\": \"Povolit odstranění dokončených položek ze seznamu úkolů\",\n      \"server-name\": \"Název serveru\",\n      \"title\": \"Obecné\",\n      \"label\": \"Systém\"\n    },\n    \"version\": \"Verze\",\n    \"access-token\": {\n      \"access-token-copied-to-clipboard\": \"Přístupový token zkopírován do schránky\",\n      \"access-token-deleted\": \"Přístupový token `{{description}}` odstraněn\",\n      \"access-token-deletion\": \"Jste si jistí, že chcete odstranit přístupový token `{{description}}`?\",\n      \"access-token-deletion-description\": \"Tato akce je nevratná. Budete muset aktualizovat všechny služby používající tento token, aby používaly nový token.\",\n      \"create-dialog\": {\n        \"access-token-created\": \"Přístupový token `{{description}}` vytvořen\",\n        \"create-access-token\": \"Vytvořit přístupový token\",\n        \"created-at\": \"Vytvořeno\",\n        \"description\": \"Popis\",\n        \"duration-1m\": \"1 měsíc\",\n        \"duration-8h\": \"8 hodin\",\n        \"duration-never\": \"Nikdy\",\n        \"expiration\": \"Platnost\",\n        \"expires-at\": \"Vyprší\",\n        \"some-description\": \"Nějaký popis...\"\n      },\n      \"description\": \"Seznam všech přístupových tokenů k vašemu účtu.\",\n      \"title\": \"Přístupové tokeny\",\n      \"token\": \"Token\"\n    },\n    \"account\": {\n      \"change-password\": \"Změnit heslo\",\n      \"email-note\": \"Volitelné\",\n      \"export-memos\": \"Exportovat poznámky\",\n      \"nickname-note\": \"Zobrazeno v banneru\",\n      \"openapi-reset\": \"Resetovat OpenAPI klíč\",\n      \"openapi-sample-post\": \"Ahoj #memos z {{url}}\",\n      \"openapi-title\": \"OpenAPI\",\n      \"reset-api\": \"Resetovat API\",\n      \"title\": \"Informace o účtu\",\n      \"update-information\": \"Aktualizovat informace\",\n      \"username-note\": \"Slouží k přihlášení\"\n    },\n    \"instance\": {\n      \"disallow-change-nickname\": \"Zakázat změnu přezdívky\",\n      \"disallow-change-username\": \"Zakázat změnu uživatelského jména\",\n      \"disallow-password-auth\": \"Zakázat ověřování heslem\",\n      \"disallow-user-registration\": \"Zakázat registraci uživatelů\",\n      \"monday\": \"Pondělí\",\n      \"saturday\": \"Sobota\",\n      \"sunday\": \"Neděle\",\n      \"week-start-day\": \"Začátek týdne\"\n    },\n    \"memo\": {\n      \"content-length-limit\": \"Omezení velikosti obsahu (bajty)\",\n      \"enable-blur-nsfw-content\": \"Povolit rozostření citlivého obsahu\",\n      \"enable-memo-comments\": \"Povolit komentáře k poznámkám\",\n      \"enable-memo-location\": \"Povolit umístění poznámek\",\n      \"reactions\": \"Reakce\",\n      \"title\": \"Nastavení související s poznámkami\",\n      \"label\": \"Poznámky\"\n    },\n    \"webhook\": {\n      \"create-dialog\": {\n        \"an-easy-to-remember-name\": \"Snadno zapamatovatelný název\",\n        \"create-webhook\": \"Vytvořit webhook\",\n        \"create-webhook-success\": \"Webhook `{{name}}` vytvořen\",\n        \"edit-webhook\": \"Upravit webhook\",\n        \"payload-url\": \"Adresa Payload URL\",\n        \"title\": \"Název\",\n        \"url-example-post-receive\": \"https://example.com/postreceive\"\n      },\n      \"delete-dialog\": {\n        \"delete-webhook-description\": \"Tato akce je nevratná.\",\n        \"delete-webhook-success\": \"Webhook `{{name}}` úspěšně odstraněn\",\n        \"delete-webhook-title\": \"Jste si jisti, že chcete odstranit webhook `{{name}}`?\"\n      },\n      \"no-webhooks-found\": \"Žádné webhooky nenalezeny.\",\n      \"title\": \"Webhooky\",\n      \"url\": \"URL\"\n    }\n  },\n  \"tag\": {\n    \"all-tags\": \"Všechny štítky\",\n    \"create-tag\": \"Vytvořit štítek\",\n    \"create-tags-guide\": \"Štítky můžete vytvořit zadáním `#tag`.\",\n    \"delete-confirm\": \"Jste si jisti, že chcete tento štítek odstranit? Všechny související poznámky budou archivovány.\",\n    \"delete-success\": \"Štítek úspěšně odstraněn\",\n    \"delete-tag\": \"Odstranit štítek\",\n    \"new-name\": \"Nový název\",\n    \"no-tag-found\": \"Žádný štítek nenalezen\",\n    \"old-name\": \"Původní název\",\n    \"rename-error-empty\": \"Název štítku nemůže být prázdný nebo obsahovat mezery\",\n    \"rename-error-repeat\": \"Nový název se nemůže shodovat s původním názvem\",\n    \"rename-success\": \"Štítek úspěšně přejmenován\",\n    \"rename-tag\": \"Přejmenovat štítek\",\n    \"rename-tip\": \"Všechny vaše poznámky s tímto štítkem budou aktualizovány.\"\n  },\n  \"tooltip\": {\n    \"link-memo\": \"Propojit poznámku\",\n    \"markdown-menu\": \"Markdown\",\n    \"select-location\": \"Umístění\",\n    \"select-visibility\": \"Viditelnost\",\n    \"tags\": \"Štítky\",\n    \"upload-attachment\": \"Nahrát přílohy\"\n  }\n}\n"
  },
  {
    "path": "web/src/locales/de.json",
    "content": "{\n  \"about\": {\n    \"blogs\": \"Blogs\",\n    \"description\": \"Ein datenschutzorientierter, leichtgewichtiger Notizdienst. Erfasse und teile deine großartigen Gedanken ganz einfach.\",\n    \"documents\": \"Dokumente\",\n    \"github-repository\": \"GitHub Repo\",\n    \"official-website\": \"Offizielle Webseite\"\n  },\n  \"auth\": {\n    \"create-your-account\": \"Benutzerkonto anlegen\",\n    \"host-tip\": \"Du registrierst dich als Host dieser Seite.\",\n    \"new-password\": \"Neues Passwort\",\n    \"repeat-new-password\": \"Neues Passwort wiederholen\",\n    \"sign-in-tip\": \"Bereits ein Benutzerkonto?\",\n    \"sign-up-tip\": \"Noch kein Benutzerkonto?\"\n  },\n  \"common\": {\n    \"about\": \"Über\",\n    \"add\": \"Hinzufügen\",\n    \"admin\": \"Administration\",\n    \"all\": \"Alle\",\n    \"archive\": \"Archivieren\",\n    \"archived\": \"Archiviert\",\n    \"attachments\": \"Anhänge\",\n    \"auto-expand\": \"Automatisch erweitern\",\n    \"avatar\": \"Avatar\",\n    \"basic\": \"Allgemeines\",\n    \"beta\": \"Beta\",\n    \"calendar\": \"Kalender\",\n    \"cancel\": \"Abbrechen\",\n    \"change\": \"Ändern\",\n    \"clear\": \"Leeren\",\n    \"close\": \"Schließen\",\n    \"collapse\": \"Einklappen\",\n    \"confirm\": \"Bestätigen\",\n    \"copy\": \"Kopieren\",\n    \"create\": \"Erstellen\",\n    \"created-at\": \"Erstellt am\",\n    \"database\": \"Datenbank\",\n    \"day\": \"Tag\",\n    \"days\": {\n      \"fri\": \"Fr\",\n      \"mon\": \"Mo\",\n      \"sat\": \"Sa\",\n      \"sun\": \"So\",\n      \"thu\": \"Do\",\n      \"tue\": \"Di\",\n      \"wed\": \"Mi\"\n    },\n    \"delete\": \"Löschen\",\n    \"description\": \"Beschreibung\",\n    \"edit\": \"Bearbeiten\",\n    \"email\": \"E-Mail\",\n    \"expand\": \"Erweitern\",\n    \"explore\": \"Erkunden\",\n    \"file\": \"Datei\",\n    \"filter\": \"Filter\",\n    \"home\": \"Startseite\",\n    \"image\": \"Bild\",\n    \"in\": \"In\",\n    \"inbox\": \"Eingang\",\n    \"input\": \"Eingabe\",\n    \"language\": \"Sprache\",\n    \"last-updated-at\": \"Zuletzt aktualisiert am\",\n    \"learn-more\": \"Mehr erfahren\",\n    \"link\": \"Link\",\n    \"map\": \"Karte\",\n    \"mark\": \"Markieren\",\n    \"memo\": \"Notiz\",\n    \"memos\": \"Notizen\",\n    \"more\": \"Mehr\",\n    \"name\": \"Name\",\n    \"new\": \"Neu\",\n    \"nickname\": \"Spitzname\",\n    \"null\": \"Null\",\n    \"or\": \"oder\",\n    \"password\": \"Passwort\",\n    \"pin\": \"Anpinnen\",\n    \"pinned\": \"Angeheftet\",\n    \"preview\": \"Vorschau\",\n    \"profile\": \"Profil\",\n    \"properties\": \"Eigenschaften\",\n    \"referenced-by\": \"Verknüpft mit\",\n    \"referencing\": \"Verknüpft\",\n    \"relations\": \"Beziehungen\",\n    \"remember-me\": \"Erinnere mich\",\n    \"rename\": \"Umbenennen\",\n    \"reset\": \"Zurücksetzen\",\n    \"resources\": \"Ressourcen\",\n    \"restore\": \"Wiederherstellen\",\n    \"role\": \"Rolle\",\n    \"save\": \"Speichern\",\n    \"search\": \"Suche\",\n    \"select\": \"Auswählen\",\n    \"settings\": \"Einstellungen\",\n    \"share\": \"Teilen\",\n    \"shortcut-filter\": \"Verknüpfungsfilter\",\n    \"shortcuts\": \"Verknüpfungen\",\n    \"sign-in\": \"Anmelden\",\n    \"sign-in-with\": \"Anmelden mit {{provider}}\",\n    \"sign-out\": \"Abmelden\",\n    \"sign-up\": \"Registrieren\",\n    \"statistics\": \"Statistiken\",\n    \"tags\": \"Tags\",\n    \"title\": \"Titel\",\n    \"today\": \"Heute\",\n    \"tree-mode\": \"Baumansicht\",\n    \"type\": \"Typ\",\n    \"unpin\": \"Pin lösen\",\n    \"update\": \"Update\",\n    \"upload\": \"Hochladen\",\n    \"user\": \"Benutzer\",\n    \"username\": \"Benutzername\",\n    \"version\": \"Version\",\n    \"visibility\": \"Sichtbarkeit\",\n    \"yourself\": \"Du selbst\"\n  },\n  \"editor\": {\n    \"add-your-comment-here\": \"Füge deinen Kommentar hinzu...\",\n    \"any-thoughts\": \"Ein Gedanke...\",\n    \"exit-focus-mode\": \"Fokus Modus verlassen\",\n    \"focus-mode\": \"Fokus Modus\",\n    \"no-changes-detected\": \"Keine Änderungen erkannt\",\n    \"save\": \"Speichern\",\n    \"saving\": \"Speichern...\",\n    \"slash-commands\": \"Nutze `/` für Befehle\"\n  },\n  \"inbox\": {\n    \"failed-to-load\": \"Fehler beim Laden des Eintrags\",\n    \"memo-comment\": \"{{user}} hat einen Kommentar zu {{memo}} hinterlassen.\",\n    \"no-archived\": \"Keine archivierten Benachrichtigungen\",\n    \"no-unread\": \"Keine ungelesenen Benachrichtigungen\",\n    \"unread\": \"Ungelesen\"\n  },\n  \"markdown\": {\n    \"checkbox\": \"Checkbox\",\n    \"code-block\": \"Quellcode-Block\",\n    \"content-syntax\": \"Inhaltssyntax\"\n  },\n  \"memo\": {\n    \"archived-at\": \"Archiviert am\",\n    \"click-to-hide-nsfw-content\": \"Klicken, um sensible Inhalte auszublenden\",\n    \"click-to-show-nsfw-content\": \"Klicken, um sensible Inhalte anzuzeigen\",\n    \"code\": \"Code\",\n    \"comment\": {\n      \"self\": \"Kommentare\",\n      \"write-a-comment\": \"Schreibe einen Kommentar\"\n    },\n    \"copy-content\": \"Inhalt kopieren\",\n    \"copy-link\": \"Link kopieren\",\n    \"count-memos-in-date\": \"{{count}} {{memos}} in {{date}}\",\n    \"delete-confirm\": \"Bist du sicher, dass du diese Notiz löschen möchtest?\\n\\nDIESE AKTION KANN NICHT RÜCKGÄNGIG GEMACHT WERDEN\",\n    \"delete-confirm-description\": \"Dieser Vorgang ist nicht rückgängig zu machen. Anhänge, Links und Verknüpfungen werden ebenfalls gelöscht.\",\n    \"direction\": \"Richtung\",\n    \"direction-asc\": \"Aufsteigend\",\n    \"direction-desc\": \"Absteigend\",\n    \"display-time\": \"Anzeigedatum\",\n    \"filters\": {\n      \"has-code\": \"hatCode\",\n      \"has-link\": \"hatLink\",\n      \"has-task-list\": \"hatAufgabenliste\"\n    },\n    \"links\": \"Links\",\n    \"load-more\": \"Mehr laden\",\n    \"no-archived-memos\": \"Keine archivierten Notizen.\",\n    \"no-memos\": \"Keine Notizen.\",\n    \"order-by\": \"Sortieren nach\",\n    \"search-placeholder\": \"Notizen durchsuchen...\",\n    \"show-less\": \"Weniger anzeigen\",\n    \"show-more\": \"Mehr anzeigen\",\n    \"to-do\": \"Aufgaben\",\n    \"view-detail\": \"Details anzeigen\",\n    \"visibility\": {\n      \"disabled\": \"Öffentliche Notizen sind deaktiviert\",\n      \"private\": \"Privat\",\n      \"protected\": \"Geschützt\",\n      \"public\": \"Öffentlich\"\n    }\n  },\n  \"message\": {\n    \"archived-successfully\": \"Erfolgreich archiviert\",\n    \"change-memo-created-time\": \"Erstellungszeitpunkt ändern\",\n    \"copied\": \"Kopiert\",\n    \"deleted-successfully\": \"Erfolgreich gelöscht\",\n    \"description-is-required\": \"Beschreibung ist erforderlich\",\n    \"failed-to-embed-memo\": \"Notiz konnte nicht eingebettet werden\",\n    \"fill-all\": \"Bitte alle Felder ausfüllen.\",\n    \"fill-all-required-fields\": \"Bitte alle Pflichtfelder ausfüllen\",\n    \"maximum-upload-size-is\": \"Die maximal zulässige Upload-Größe beträgt {{size}} MiB\",\n    \"memo-not-found\": \"Notiz nicht gefunden.\",\n    \"new-password-not-match\": \"Neue Passwörter stimmen nicht überein.\",\n    \"no-data\": \"Keine Daten gefunden.\",\n    \"password-changed\": \"Passwort geändert\",\n    \"password-not-match\": \"Passwörter stimmen nicht überein.\",\n    \"restored-successfully\": \"Erfolgreich wiederhergestellt\",\n    \"succeed-copy-content\": \"Inhalt erfolgreich kopiert.\",\n    \"succeed-copy-link\": \"Link erfolgreich kopiert\",\n    \"update-succeed\": \"Update erfolgreich\",\n    \"user-not-found\": \"Benutzer nicht gefunden\"\n  },\n  \"reference\": {\n    \"add-references\": \"Referenzen hinzufügen\",\n    \"embedded-usage\": \"Als eingebetteten Inhalt verwenden\",\n    \"no-memos-found\": \"Keine Notizen gefunden\",\n    \"search-placeholder\": \"Suche Notizen\"\n  },\n  \"resource\": {\n    \"clear\": \"Löschen\",\n    \"copy-link\": \"Link kopieren\",\n    \"create-dialog\": {\n      \"external-link\": {\n        \"file-name\": \"Dateiname\",\n        \"file-name-placeholder\": \"Dateiname\",\n        \"link\": \"Link\",\n        \"link-placeholder\": \"https://der.link.zu/deiner/ressource\",\n        \"option\": \"Externer Link\",\n        \"type\": \"Typ\",\n        \"type-placeholder\": \"Dateityp\"\n      },\n      \"local-file\": {\n        \"choose\": \"Datei auswählen…\",\n        \"option\": \"Lokale Datei\"\n      },\n      \"title\": \"Ressource erstellen\",\n      \"upload-method\": \"Upload-Methode\"\n    },\n    \"delete-all-unused\": \"Alle ungenutzten löschen\",\n    \"delete-all-unused-confirm\": \"Bist du sicher, dass du alle ungenutzten Elemente löschen willst? DIESER VORGANG KANN NICHT RÜCKGÄNGIG GEMACHT WERDEN\",\n    \"delete-all-unused-error\": \"Das Löschen ungenutzter Elemente ist fehlgeschlagen\",\n    \"delete-all-unused-success\": \"Elemente erfolgreich gelöscht\",\n    \"delete-resource\": \"Ressource löschen\",\n    \"delete-selected-resources\": \"Ausgewählte Ressourcen löschen\",\n    \"fetching-data\": \"Lade Daten…\",\n    \"file-drag-drop-prompt\": \"Datei hierher ziehen und ablegen, um sie hochzuladen\",\n    \"linked-amount\": \"Anzahl verlinkter Memos\",\n    \"no-files-selected\": \"Keine Dateien ausgewählt\",\n    \"no-resources\": \"Keine Ressourcen.\",\n    \"no-unused-resources\": \"Keine ungenutzten Ressourcen\",\n    \"reset-link\": \"Link zurücksetzen\",\n    \"reset-link-prompt\": \"Bist du sicher, dass du diesen Link zurücksetzen möchtest?\\nDadurch werden alle aktuellen Linkverwendungen ungültig.\\n\\nDIESE AKTION KANN NICHT RÜCKGÄNGIG GEMACHT WERDEN\",\n    \"reset-resource-link\": \"Ressourcen-Link zurücksetzen\",\n    \"unused-resources\": \"Ungenutzte Ressourcen\"\n  },\n  \"router\": {\n    \"back-to-top\": \"Zurück nach oben\",\n    \"go-to-home\": \"Zurück zur Startseite\"\n  },\n  \"setting\": {\n    \"member\": {\n      \"admin\": \"Admin\",\n      \"archive-member\": \"Mitglied archivieren\",\n      \"archive-success\": \"{{username}} erfolgreich archiviert\",\n      \"archive-warning\": \"Bist du sicher, dass du {{username}} archivieren möchtest?\",\n      \"archive-warning-description\": \"Die Archivierung deaktiviert den Account. Du kannst ihn später Wiederherstellen oder löschen.\",\n      \"create-a-member\": \"Mitglied hinzufügen\",\n      \"delete-member\": \"Mitglied löschen\",\n      \"delete-success\": \"{{username}} erfolgreich gelöscht\",\n      \"delete-warning\": \"Bist du sicher, dass du {{username}} löschen möchtest?\\n\\nDIESE AKTION KANN NICHT RÜCKGÄNGIG GEMACHT WERDEN\",\n      \"delete-warning-description\": \"DIESE AKTION KANN NICHT RÜCKGÄNGIG GEMACHT WERDEN\",\n      \"restore-success\": \"{{username}} erfolgreich wiederhergestellt\",\n      \"user\": \"Benutzer\",\n      \"label\": \"Mitglied\",\n      \"list-title\": \"Mitgliederliste\"\n    },\n    \"my-account\": {\n      \"label\": \"Mein Konto\"\n    },\n    \"preference\": {\n      \"default-memo-sort-option\": \"Anzeigedatum der Notiz\",\n      \"default-memo-visibility\": \"Standard-Notizsichtbarkeit\",\n      \"theme\": \"Design\",\n      \"label\": \"Einstellungen\"\n    },\n    \"shortcut\": {\n      \"delete-confirm\": \"Bist du sicher, dass du die Verknüpfung `{{title}}` löschen möchtest?\",\n      \"delete-success\": \"Verknüpfung `{{title}}` erfolgreich gelöscht\"\n    },\n    \"sso\": {\n      \"authorization-endpoint\": \"Autorisierungsendpunkt\",\n      \"client-id\": \"Client ID\",\n      \"client-secret\": \"Client Secret\",\n      \"confirm-delete\": \"Bist du sicher, dass du die SSO-Konfiguration von \\\"{{name}}\\\" löschen möchtest? DIESE AKTION KANN NICHT RÜCKGÄNGIG GEMACHT WERDEN\",\n      \"create-sso\": \"SSO erstellen\",\n      \"custom\": \"Benutzerdefiniert\",\n      \"delete-sso\": \"Löschen bestätigen\",\n      \"disabled-password-login-warning\": \"Passwort-Login ist deaktiviert, sei besonders vorsichtig beim Entfernen von Identitätsanbietern\",\n      \"display-name\": \"Anzeigename\",\n      \"identifier\": \"Kennung\",\n      \"identifier-filter\": \"Kennungsfilter\",\n      \"no-sso-found\": \"Kein SSO gefunden.\",\n      \"redirect-url\": \"Weiterleitungs-URL\",\n      \"scopes\": \"Berechtigungen\",\n      \"single-sign-on\": \"Single Sign-On (SSO) für Authentifizierung konfigurieren\",\n      \"sso-created\": \"SSO {{name}} erstellt\",\n      \"sso-list\": \"SSO-Liste\",\n      \"sso-updated\": \"SSO {{name}} aktualisiert\",\n      \"template\": \"Vorlage\",\n      \"token-endpoint\": \"Token-Endpunkt\",\n      \"update-sso\": \"SSO aktualisieren\",\n      \"user-endpoint\": \"Benutzer-Endpunkt\",\n      \"label\": \"SSO\"\n    },\n    \"storage\": {\n      \"accesskey\": \"Zugangsschlüssel\",\n      \"accesskey-placeholder\": \"Zugangsschlüssel / Zugangs-ID\",\n      \"bucket\": \"Bucket\",\n      \"bucket-placeholder\": \"Bucket-Name\",\n      \"create-a-service\": \"Dienst erstellen\",\n      \"create-storage\": \"Speicher erstellen\",\n      \"current-storage\": \"Aktueller Objektspeicher\",\n      \"delete-storage\": \"Speicher löschen\",\n      \"endpoint\": \"Endpunkt\",\n      \"filepath-template\": \"Dateipfad-Vorlage\",\n      \"local-storage-path\": \"Lokaler Speicherpfad\",\n      \"path\": \"Speicherpfad\",\n      \"path-description\": \"Du kannst dieselben dynamischen Variablen wie im lokalen Speicher verwenden, z.B. {filename}\",\n      \"path-placeholder\": \"benutzerdefinierter/pfad\",\n      \"presign-placeholder\": \"Vorab signierte URL, optional\",\n      \"region\": \"Region\",\n      \"region-placeholder\": \"Regionsname\",\n      \"s3-compatible-url\": \"S3-kompatible URL\",\n      \"secretkey\": \"Geheimer Schlüssel\",\n      \"secretkey-placeholder\": \"Geheimer Schlüssel / Zugangsschlüssel\",\n      \"storage-services\": \"Speicherdienste\",\n      \"type-database\": \"Datenbank\",\n      \"type-local\": \"Lokales Dateisystem\",\n      \"update-a-service\": \"Dienst aktualisieren\",\n      \"update-local-path\": \"Lokalen Speicherpfad aktualisieren\",\n      \"update-local-path-description\": \"Der lokale Speicherpfad ist ein relativer Pfad zu deiner Datenbankdatei\",\n      \"update-storage\": \"Speicher aktualisieren\",\n      \"url-prefix\": \"URL-Präfix\",\n      \"url-prefix-placeholder\": \"Benutzerdefiniertes URL-Präfix, optional\",\n      \"url-suffix\": \"URL-Suffix\",\n      \"url-suffix-placeholder\": \"Benutzerdefiniertes URL-Suffix, optional\",\n      \"warning-text\": \"Bist du sicher, dass du den Speicherdienst \\\"{{name}}\\\" löschen möchtest? DIESE AKTION KANN NICHT RÜCKGÄNGIG GEMACHT WERDEN\",\n      \"label\": \"Speicher\"\n    },\n    \"system\": {\n      \"additional-script\": \"Zusätzliches Skript\",\n      \"additional-script-placeholder\": \"Zusätzlicher JavaScript-Code\",\n      \"additional-style\": \"Zusätzlicher Stil\",\n      \"additional-style-placeholder\": \"Zusätzlicher CSS-Code\",\n      \"allow-user-signup\": \"Benutzerregistrierung erlauben\",\n      \"customize-server\": {\n        \"description\": \"Beschreibung\",\n        \"icon-url\": \"Icon-URL\",\n        \"locale\": \"Server-Sprache\",\n        \"title\": \"Server anpassen\"\n      },\n      \"disable-password-login\": \"Passwort-Login deaktivieren\",\n      \"disable-password-login-final-warning\": \"Bitte tippe \\\"CONFIRM\\\", wenn du weißt, was du tust.\",\n      \"disable-password-login-warning\": \"Dadurch wird der Passwort-Login für alle Benutzer deaktiviert. Es ist nicht möglich, sich anzumelden, ohne diese Einstellung in der Datenbank rückgängig zu machen, falls deine konfigurierten Identitätsanbieter ausfallen. Sei besonders vorsichtig beim Entfernen eines Identitätsanbieters.\",\n      \"display-with-updated-time\": \"Mit aktualisierter Zeit anzeigen\",\n      \"enable-auto-compact\": \"Automatische Komprimierung aktivieren\",\n      \"enable-double-click-to-edit\": \"Doppelklick zum Bearbeiten aktivieren\",\n      \"enable-password-login\": \"Passwort-Login aktivieren\",\n      \"enable-password-login-warning\": \"Dadurch wird der Passwort-Login für alle Benutzer aktiviert. Fahre nur fort, wenn du möchtest, dass sich Benutzer sowohl mit SSO als auch mit Passwort anmelden können.\",\n      \"max-upload-size\": \"Maximale Uploadgröße (MiB)\",\n      \"max-upload-size-hint\": \"Empfohlener Wert ist 32 MiB.\",\n      \"removed-completed-task-list-items\": \"Entfernen abgeschlossener Aufgaben aktivieren\",\n      \"server-name\": \"Servername\",\n      \"title\": \"Allgemein\",\n      \"label\": \"System\"\n    },\n    \"version\": \"Version\",\n    \"access-token\": {\n      \"access-token-copied-to-clipboard\": \"Zugangstoken in die Zwischenablage kopiert\",\n      \"access-token-deleted\": \"Zugangstoken `{{description}}` gelöscht\",\n      \"access-token-deletion\": \"Bist du sicher, dass du das Zugangstoken `{{description}}` löschen möchtest? DIESE AKTION KANN NICHT RÜCKGÄNGIG GEMACHT WERDEN.\",\n      \"access-token-deletion-description\": \"Diese Aktion kann nicht rückgängig gemacht werden. Du musst alle Dienste, die diesen Token verwenden, mit einem neuen Token nutzen.\",\n      \"create-dialog\": {\n        \"access-token-created\": \"Zugangstoken `{{description}}` erstellt\",\n        \"create-access-token\": \"Zugangstoken erstellen\",\n        \"created-at\": \"Erstellt am\",\n        \"description\": \"Beschreibung\",\n        \"duration-1m\": \"1 Monat\",\n        \"duration-8h\": \"8 Stunden\",\n        \"duration-never\": \"Nie\",\n        \"expiration\": \"Ablauf\",\n        \"expires-at\": \"Läuft ab am\",\n        \"some-description\": \"Eine Beschreibung...\"\n      },\n      \"description\": \"Liste aller Zugangstoken für deinen Benutzer.\",\n      \"title\": \"Zugangstoken\",\n      \"token\": \"Token\"\n    },\n    \"account\": {\n      \"change-password\": \"Passwort ändern\",\n      \"email-note\": \"Optional\",\n      \"export-memos\": \"Notizen exportieren\",\n      \"nickname-note\": \"Wird im Banner angezeigt\",\n      \"openapi-reset\": \"OpenAPI Key zurücksetzen\",\n      \"openapi-sample-post\": \"Hallo #memos von {{url}}\",\n      \"openapi-title\": \"OpenAPI\",\n      \"reset-api\": \"API zurücksetzen\",\n      \"title\": \"Konto-Informationen\",\n      \"update-information\": \"Informationen ändern\",\n      \"username-note\": \"Zum Anmelden verwendet\"\n    },\n    \"instance\": {\n      \"disallow-change-nickname\": \"Ändern des Spitznamens verbieten\",\n      \"disallow-change-username\": \"Ändern des Benutzernamens verbieten\",\n      \"disallow-password-auth\": \"Passwort-Authentifizierung verbieten\",\n      \"disallow-user-registration\": \"Benutzerregistrierung verbieten\",\n      \"monday\": \"Montag\",\n      \"saturday\": \"Samstag\",\n      \"sunday\": \"Sonntag\",\n      \"week-start-day\": \"Wochenstarttag\"\n    },\n    \"memo\": {\n      \"content-length-limit\": \"Limitierung der Inhaltslänge (Byte)\",\n      \"enable-blur-nsfw-content\": \"Unschärfe für sensible Inhalte (NSFW) aktivieren\",\n      \"enable-memo-comments\": \"Kommentare für Notizen aktivieren\",\n      \"enable-memo-location\": \"Notiz-Standort aktivieren\",\n      \"reactions\": \"Reaktionen\",\n      \"title\": \"Notiz-Einstellungen\",\n      \"label\": \"Notiz\"\n    },\n    \"webhook\": {\n      \"create-dialog\": {\n        \"an-easy-to-remember-name\": \"Einprägsamer Name\",\n        \"create-webhook\": \"Webhook erstellen\",\n        \"create-webhook-success\": \"Webhook `{{name}}` erstellt\",\n        \"edit-webhook\": \"Webhook bearbeiten\",\n        \"payload-url\": \"Payload-URL\",\n        \"title\": \"Titel\",\n        \"url-example-post-receive\": \"https://example.com/postreceive\"\n      },\n      \"delete-dialog\": {\n        \"delete-webhook-description\": \"Diese Aktion kann nicht rückgängig gemacht werden\",\n        \"delete-webhook-success\": \"Webhook `{{name}}` erfolgreich gelöscht\",\n        \"delete-webhook-title\": \"Bist du sicher, dass du den Webhook `{{name}}` löschen möchtest?\"\n      },\n      \"no-webhooks-found\": \"Keine Webhooks gefunden.\",\n      \"title\": \"Webhooks\",\n      \"url\": \"URL\"\n    }\n  },\n  \"tag\": {\n    \"all-tags\": \"Alle Tags\",\n    \"create-tag\": \"Tag erstellen\",\n    \"create-tags-guide\": \"Du kannst Tags erstellen, indem du `#tag` eingibst.\",\n    \"delete-confirm\": \"Bist du sicher, dass du diesen Tag löschen möchtest? Alle zugehörigen Notizen werden archiviert.\",\n    \"delete-success\": \"Tag erfolgreich gelöscht\",\n    \"delete-tag\": \"Tag löschen\",\n    \"new-name\": \"Neuer Name\",\n    \"no-tag-found\": \"Kein Tag gefunden\",\n    \"old-name\": \"Alter Name\",\n    \"rename-error-empty\": \"Tag-Name darf nicht leer sein oder Leerzeichen enthalten\",\n    \"rename-error-repeat\": \"Neuer Name darf nicht mit dem alten Namen übereinstimmen\",\n    \"rename-success\": \"Tag erfolgreich umbenannt\",\n    \"rename-tag\": \"Tag umbenennen\",\n    \"rename-tip\": \"Alle deine Notizen mit diesem Tag werden aktualisiert.\"\n  },\n  \"tooltip\": {\n    \"link-memo\": \"Link Memo\",\n    \"markdown-menu\": \"Markdown\",\n    \"select-location\": \"Ort\",\n    \"select-visibility\": \"Sichtbarkeit\",\n    \"tags\": \"Tags\",\n    \"upload-attachment\": \"Anhang hochladen\"\n  }\n}\n"
  },
  {
    "path": "web/src/locales/en-GB.json",
    "content": "{\n  \"setting\": {\n    \"sso\": {\n      \"authorization-endpoint\": \"Authorisation endpoint\"\n    },\n    \"system\": {\n      \"customize-server\": {\n        \"title\": \"Customise Server\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "web/src/locales/en.json",
    "content": "{\n  \"about\": {\n    \"blogs\": \"Blogs\",\n    \"description\": \"A privacy-first, lightweight note-taking service. Easily capture and share your great thoughts.\",\n    \"documents\": \"Documents\",\n    \"github-repository\": \"GitHub Repo\",\n    \"official-website\": \"Official Website\"\n  },\n  \"auth\": {\n    \"create-your-account\": \"Create your account\",\n    \"host-tip\": \"You are registering as the Site Host.\",\n    \"new-password\": \"New password\",\n    \"repeat-new-password\": \"Repeat the new password\",\n    \"sign-in-tip\": \"Already have an account?\",\n    \"sign-up-tip\": \"Don't have an account yet?\"\n  },\n  \"common\": {\n    \"about\": \"About\",\n    \"add\": \"Add\",\n    \"admin\": \"Admin\",\n    \"all\": \"All\",\n    \"archive\": \"Archive\",\n    \"archived\": \"Archived\",\n    \"attachments\": \"Attachments\",\n    \"auto-expand\": \"Auto expand\",\n    \"avatar\": \"Avatar\",\n    \"basic\": \"Basic\",\n    \"beta\": \"Beta\",\n    \"calendar\": \"Calendar\",\n    \"cancel\": \"Cancel\",\n    \"change\": \"Change\",\n    \"clear\": \"Clear\",\n    \"close\": \"Close\",\n    \"collapse\": \"Collapse\",\n    \"confirm\": \"Confirm\",\n    \"copy\": \"Copy\",\n    \"create\": \"Create\",\n    \"created-at\": \"Created at\",\n    \"database\": \"Database\",\n    \"day\": \"Day\",\n    \"days\": {\n      \"fri\": \"Fri\",\n      \"mon\": \"Mon\",\n      \"sat\": \"Sat\",\n      \"sun\": \"Sun\",\n      \"thu\": \"Thu\",\n      \"tue\": \"Tue\",\n      \"wed\": \"Wed\"\n    },\n    \"delete\": \"Delete\",\n    \"description\": \"Description\",\n    \"edit\": \"Edit\",\n    \"email\": \"Email\",\n    \"expand\": \"Expand\",\n    \"explore\": \"Explore\",\n    \"file\": \"File\",\n    \"filter\": \"Filter\",\n    \"home\": \"Home\",\n    \"image\": \"Image\",\n    \"in\": \"In\",\n    \"inbox\": \"Inbox\",\n    \"input\": \"Input\",\n    \"language\": \"Language\",\n    \"last-updated-at\": \"Last updated at\",\n    \"learn-more\": \"Learn more\",\n    \"link\": \"Link\",\n    \"map\": \"Map\",\n    \"mark\": \"Mark\",\n    \"memo\": \"Memo\",\n    \"memos\": \"Memos\",\n    \"more\": \"More\",\n    \"name\": \"Name\",\n    \"new\": \"New\",\n    \"nickname\": \"Nickname\",\n    \"null\": \"Null\",\n    \"or\": \"or\",\n    \"password\": \"Password\",\n    \"pin\": \"Pin\",\n    \"pinned\": \"Pinned\",\n    \"preview\": \"Preview\",\n    \"profile\": \"Profile\",\n    \"properties\": \"Properties\",\n    \"referenced-by\": \"Referenced by\",\n    \"referencing\": \"Referencing\",\n    \"relations\": \"Relations\",\n    \"remember-me\": \"Remember me\",\n    \"rename\": \"Rename\",\n    \"reset\": \"Reset\",\n    \"resources\": \"Resources\",\n    \"restore\": \"Restore\",\n    \"role\": \"Role\",\n    \"save\": \"Save\",\n    \"search\": \"Search\",\n    \"select\": \"Select\",\n    \"settings\": \"Settings\",\n    \"share\": \"Share\",\n    \"shortcut-filter\": \"Shortcut filter\",\n    \"shortcuts\": \"Shortcuts\",\n    \"sign-in\": \"Sign in\",\n    \"sign-in-with\": \"Sign in with {{provider}}\",\n    \"sign-out\": \"Sign out\",\n    \"sign-up\": \"Sign up\",\n    \"statistics\": \"Statistics\",\n    \"tags\": \"Tags\",\n    \"title\": \"Title\",\n    \"today\": \"Today\",\n    \"tree-mode\": \"Tree mode\",\n    \"type\": \"Type\",\n    \"unpin\": \"Unpin\",\n    \"update\": \"Update\",\n    \"upload\": \"Upload\",\n    \"user\": \"User\",\n    \"username\": \"Username\",\n    \"version\": \"Version\",\n    \"visibility\": \"Visibility\",\n    \"yourself\": \"Yourself\"\n  },\n  \"editor\": {\n    \"add-your-comment-here\": \"Add your comment here...\",\n    \"any-thoughts\": \"Any thoughts...\",\n    \"exit-focus-mode\": \"Exit Focus Mode\",\n    \"focus-mode\": \"Focus Mode\",\n    \"no-changes-detected\": \"No changes detected\",\n    \"save\": \"Save\",\n    \"saving\": \"Saving...\",\n    \"slash-commands\": \"Type `/` for commands\"\n  },\n  \"inbox\": {\n    \"failed-to-load\": \"Failed to load inbox item\",\n    \"memo-comment\": \"{{user}} has a comment on your {{memo}}.\",\n    \"no-archived\": \"No archived notifications\",\n    \"no-unread\": \"No unread notifications\",\n    \"unread\": \"Unread\"\n  },\n  \"live-update\": {\n    \"connected\": \"Live updates active\",\n    \"connecting\": \"Connecting to live updates...\",\n    \"disconnected\": \"Live updates unavailable\"\n  },\n  \"markdown\": {\n    \"checkbox\": \"Checkbox\",\n    \"code-block\": \"Code block\",\n    \"content-syntax\": \"Content syntax\"\n  },\n  \"memo\": {\n    \"archived-at\": \"Archived at\",\n    \"click-to-hide-nsfw-content\": \"Click to hide NSFW content\",\n    \"click-to-show-nsfw-content\": \"Click to show NSFW content\",\n    \"code\": \"Code\",\n    \"comment\": {\n      \"self\": \"Comments\",\n      \"write-a-comment\": \"Write a comment\"\n    },\n    \"copy-content\": \"Copy Content\",\n    \"copy-link\": \"Copy Link\",\n    \"count-memos-in-date\": \"{{count}} {{memos}} in {{date}}\",\n    \"delete-confirm\": \"Are you sure you want to delete this memo?\",\n    \"delete-confirm-description\": \"This action is irreversible. Attachments, links, and references will also be removed.\",\n    \"direction\": \"Direction\",\n    \"direction-asc\": \"Ascending\",\n    \"direction-desc\": \"Descending\",\n    \"display-time\": \"Display Time\",\n    \"filters\": {\n      \"has-code\": \"hasCode\",\n      \"has-link\": \"hasLink\",\n      \"has-task-list\": \"hasTaskList\",\n      \"label\": \"Filters\"\n    },\n    \"links\": \"Links\",\n    \"load-more\": \"Load more\",\n    \"no-archived-memos\": \"No archived memos.\",\n    \"no-memos\": \"No memos.\",\n    \"order-by\": \"Order By\",\n    \"search-placeholder\": \"Search memos...\",\n    \"share\": {\n      \"active-links\": \"Active share links\",\n      \"copied\": \"Copied!\",\n      \"copy\": \"Copy link\",\n      \"create-failed\": \"Failed to create share link\",\n      \"create-link\": \"Create new link\",\n      \"creating\": \"Creating…\",\n      \"expiry-1-day\": \"1 day\",\n      \"expiry-30-days\": \"30 days\",\n      \"expiry-7-days\": \"7 days\",\n      \"expiry-label\": \"Expires\",\n      \"expiry-never\": \"Never\",\n      \"expires-on\": \"Expires {{date}}\",\n      \"invalid-link\": \"This link is invalid or has expired.\",\n      \"never-expires\": \"Never expires\",\n      \"no-links\": \"No share links yet. Create one below.\",\n      \"open-panel\": \"Manage share links\",\n      \"revoke\": \"Revoke\",\n      \"revoke-failed\": \"Failed to revoke link\",\n      \"revoked\": \"Share link revoked\",\n      \"section-label\": \"Sharing\",\n      \"share\": \"Share\",\n      \"shared-by\": \"Shared by {{creator}}\",\n      \"title\": \"Share this memo\"\n    },\n    \"show-less\": \"Show less\",\n    \"show-more\": \"Show more\",\n    \"to-do\": \"To-do\",\n    \"view-detail\": \"View Detail\",\n    \"visibility\": {\n      \"disabled\": \"Public memos are disabled\",\n      \"private\": \"Private\",\n      \"protected\": \"Protected\",\n      \"public\": \"Public\"\n    }\n  },\n  \"message\": {\n    \"archived-successfully\": \"Archived successfully\",\n    \"change-memo-created-time\": \"Change memo created time\",\n    \"copied\": \"Copied\",\n    \"deleted-successfully\": \"Memo deleted successfully\",\n    \"description-is-required\": \"Description is required\",\n    \"failed-to-embed-memo\": \"Failed to embed memo\",\n    \"fill-all\": \"Please fill in all fields.\",\n    \"fill-all-required-fields\": \"Please fill all required fields\",\n    \"maximum-upload-size-is\": \"Maximum allowed upload size is {{size}} MiB\",\n    \"memo-not-found\": \"Memo not found.\",\n    \"new-password-not-match\": \"New passwords do not match.\",\n    \"no-data\": \"No data found.\",\n    \"password-changed\": \"Password Changed\",\n    \"password-not-match\": \"Passwords do not match.\",\n    \"restored-successfully\": \"Restored successfully\",\n    \"succeed-copy-content\": \"Content copied successfully.\",\n    \"succeed-copy-link\": \"Link copied successfully.\",\n    \"update-succeed\": \"Update succeeded\",\n    \"user-not-found\": \"User not found\"\n  },\n  \"reference\": {\n    \"add-references\": \"Add references\",\n    \"embedded-usage\": \"Use as Embedded Content\",\n    \"no-memos-found\": \"No memos found\",\n    \"search-placeholder\": \"Search content\"\n  },\n  \"resource\": {\n    \"clear\": \"Clear\",\n    \"copy-link\": \"Copy Link\",\n    \"create-dialog\": {\n      \"external-link\": {\n        \"file-name\": \"File name\",\n        \"file-name-placeholder\": \"File name\",\n        \"link\": \"Link\",\n        \"link-placeholder\": \"https://the.link.to/your/resource\",\n        \"option\": \"External link\",\n        \"type\": \"Type\",\n        \"type-placeholder\": \"File type\"\n      },\n      \"local-file\": {\n        \"choose\": \"Choose a file…\",\n        \"option\": \"Local file\"\n      },\n      \"title\": \"Create Resource\",\n      \"upload-method\": \"Upload method\"\n    },\n    \"delete-all-unused\": \"Delete all unused\",\n    \"delete-all-unused-confirm\": \"Are you sure you want to delete all unused resources? THIS ACTION IS IRREVERSIBLE\",\n    \"delete-all-unused-error\": \"Failed to delete unused resources\",\n    \"delete-all-unused-success\": \"Resources deleted successfully\",\n    \"delete-resource\": \"Delete Resource\",\n    \"delete-selected-resources\": \"Delete Selected Resources\",\n    \"fetching-data\": \"Fetching data…\",\n    \"file-drag-drop-prompt\": \"Drag and drop your file here to upload file\",\n    \"linked-amount\": \"Linked amount\",\n    \"no-files-selected\": \"No files selected\",\n    \"no-resources\": \"No resources.\",\n    \"no-unused-resources\": \"No unused resources\",\n    \"reset-link\": \"Reset Link\",\n    \"reset-link-prompt\": \"Are you sure you want to reset the link? This will break all current link usages. THIS ACTION IS IRREVERSIBLE\",\n    \"reset-resource-link\": \"Reset Resource Link\",\n    \"unused-resources\": \"Unused resources\"\n  },\n  \"router\": {\n    \"back-to-top\": \"Back to Top\",\n    \"go-to-home\": \"Go to Home\"\n  },\n  \"setting\": {\n    \"access-token\": {\n      \"access-token-copied-to-clipboard\": \"Access token copied to clipboard\",\n      \"access-token-deleted\": \"Access token `{{description}}` deleted\",\n      \"access-token-deletion\": \"Are you sure you want to delete access token `{{description}}`?\",\n      \"access-token-deletion-description\": \"This action is irreversible. You will need to update any services using this token to use a new token.\",\n      \"create-dialog\": {\n        \"access-token-created\": \"Access token `{{description}}` created\",\n        \"create-access-token\": \"Create Access Token\",\n        \"created-at\": \"Created At\",\n        \"description\": \"Description\",\n        \"duration-1m\": \"1 Month\",\n        \"duration-8h\": \"8 Hours\",\n        \"duration-never\": \"Never\",\n        \"expiration\": \"Expiration\",\n        \"expires-at\": \"Expires At\",\n        \"some-description\": \"Some description...\"\n      },\n      \"description\": \"A list of all access tokens for your account.\",\n      \"title\": \"Access Tokens\",\n      \"token\": \"Token\"\n    },\n    \"account\": {\n      \"change-password\": \"Change password\",\n      \"email-note\": \"Optional\",\n      \"export-memos\": \"Export Memos\",\n      \"nickname-note\": \"Displayed in the banner\",\n      \"openapi-reset\": \"Reset OpenAPI Key\",\n      \"openapi-sample-post\": \"Hello #memos from {{url}}\",\n      \"openapi-title\": \"OpenAPI\",\n      \"reset-api\": \"Reset API\",\n      \"title\": \"Account Information\",\n      \"update-information\": \"Update Information\",\n      \"username-note\": \"Used to sign in\"\n    },\n    \"instance\": {\n      \"disallow-change-nickname\": \"Disallow changing nickname\",\n      \"disallow-change-username\": \"Disallow changing username\",\n      \"disallow-password-auth\": \"Disallow password auth\",\n      \"disallow-user-registration\": \"Disallow user registration\",\n      \"monday\": \"Monday\",\n      \"saturday\": \"Saturday\",\n      \"sunday\": \"Sunday\",\n      \"week-start-day\": \"Week start day\"\n    },\n    \"member\": {\n      \"admin\": \"Admin\",\n      \"archive-member\": \"Archive member\",\n      \"archive-success\": \"{{username}} archived successfully\",\n      \"archive-warning\": \"Are you sure you want to archive {{username}}?\",\n      \"archive-warning-description\": \"Archiving disables the account. You can restore or delete it later.\",\n      \"create-a-member\": \"Create a member\",\n      \"delete-member\": \"Delete Member\",\n      \"delete-success\": \"{{username}} deleted successfully\",\n      \"delete-warning\": \"Are you sure you want to delete {{username}}?\",\n      \"delete-warning-description\": \"THIS ACTION IS IRREVERSIBLE\",\n      \"label\": \"Member\",\n      \"list-title\": \"Member list\",\n      \"restore-success\": \"{{username}} restored successfully\",\n      \"user\": \"User\"\n    },\n    \"memo\": {\n      \"content-length-limit\": \"Content length limit (Byte)\",\n      \"enable-blur-nsfw-content\": \"Enable sensitive content (NSFW) blurring\",\n      \"enable-memo-comments\": \"Enable memo comments\",\n      \"enable-memo-location\": \"Enable memo location\",\n      \"label\": \"Memo\",\n      \"reactions\": \"Reactions\",\n      \"title\": \"Memo related settings\"\n    },\n    \"my-account\": {\n      \"label\": \"My Account\"\n    },\n    \"preference\": {\n      \"default-memo-sort-option\": \"Memo display time\",\n      \"default-memo-visibility\": \"Default memo visibility\",\n      \"label\": \"Preferences\",\n      \"theme\": \"Theme\"\n    },\n    \"shortcut\": {\n      \"delete-confirm\": \"Are you sure you want to delete shortcut `{{title}}`?\",\n      \"delete-success\": \"Shortcut `{{title}}` deleted successfully\"\n    },\n    \"sso\": {\n      \"authorization-endpoint\": \"Authorization endpoint\",\n      \"client-id\": \"Client ID\",\n      \"client-secret\": \"Client secret\",\n      \"confirm-delete\": \"Are you sure you want to delete `{{name}}` SSO configuration? THIS ACTION IS IRREVERSIBLE\",\n      \"create-sso\": \"Create SSO\",\n      \"custom\": \"Custom\",\n      \"delete-sso\": \"Confirm delete\",\n      \"disabled-password-login-warning\": \"Password-login is disabled, be extra careful when removing identity providers\",\n      \"display-name\": \"Display Name\",\n      \"identifier\": \"Identifier\",\n      \"identifier-filter\": \"Identifier Filter\",\n      \"label\": \"SSO\",\n      \"no-sso-found\": \"No SSO found.\",\n      \"redirect-url\": \"Redirect URL\",\n      \"scopes\": \"Scopes\",\n      \"single-sign-on\": \"Configuring Single Sign-On (SSO) for Authentication\",\n      \"sso-created\": \"SSO {{name}} created\",\n      \"sso-list\": \"SSO List\",\n      \"sso-updated\": \"SSO {{name}} updated\",\n      \"template\": \"Template\",\n      \"token-endpoint\": \"Token endpoint\",\n      \"update-sso\": \"Update SSO\",\n      \"user-endpoint\": \"User endpoint\"\n    },\n    \"storage\": {\n      \"accesskey\": \"Access key\",\n      \"accesskey-placeholder\": \"Access key / Access ID\",\n      \"bucket\": \"Bucket\",\n      \"bucket-placeholder\": \"Bucket name\",\n      \"create-a-service\": \"Create a service\",\n      \"create-storage\": \"Create Storage\",\n      \"current-storage\": \"Current object storage\",\n      \"delete-storage\": \"Delete Storage\",\n      \"endpoint\": \"Endpoint\",\n      \"filepath-template\": \"Filepath template\",\n      \"label\": \"Storage\",\n      \"local-storage-path\": \"Local storage path\",\n      \"path\": \"Storage Path\",\n      \"path-description\": \"You can use the same dynamic variables from local storage, like {filename}\",\n      \"path-placeholder\": \"custom/path\",\n      \"presign-placeholder\": \"Pre-sign URL, optional\",\n      \"region\": \"Region\",\n      \"region-placeholder\": \"Region name\",\n      \"s3-compatible-url\": \"S3 Compatible URL\",\n      \"secretkey\": \"Secret key\",\n      \"secretkey-placeholder\": \"Secret key / Access Key\",\n      \"storage-services\": \"Storage services\",\n      \"type-database\": \"Database\",\n      \"type-local\": \"Local file system\",\n      \"update-a-service\": \"Update a service\",\n      \"update-local-path\": \"Update Local Storage Path\",\n      \"update-local-path-description\": \"Local storage path is a relative path to your database file\",\n      \"update-storage\": \"Update Storage\",\n      \"url-prefix\": \"URL prefix\",\n      \"url-prefix-placeholder\": \"Custom URL prefix, optional\",\n      \"url-suffix\": \"URL suffix\",\n      \"url-suffix-placeholder\": \"Custom URL suffix, optional\",\n      \"warning-text\": \"Are you sure you want to delete storage service `{{name}}`? THIS ACTION IS IRREVERSIBLE\"\n    },\n    \"system\": {\n      \"additional-script\": \"Additional script\",\n      \"additional-script-placeholder\": \"Additional JavaScript code\",\n      \"additional-style\": \"Additional style\",\n      \"additional-style-placeholder\": \"Additional CSS code\",\n      \"allow-user-signup\": \"Allow user signup\",\n      \"customize-server\": {\n        \"description\": \"Description\",\n        \"icon-url\": \"Icon URL\",\n        \"locale\": \"Server Locale\",\n        \"title\": \"Customize Server\"\n      },\n      \"disable-password-login\": \"Disable password login\",\n      \"disable-password-login-final-warning\": \"Please type `CONFIRM` if you know what you are doing.\",\n      \"disable-password-login-warning\": \"This will disable password login for all users. It is not possible to log in without reverting this setting in the database if your configured identity providers fail. You'll also have to be extra careful when removing an identity provider\",\n      \"display-with-updated-time\": \"Display with updated time\",\n      \"enable-auto-compact\": \"Enable auto compact\",\n      \"enable-double-click-to-edit\": \"Enable double click to edit\",\n      \"enable-password-login\": \"Enable password login\",\n      \"enable-password-login-warning\": \"This will enable password login for all users. Continue only if you want to users to be able to log in using both SSO and password\",\n      \"label\": \"System\",\n      \"max-upload-size\": \"Maximum upload size (MiB)\",\n      \"max-upload-size-hint\": \"Recommended value is 32 MiB.\",\n      \"removed-completed-task-list-items\": \"Enable removal of completed task list items\",\n      \"server-name\": \"Server Name\",\n      \"title\": \"General\"\n    },\n    \"version\": \"Version\",\n    \"webhook\": {\n      \"create-dialog\": {\n        \"an-easy-to-remember-name\": \"An easy-to-remember name\",\n        \"create-webhook\": \"Create webhook\",\n        \"create-webhook-success\": \"Webhook `{{name}}` created\",\n        \"edit-webhook\": \"Edit webhook\",\n        \"payload-url\": \"Payload URL\",\n        \"title\": \"Title\",\n        \"url-example-post-receive\": \"https://example.com/postreceive\"\n      },\n      \"delete-dialog\": {\n        \"delete-webhook-description\": \"This action is irreversible.\",\n        \"delete-webhook-success\": \"Webhook `{{name}}` deleted successfully\",\n        \"delete-webhook-title\": \"Are you sure you want to delete webhook `{{name}}`?\"\n      },\n      \"no-webhooks-found\": \"No webhooks found.\",\n      \"title\": \"Webhooks\",\n      \"url\": \"URL\"\n    }\n  },\n  \"tag\": {\n    \"all-tags\": \"All Tags\",\n    \"create-tag\": \"Create Tag\",\n    \"create-tags-guide\": \"You can create tags by inputting `#tag`.\",\n    \"delete-confirm\": \"Are you sure you want to delete this tag? All related memos will be archived.\",\n    \"delete-success\": \"Tag deleted successfully\",\n    \"delete-tag\": \"Delete Tag\",\n    \"new-name\": \"New Name\",\n    \"no-tag-found\": \"No tag found\",\n    \"old-name\": \"Old Name\",\n    \"rename-error-empty\": \"Tag name cannot be empty or contain spaces\",\n    \"rename-error-repeat\": \"New name cannot be the same as the old name\",\n    \"rename-success\": \"Renamed tag successfully\",\n    \"rename-tag\": \"Rename tag\",\n    \"rename-tip\": \"All your memos with this tag will be updated.\"\n  },\n  \"tooltip\": {\n    \"link-memo\": \"Link Memo\",\n    \"markdown-menu\": \"Markdown\",\n    \"select-location\": \"Location\",\n    \"select-visibility\": \"Visibility\",\n    \"tags\": \"Tags\",\n    \"upload-attachment\": \"Upload Attachment(s)\"\n  }\n}\n"
  },
  {
    "path": "web/src/locales/es.json",
    "content": "{\n  \"about\": {\n    \"blogs\": \"Blogs\",\n    \"description\": \"Un servicio de notas ligero y centrado en la privacidad. Captura y comparte fácilmente tus grandes ideas.\",\n    \"documents\": \"Documentos\",\n    \"github-repository\": \"Repositorio GitHub\",\n    \"official-website\": \"Sitio web oficial\"\n  },\n  \"auth\": {\n    \"create-your-account\": \"Crea tu cuenta\",\n    \"host-tip\": \"Te estás registrando como anfitrión del sitio.\",\n    \"new-password\": \"Nueva contraseña\",\n    \"repeat-new-password\": \"Repite la nueva contraseña\",\n    \"sign-in-tip\": \"¿Ya tienes una cuenta?\",\n    \"sign-up-tip\": \"¿Aún no tienes una cuenta?\"\n  },\n  \"common\": {\n    \"about\": \"Acerca de\",\n    \"add\": \"Agregar\",\n    \"admin\": \"Administrador\",\n    \"all\": \"Todos\",\n    \"archive\": \"Archivar\",\n    \"archived\": \"Archivado\",\n    \"attachments\": \"Adjuntos\",\n    \"auto-expand\": \"Auto expandir\",\n    \"avatar\": \"Avatar\",\n    \"basic\": \"Básico\",\n    \"beta\": \"Beta\",\n    \"calendar\": \"Calendario\",\n    \"cancel\": \"Cancelar\",\n    \"change\": \"Cambiar\",\n    \"clear\": \"Borrar\",\n    \"close\": \"Cerrar\",\n    \"collapse\": \"Colapsar\",\n    \"confirm\": \"Confirmar\",\n    \"copy\": \"Copiar\",\n    \"create\": \"Crear\",\n    \"created-at\": \"Creado en\",\n    \"database\": \"Base de datos\",\n    \"day\": \"Día\",\n    \"days\": {\n      \"fri\": \"Vie\",\n      \"mon\": \"Lun\",\n      \"sat\": \"Sáb\",\n      \"sun\": \"Dom\",\n      \"thu\": \"Jue\",\n      \"tue\": \"Mar\",\n      \"wed\": \"Mié\"\n    },\n    \"delete\": \"Eliminar\",\n    \"description\": \"Descripción\",\n    \"edit\": \"Editar\",\n    \"email\": \"Correo electrónico\",\n    \"expand\": \"Expandir\",\n    \"explore\": \"Explorar\",\n    \"file\": \"Archivo\",\n    \"filter\": \"Filtro\",\n    \"home\": \"Inicio\",\n    \"image\": \"Imagen\",\n    \"in\": \"En\",\n    \"inbox\": \"Bandeja de entrada\",\n    \"input\": \"Entrada\",\n    \"language\": \"Idioma\",\n    \"last-updated-at\": \"Última actualización en\",\n    \"learn-more\": \"Saber más\",\n    \"link\": \"Enlace\",\n    \"map\": \"Mapa\",\n    \"mark\": \"Marcar\",\n    \"memo\": \"Memo\",\n    \"memos\": \"Memos\",\n    \"more\": \"Más\",\n    \"name\": \"Nombre\",\n    \"new\": \"Nuevo\",\n    \"nickname\": \"Apodo\",\n    \"null\": \"Nulo\",\n    \"or\": \"o\",\n    \"password\": \"Contraseña\",\n    \"pin\": \"Enganchar\",\n    \"pinned\": \"Fijado\",\n    \"preview\": \"Vista previa\",\n    \"profile\": \"Perfil\",\n    \"properties\": \"Propiedades\",\n    \"referenced-by\": \"Referenciado por\",\n    \"referencing\": \"Referenciando\",\n    \"relations\": \"Relaciones\",\n    \"remember-me\": \"Recuérdame\",\n    \"rename\": \"Renombrar\",\n    \"reset\": \"Restablecer\",\n    \"resources\": \"Recursos\",\n    \"restore\": \"Restaurar\",\n    \"role\": \"Rol\",\n    \"save\": \"Guardar\",\n    \"search\": \"Buscar\",\n    \"select\": \"Seleccionar\",\n    \"settings\": \"Configuración\",\n    \"share\": \"Compartir\",\n    \"shortcut-filter\": \"Filtro de atajos\",\n    \"shortcuts\": \"Atajos\",\n    \"sign-in\": \"Iniciar sesión\",\n    \"sign-in-with\": \"Iniciar sesión con {{provider}}\",\n    \"sign-out\": \"Cerrar sesión\",\n    \"sign-up\": \"Registrarse\",\n    \"statistics\": \"Estadísticas\",\n    \"tags\": \"Etiquetas\",\n    \"title\": \"Título\",\n    \"today\": \"Hoy\",\n    \"tree-mode\": \"Modo árbol\",\n    \"type\": \"Tipo\",\n    \"unpin\": \"Desenganchar\",\n    \"update\": \"Actualizar\",\n    \"upload\": \"Subir\",\n    \"user\": \"Usuario\",\n    \"username\": \"Usuario\",\n    \"version\": \"Versión\",\n    \"visibility\": \"Visibilidad\",\n    \"yourself\": \"Tú mismo\"\n  },\n  \"editor\": {\n    \"add-your-comment-here\": \"Agrega tu comentario aquí...\",\n    \"any-thoughts\": \"Alguna idea...\",\n    \"exit-focus-mode\": \"Salir del modo enfoque\",\n    \"focus-mode\": \"Modo enfoque\",\n    \"no-changes-detected\": \"No se detectaron cambios\",\n    \"save\": \"Guardar\",\n    \"saving\": \"Guardando...\",\n    \"slash-commands\": \"Escribe `/` para comandos\"\n  },\n  \"inbox\": {\n    \"failed-to-load\": \"Error al cargar el elemento de bandeja de entrada\",\n    \"memo-comment\": \"{{user}} tiene un comentario sobre tu {{memo}}.\",\n    \"no-archived\": \"No hay notificaciones archivadas\",\n    \"no-unread\": \"No hay notificaciones no leídas\",\n    \"unread\": \"No leído\"\n  },\n  \"markdown\": {\n    \"checkbox\": \"Casilla de verificación\",\n    \"code-block\": \"Bloque de código\",\n    \"content-syntax\": \"Sintaxis de contenido\"\n  },\n  \"memo\": {\n    \"archived-at\": \"Archivado en\",\n    \"click-to-hide-nsfw-content\": \"Haz clic para ocultar contenido sensible\",\n    \"click-to-show-nsfw-content\": \"Haz clic para mostrar contenido sensible\",\n    \"code\": \"Código\",\n    \"comment\": {\n      \"self\": \"Comentarios\",\n      \"write-a-comment\": \"Escribe un comentario\"\n    },\n    \"copy-content\": \"Copiar contenido\",\n    \"copy-link\": \"Copiar enlace\",\n    \"count-memos-in-date\": \"{{count}} {{memos}} en {{date}}\",\n    \"delete-confirm\": \"¿Estás seguro de que quieres eliminar este memo?\",\n    \"delete-confirm-description\": \"Esta acción es irreversible. Los adjuntos, enlaces y referencias también se eliminarán.\",\n    \"direction\": \"Dirección\",\n    \"direction-asc\": \"Ascendente\",\n    \"direction-desc\": \"Descendente\",\n    \"display-time\": \"Hora de visualización\",\n    \"filters\": {\n      \"has-code\": \"tieneCódigo\",\n      \"has-link\": \"tieneEnlace\",\n      \"has-task-list\": \"tieneListaTareas\"\n    },\n    \"links\": \"Enlaces\",\n    \"load-more\": \"Cargar más\",\n    \"no-archived-memos\": \"No hay memos archivados.\",\n    \"no-memos\": \"No hay memos.\",\n    \"order-by\": \"Ordenar por\",\n    \"search-placeholder\": \"Buscar memos...\",\n    \"show-less\": \"Mostrar menos\",\n    \"show-more\": \"Mostrar más\",\n    \"to-do\": \"Tareas\",\n    \"view-detail\": \"Ver detalles\",\n    \"visibility\": {\n      \"disabled\": \"Los memos públicos están deshabilitados\",\n      \"private\": \"Privado\",\n      \"protected\": \"Protegido\",\n      \"public\": \"Público\"\n    }\n  },\n  \"message\": {\n    \"archived-successfully\": \"Archivado correctamente\",\n    \"change-memo-created-time\": \"Cambiar la hora de creación del memo\",\n    \"copied\": \"Copiado\",\n    \"deleted-successfully\": \"Eliminado correctamente\",\n    \"description-is-required\": \"La descripción es obligatoria\",\n    \"failed-to-embed-memo\": \"No se pudo incrustar el memo\",\n    \"fill-all\": \"Por favor rellena todos los campos.\",\n    \"fill-all-required-fields\": \"Por favor rellena todos los campos obligatorios\",\n    \"maximum-upload-size-is\": \"El tamaño máximo de subida es de {{size}} MiB\",\n    \"memo-not-found\": \"Memo no encontrado.\",\n    \"new-password-not-match\": \"Las nuevas contraseñas no coinciden.\",\n    \"no-data\": \"No se encontraron datos.\",\n    \"password-changed\": \"Contraseña cambiada\",\n    \"password-not-match\": \"Las contraseñas no coinciden.\",\n    \"restored-successfully\": \"Restaurado con éxito\",\n    \"succeed-copy-content\": \"Contenido copiado correctamente.\",\n    \"succeed-copy-link\": \"Enlace copiado correctamente.\",\n    \"update-succeed\": \"Actualización exitosa\",\n    \"user-not-found\": \"Usuario no encontrado\"\n  },\n  \"reference\": {\n    \"add-references\": \"Agregar referencias\",\n    \"embedded-usage\": \"Usar como contenido incrustado\",\n    \"no-memos-found\": \"No se encontraron memos\",\n    \"search-placeholder\": \"Buscar contenido\"\n  },\n  \"resource\": {\n    \"clear\": \"Limpiar\",\n    \"copy-link\": \"Copiar enlace\",\n    \"create-dialog\": {\n      \"external-link\": {\n        \"file-name\": \"Nombre del archivo\",\n        \"file-name-placeholder\": \"Nombre del archivo\",\n        \"link\": \"Enlace\",\n        \"link-placeholder\": \"https://el.enlace.a/tu/recurso\",\n        \"option\": \"Enlace externo\",\n        \"type\": \"Tipo\",\n        \"type-placeholder\": \"Tipo de archivo\"\n      },\n      \"local-file\": {\n        \"choose\": \"Escoge un archivo…\",\n        \"option\": \"Archivo local\"\n      },\n      \"title\": \"Crear recurso\",\n      \"upload-method\": \"Método de carga\"\n    },\n    \"delete-all-unused\": \"Eliminar todo lo no utilizado\",\n    \"delete-all-unused-confirm\": \"¿Estás seguro de que quieres eliminar todos los recursos no utilizados? ESTA ACCIÓN ES IRREVERSIBLE\",\n    \"delete-all-unused-error\": \"Error al eliminar los recursos no utilizados\",\n    \"delete-all-unused-success\": \"Recursos eliminados correctamente\",\n    \"delete-resource\": \"Borrar recurso\",\n    \"delete-selected-resources\": \"Borrar recursos seleccionados\",\n    \"fetching-data\": \"Obteniendo datos…\",\n    \"file-drag-drop-prompt\": \"Arrastra y suelta tu archivo aquí para subirlo\",\n    \"linked-amount\": \"Notas vinculadas\",\n    \"no-files-selected\": \"No hay archivos seleccionados\",\n    \"no-resources\": \"Sin recursos.\",\n    \"no-unused-resources\": \"No hay recursos sin utilizar\",\n    \"reset-link\": \"Restablecer enlace\",\n    \"reset-link-prompt\": \"¿Estás seguro de restablecer el enlace? Esto romperá todos los usos actuales del enlace. ESTA ACCIÓN ES IRREVERSIBLE\",\n    \"reset-resource-link\": \"Restablecer enlace de recursos\",\n    \"unused-resources\": \"Recursos sin utilizar\"\n  },\n  \"router\": {\n    \"back-to-top\": \"Volver al principio\",\n    \"go-to-home\": \"Ir al inicio\"\n  },\n  \"setting\": {\n    \"member\": {\n      \"admin\": \"Administrador\",\n      \"archive-member\": \"Archivar miembro\",\n      \"archive-success\": \"{{username}} archivado correctamente\",\n      \"archive-warning\": \"¿Estás seguro de archivar a {{username}}?\",\n      \"archive-warning-description\": \"Archivar deshabilita la cuenta. Puedes restaurarla o eliminarla más tarde.\",\n      \"create-a-member\": \"Crear un miembro\",\n      \"delete-member\": \"Eliminar miembro\",\n      \"delete-success\": \"{{username}} eliminado correctamente\",\n      \"delete-warning\": \"¿Estás seguro de eliminar a {{username}}? ESTA ACCIÓN ES IRREVERSIBLE\",\n      \"delete-warning-description\": \"ESTA ACCIÓN ES IRREVERSIBLE\",\n      \"restore-success\": \"{{username}} restaurado correctamente\",\n      \"user\": \"Usuario\",\n      \"label\": \"Miembro\",\n      \"list-title\": \"Lista de miembros\"\n    },\n    \"my-account\": {\n      \"label\": \"Mi cuenta\"\n    },\n    \"preference\": {\n      \"default-memo-sort-option\": \"Hora de visualización del memo\",\n      \"default-memo-visibility\": \"Visibilidad predeterminada del memo\",\n      \"theme\": \"Tema\",\n      \"label\": \"Preferencias\"\n    },\n    \"shortcut\": {\n      \"delete-confirm\": \"¿Estás seguro de que quieres eliminar el atajo `{{title}}`?\",\n      \"delete-success\": \"Atajo `{{title}}` eliminado correctamente\"\n    },\n    \"sso\": {\n      \"authorization-endpoint\": \"Punto final de autorización\",\n      \"client-id\": \"ID del cliente\",\n      \"client-secret\": \"Secreto del cliente\",\n      \"confirm-delete\": \"¿Estás seguro de eliminar la configuración SSO \\\"{{name}}\\\"? ESTA ACCIÓN ES IRREVERSIBLE\",\n      \"create-sso\": \"Crear SSO\",\n      \"custom\": \"Personalizado\",\n      \"delete-sso\": \"Confirmar eliminación\",\n      \"disabled-password-login-warning\": \"El inicio de sesión con contraseña está deshabilitado, ten especial cuidado al eliminar proveedores de identidad\",\n      \"display-name\": \"Nombre para mostrar\",\n      \"identifier\": \"Identificador\",\n      \"identifier-filter\": \"Filtro de identificador\",\n      \"no-sso-found\": \"No se encontró SSO.\",\n      \"redirect-url\": \"URL de redirección\",\n      \"scopes\": \"Ámbitos\",\n      \"single-sign-on\": \"Configurando Single Sign-On (SSO) para autenticación\",\n      \"sso-created\": \"SSO {{name}} creado\",\n      \"sso-list\": \"Lista de SSO\",\n      \"sso-updated\": \"SSO {{name}} actualizado\",\n      \"template\": \"Plantilla\",\n      \"token-endpoint\": \"Punto final de token\",\n      \"update-sso\": \"Actualizar SSO\",\n      \"user-endpoint\": \"Punto final de usuario\",\n      \"label\": \"SSO\"\n    },\n    \"storage\": {\n      \"accesskey\": \"Clave de acceso\",\n      \"accesskey-placeholder\": \"Clave de acceso / ID de acceso\",\n      \"bucket\": \"Bucket\",\n      \"bucket-placeholder\": \"Nombre del bucket\",\n      \"create-a-service\": \"Crear un servicio\",\n      \"create-storage\": \"Crear almacenamiento\",\n      \"current-storage\": \"Almacenamiento de objetos actual\",\n      \"delete-storage\": \"Eliminar almacenamiento\",\n      \"endpoint\": \"Punto final\",\n      \"filepath-template\": \"Plantilla de ruta de archivo\",\n      \"local-storage-path\": \"Ruta de almacenamiento local\",\n      \"path\": \"Ruta de almacenamiento\",\n      \"path-description\": \"Puedes usar las mismas variables dinámicas del almacenamiento local, como {filename}\",\n      \"path-placeholder\": \"ruta/personalizada\",\n      \"presign-placeholder\": \"URL pre-firmada, opcional\",\n      \"region\": \"Región\",\n      \"region-placeholder\": \"Nombre de la región\",\n      \"s3-compatible-url\": \"URL compatible con S3\",\n      \"secretkey\": \"Clave secreta\",\n      \"secretkey-placeholder\": \"Clave secreta / Clave de acceso\",\n      \"storage-services\": \"Servicios de almacenamiento\",\n      \"type-database\": \"Base de datos\",\n      \"type-local\": \"Sistema de archivos local\",\n      \"update-a-service\": \"Actualizar un servicio\",\n      \"update-local-path\": \"Actualizar ruta de almacenamiento local\",\n      \"update-local-path-description\": \"La ruta de almacenamiento local es una ruta relativa a tu archivo de base de datos\",\n      \"update-storage\": \"Actualizar almacenamiento\",\n      \"url-prefix\": \"Prefijo de URL\",\n      \"url-prefix-placeholder\": \"Prefijo de URL personalizado, opcional\",\n      \"url-suffix\": \"Sufijo de URL\",\n      \"url-suffix-placeholder\": \"Sufijo de URL personalizado, opcional\",\n      \"warning-text\": \"¿Estás seguro de eliminar el servicio de almacenamiento \\\"{{name}}\\\"? ESTA ACCIÓN ES IRREVERSIBLE\",\n      \"label\": \"Almacenamiento\"\n    },\n    \"system\": {\n      \"additional-script\": \"Script adicional\",\n      \"additional-script-placeholder\": \"Código JavaScript adicional\",\n      \"additional-style\": \"Estilo adicional\",\n      \"additional-style-placeholder\": \"Código CSS adicional\",\n      \"allow-user-signup\": \"Permitir registro de usuarios\",\n      \"customize-server\": {\n        \"description\": \"Descripción\",\n        \"icon-url\": \"URL del icono\",\n        \"locale\": \"Idioma del servidor\",\n        \"title\": \"Personalizar servidor\"\n      },\n      \"disable-password-login\": \"Desactivar inicio de sesión con contraseña\",\n      \"disable-password-login-final-warning\": \"Por favor escribe \\\"CONFIRM\\\" si sabes lo que haces.\",\n      \"disable-password-login-warning\": \"Esto deshabilitará el inicio de sesión con contraseña para todos los usuarios. No será posible iniciar sesión sin revertir esta configuración en la base de datos si tus proveedores de identidad fallan. Debes tener especial cuidado al eliminar un proveedor de identidad\",\n      \"display-with-updated-time\": \"Mostrar con hora actualizada\",\n      \"enable-auto-compact\": \"Habilitar auto compactación\",\n      \"enable-double-click-to-edit\": \"Habilitar doble clic para editar\",\n      \"enable-password-login\": \"Habilitar inicio de sesión con contraseña\",\n      \"enable-password-login-warning\": \"Esto habilitará el inicio de sesión con contraseña para todos los usuarios. Continúa solo si quieres que los usuarios puedan iniciar sesión tanto con SSO como con contraseña\",\n      \"max-upload-size\": \"Tamaño máximo de subida (MiB)\",\n      \"max-upload-size-hint\": \"El valor recomendado es 32 MiB.\",\n      \"removed-completed-task-list-items\": \"Habilitar eliminación de tareas completadas\",\n      \"server-name\": \"Nombre del servidor\",\n      \"title\": \"General\",\n      \"label\": \"Sistema\"\n    },\n    \"version\": \"Versión\",\n    \"access-token\": {\n      \"access-token-copied-to-clipboard\": \"Token de acceso copiado al portapapeles\",\n      \"access-token-deleted\": \"Token de acceso `{{description}}` eliminado\",\n      \"access-token-deletion\": \"¿Estás seguro de que quieres eliminar el token de acceso `{{description}}`?\",\n      \"access-token-deletion-description\": \"Esta acción es irreversible. Necesitarás actualizar cualquier servicio que use este token para usar uno nuevo.\",\n      \"create-dialog\": {\n        \"access-token-created\": \"Token de acceso `{{description}}` creado\",\n        \"create-access-token\": \"Crear token de acceso\",\n        \"created-at\": \"Creado en\",\n        \"description\": \"Descripción\",\n        \"duration-1m\": \"1 mes\",\n        \"duration-8h\": \"8 horas\",\n        \"duration-never\": \"Nunca\",\n        \"expiration\": \"Expiración\",\n        \"expires-at\": \"Expira en\",\n        \"some-description\": \"Alguna descripción...\"\n      },\n      \"description\": \"Lista de todos los tokens de acceso de tu cuenta.\",\n      \"title\": \"Tokens de acceso\",\n      \"token\": \"Token\"\n    },\n    \"account\": {\n      \"change-password\": \"Cambiar contraseña\",\n      \"email-note\": \"Opcional\",\n      \"export-memos\": \"Exportar memos\",\n      \"nickname-note\": \"Mostrado en el banner\",\n      \"openapi-reset\": \"Restablecer clave OpenAPI\",\n      \"openapi-sample-post\": \"Hola #memos desde {{url}}\",\n      \"openapi-title\": \"OpenAPI\",\n      \"reset-api\": \"Restablecer API\",\n      \"title\": \"Información de la cuenta\",\n      \"update-information\": \"Actualizar información\",\n      \"username-note\": \"Usado para iniciar sesión\"\n    },\n    \"instance\": {\n      \"disallow-change-nickname\": \"No permitir cambiar apodo\",\n      \"disallow-change-username\": \"No permitir cambiar nombre de usuario\",\n      \"disallow-password-auth\": \"No permitir autenticación por contraseña\",\n      \"disallow-user-registration\": \"No permitir registro de usuarios\",\n      \"monday\": \"Lunes\",\n      \"saturday\": \"Sábado\",\n      \"sunday\": \"Domingo\",\n      \"week-start-day\": \"Día de inicio de semana\"\n    },\n    \"memo\": {\n      \"content-length-limit\": \"Límite de longitud de contenido (Bytes)\",\n      \"enable-blur-nsfw-content\": \"Habilitar difuminado de contenido sensible (NSFW)\",\n      \"enable-memo-comments\": \"Habilitar comentarios en los memos\",\n      \"enable-memo-location\": \"Habilitar ubicación del memo\",\n      \"reactions\": \"Reacciones\",\n      \"title\": \"Configuración de memos\",\n      \"label\": \"Memo\"\n    },\n    \"webhook\": {\n      \"create-dialog\": {\n        \"an-easy-to-remember-name\": \"Un nombre fácil de recordar\",\n        \"create-webhook\": \"Crear webhook\",\n        \"create-webhook-success\": \"Webhook `{{name}}` creado\",\n        \"edit-webhook\": \"Editar webhook\",\n        \"payload-url\": \"URL de carga\",\n        \"title\": \"Título\",\n        \"url-example-post-receive\": \"https://example.com/postreceive\"\n      },\n      \"delete-dialog\": {\n        \"delete-webhook-description\": \"Esta acción es irreversible.\",\n        \"delete-webhook-success\": \"Webhook `{{name}}` eliminado correctamente\",\n        \"delete-webhook-title\": \"¿Estás seguro de que quieres eliminar el webhook `{{name}}`?\"\n      },\n      \"no-webhooks-found\": \"No se encontraron webhooks.\",\n      \"title\": \"Webhooks\",\n      \"url\": \"URL\"\n    }\n  },\n  \"tag\": {\n    \"all-tags\": \"Todas las etiquetas\",\n    \"create-tag\": \"Crear etiqueta\",\n    \"create-tags-guide\": \"Puedes crear etiquetas escribiendo `#etiqueta`.\",\n    \"delete-confirm\": \"¿Estás seguro de eliminar esta etiqueta? Todos los memos relacionados serán archivados.\",\n    \"delete-success\": \"Etiqueta eliminada correctamente\",\n    \"delete-tag\": \"Eliminar etiqueta\",\n    \"new-name\": \"Nombre nuevo\",\n    \"no-tag-found\": \"No se encontró ninguna etiqueta\",\n    \"old-name\": \"Nombre anterior\",\n    \"rename-error-empty\": \"El nombre de la etiqueta no puede estar vacío ni contener espacios\",\n    \"rename-error-repeat\": \"El nombre nuevo no puede ser igual al anterior\",\n    \"rename-success\": \"Etiqueta renombrada correctamente\",\n    \"rename-tag\": \"Renombrar etiqueta\",\n    \"rename-tip\": \"Todos tus memos con esta etiqueta serán actualizados.\"\n  },\n  \"tooltip\": {\n    \"link-memo\": \"Vincular memo\",\n    \"markdown-menu\": \"Markdown\",\n    \"select-location\": \"Ubicación\",\n    \"select-visibility\": \"Visibilidad\",\n    \"tags\": \"Etiquetas\",\n    \"upload-attachment\": \"Subir archivo(s)\"\n  }\n}\n"
  },
  {
    "path": "web/src/locales/fa.json",
    "content": "{\n  \"about\": {\n    \"blogs\": \"بلاگ‌ها\",\n    \"description\": \"یک سرویس یادداشت برداری سبک و در درجه اول حفظ حریم خصوصی. افکار عالی خود را به راحتی ثبت و به اشتراک بگذارید.\",\n    \"documents\": \"اسناد\",\n    \"github-repository\": \"ریپازیتوری گیت‌هاب\",\n    \"official-website\": \"وبسایت رسمی\"\n  },\n  \"auth\": {\n    \"create-your-account\": \"حساب کاربری خود را ایجاد کنید.\",\n    \"host-tip\": \"حساب شما به عنوان مدیر سایت تعریف خواهد شد.\",\n    \"new-password\": \"گذرواژه جدید\",\n    \"repeat-new-password\": \"گذرواژه جدید را تکرار کنید\",\n    \"sign-in-tip\": \"آیا حساب کاربری تعریف شده دارید؟\",\n    \"sign-up-tip\": \"آیا حساب کاربری تعریف شده ندارید؟\"\n  },\n  \"common\": {\n    \"about\": \"درباره\",\n    \"add\": \"اضافه کردن\",\n    \"admin\": \"مدیر\",\n    \"all\": \"همه\",\n    \"archive\": \"آرشیو\",\n    \"archived\": \"آرشیو شده\",\n    \"attachments\": \"پیوست‌ها\",\n    \"auto-expand\": \"گسترش خودکار\",\n    \"avatar\": \"آواتار\",\n    \"basic\": \"ساده\",\n    \"beta\": \"بتا\",\n    \"calendar\": \"تقویم\",\n    \"cancel\": \"لغو\",\n    \"change\": \"تعویض\",\n    \"clear\": \"پاکسازی\",\n    \"close\": \"بستن\",\n    \"collapse\": \"جمع کردن\",\n    \"confirm\": \"تایید\",\n    \"copy\": \"کپی\",\n    \"create\": \"ایجاد\",\n    \"created-at\": \"ایجاد شده در\",\n    \"database\": \"پایگاه داده\",\n    \"day\": \"روز\",\n    \"days\": {\n      \"fri\": \"ج\",\n      \"mon\": \"د\",\n      \"sat\": \"ش\",\n      \"sun\": \"ی\",\n      \"thu\": \"پ\",\n      \"tue\": \"س\",\n      \"wed\": \"چ\"\n    },\n    \"delete\": \"حذف\",\n    \"description\": \"توضیحات\",\n    \"edit\": \"ویرایش\",\n    \"email\": \"ایمیل\",\n    \"expand\": \"گسترش\",\n    \"explore\": \"کاوش\",\n    \"file\": \"فایل\",\n    \"filter\": \"فیلتر\",\n    \"home\": \"خانه\",\n    \"image\": \"تصویر\",\n    \"in\": \"در\",\n    \"inbox\": \"صندوق ورودی\",\n    \"input\": \"ورودی\",\n    \"language\": \"زبان\",\n    \"last-updated-at\": \"آخرین بروزرسانی در\",\n    \"learn-more\": \"اطلاعات بیشتر\",\n    \"link\": \"پیوست\",\n    \"map\": \"نقشه\",\n    \"mark\": \"علامت‌گذاری\",\n    \"memo\": \"یادداشت\",\n    \"memos\": \"یادداشت‌ها\",\n    \"more\": \"بیشتر\",\n    \"name\": \"نام\",\n    \"new\": \"جدید\",\n    \"nickname\": \"نام مستعار\",\n    \"null\": \"خالی\",\n    \"or\": \"یا\",\n    \"password\": \"گذرواژه\",\n    \"pin\": \"نشان\",\n    \"pinned\": \"نشان شده\",\n    \"preview\": \"پیشنمایش\",\n    \"profile\": \"مشخصات\",\n    \"properties\": \"ویژگی‌ها\",\n    \"referenced-by\": \"ارجاع داده شده توسط\",\n    \"referencing\": \"ارجاع دادن\",\n    \"relations\": \"روابط\",\n    \"remember-me\": \"مرابه خاطر بسپار\",\n    \"rename\": \"تغییر نام دادن\",\n    \"reset\": \"تنظیم مجدد\",\n    \"resources\": \"منابع\",\n    \"restore\": \"بازیابی\",\n    \"role\": \"نقش\",\n    \"save\": \"ذخیره\",\n    \"search\": \"جستجو\",\n    \"select\": \"انتخاب کردن\",\n    \"settings\": \"تنظیمات\",\n    \"share\": \"اشتراک\",\n    \"shortcut-filter\": \"فیلتر میانبر\",\n    \"shortcuts\": \"میانبرها\",\n    \"sign-in\": \"ورود به حساب کاربری\",\n    \"sign-in-with\": \"{{provider}} ورود با\",\n    \"sign-out\": \"خروج از حساب کاربری\",\n    \"sign-up\": \"ایجاد حساب کاربری\",\n    \"statistics\": \"آمار\",\n    \"tags\": \"برچسب‌ها\",\n    \"title\": \"عنوان\",\n    \"today\": \"امروز\",\n    \"tree-mode\": \"حالت درختی\",\n    \"type\": \"نوع\",\n    \"unpin\": \"حذف نشان\",\n    \"update\": \"بروزرسانی\",\n    \"upload\": \"بارگذاری\",\n    \"user\": \"کاربر\",\n    \"username\": \"نام کاربری\",\n    \"version\": \"نسخه\",\n    \"visibility\": \"قابل مشاهده توسط\",\n    \"yourself\": \"خودتان\"\n  },\n  \"editor\": {\n    \"add-your-comment-here\": \"نظر خود را اینجا اضافه کنید...\",\n    \"any-thoughts\": \"فکر شما...\",\n    \"exit-focus-mode\": \"خروج از حالت تمرکز\",\n    \"focus-mode\": \"حالت تمرکز\",\n    \"no-changes-detected\": \"تغییری شناسایی نشد\",\n    \"save\": \"ذخیره\",\n    \"saving\": \"در حال ذخیره...\",\n    \"slash-commands\": \"برای دستورات '/' را تایپ کنید\"\n  },\n  \"inbox\": {\n    \"failed-to-load\": \"بارگذاری آیتم صندوق ورودی ناموفق بود\",\n    \"memo-comment\": \"یک نظر برروی یادداشت شما «{{memo}}» اضافه کرده است!{{user}}\",\n    \"no-archived\": \"هیچ اعلان آرشیو شده‌ای نیست\",\n    \"no-unread\": \"هیچ اعلان خوانده نشده‌ای نیست\",\n    \"unread\": \"خوانده نشده\"\n  },\n  \"markdown\": {\n    \"checkbox\": \"چک‌باکس\",\n    \"code-block\": \"بلوک کد\",\n    \"content-syntax\": \"سینتکس محتوا\"\n  },\n  \"memo\": {\n    \"archived-at\": \"آرشیو شده در\",\n    \"click-to-hide-nsfw-content\": \"برای مخفی کردن محتوای حساس کلیک کنید\",\n    \"click-to-show-nsfw-content\": \"برای نمایش محتوای حساس کلیک کنید\",\n    \"code\": \"کد\",\n    \"comment\": {\n      \"self\": \"نظرات\",\n      \"write-a-comment\": \"یک نظر بنویسید\"\n    },\n    \"copy-content\": \"کپی محتوا\",\n    \"copy-link\": \"کپی پیوند\",\n    \"count-memos-in-date\": \"{{count}} {{memos}} در {{date}}\",\n    \"delete-confirm\": \"آیا مطمئن هستید که می‌خواهید این یادداشت را حذف کنید؟\",\n    \"delete-confirm-description\": \"این عمل غیرقابل بازگشت است. پیوست‌ها، پیوندها و ارجاعات نیز حذف خواهند شد.\",\n    \"direction\": \"جهت\",\n    \"direction-asc\": \"صعودی\",\n    \"direction-desc\": \"نزولی\",\n    \"display-time\": \"زمان نمایش\",\n    \"filters\": {\n      \"has-code\": \"دارای کد\",\n      \"has-link\": \"دارای پیوند\",\n      \"has-task-list\": \"دارای لیست کارها\"\n    },\n    \"links\": \"پیوندها\",\n    \"load-more\": \"بارگذاری بیشتر\",\n    \"no-archived-memos\": \"یادداشت آرشیو شده‌ای وجود ندارد.\",\n    \"no-memos\": \"یادداشتی یافت نشد.\",\n    \"order-by\": \"مرتب‌سازی بر اساس\",\n    \"search-placeholder\": \"جستجوی یادداشت‌ها...\",\n    \"show-less\": \"نمایش کمتر\",\n    \"show-more\": \"نمایش بیشتر\",\n    \"to-do\": \"کارها\",\n    \"view-detail\": \"نمایش جزئیات\",\n    \"visibility\": {\n      \"disabled\": \"یادداشت‌های عمومی غیرفعال هستند\",\n      \"private\": \"خصوصی\",\n      \"protected\": \"فضای کار\",\n      \"public\": \"عمومی\"\n    }\n  },\n  \"message\": {\n    \"archived-successfully\": \"با موفقیت آرشیو شد\",\n    \"change-memo-created-time\": \"تغییر زمان ایجاد یادداشت\",\n    \"copied\": \"کپی شد\",\n    \"deleted-successfully\": \"با موفقیت حذف شد\",\n    \"description-is-required\": \"توضیحات اجباری است\",\n    \"failed-to-embed-memo\": \"درج یادداشت ناموفق بود\",\n    \"fill-all\": \"لطفاً همه فیلدها را پر کنید.\",\n    \"fill-all-required-fields\": \"لطفا همه فیلدهای ضروری را پر کنید\",\n    \"maximum-upload-size-is\": \"حداکثر حجم مجاز بارگذاری {{size}} مگابایت است\",\n    \"memo-not-found\": \"یادداشت یافت نشد.\",\n    \"new-password-not-match\": \"گذرواژه‌های جدید مطابقت ندارند.\",\n    \"no-data\": \"هیچ داده‌ای یافت نشد.\",\n    \"password-changed\": \"گذرواژه تغییر یافت\",\n    \"password-not-match\": \"گذرواژه‌ها مطابقت ندارند.\",\n    \"restored-successfully\": \"با موفقیت بازیابی شد\",\n    \"succeed-copy-content\": \"محتوا با موفقیت کپی شد.\",\n    \"succeed-copy-link\": \"پیوند با موفقیت کپی شد.\",\n    \"update-succeed\": \"بروزرسانی موفق بود\",\n    \"user-not-found\": \"کاربر یافت نشد\"\n  },\n  \"reference\": {\n    \"add-references\": \"افزودن منابع\",\n    \"embedded-usage\": \"استفاده به عنوان محتوای جاسازی‌شده\",\n    \"no-memos-found\": \"یادداشتی یافت نشد\",\n    \"search-placeholder\": \"جستجوی محتوا\"\n  },\n  \"resource\": {\n    \"clear\": \"پاک کردن\",\n    \"copy-link\": \"کپی لینک\",\n    \"create-dialog\": {\n      \"external-link\": {\n        \"file-name\": \"نام فایل\",\n        \"file-name-placeholder\": \"نام فایل\",\n        \"link\": \"پیوست\",\n        \"link-placeholder\": \"https://the.link.to/your/resource\",\n        \"option\": \"پیوست خارجی\",\n        \"type\": \"نوع\",\n        \"type-placeholder\": \"نوع فایل\"\n      },\n      \"local-file\": {\n        \"choose\": \"یک فایل را انتخاب کنید...\",\n        \"option\": \"فایل محلی\"\n      },\n      \"title\": \"ایجاد منبع\",\n      \"upload-method\": \"روش بارگذاری\"\n    },\n    \"delete-all-unused\": \"حذف همه استفاده نشده‌ها\",\n    \"delete-all-unused-confirm\": \"آیا مطمئن هستید که می‌خواهید همه منابع استفاده نشده را حذف کنید؟ این عمل غیرقابل بازگشت است\",\n    \"delete-all-unused-error\": \"حذف منابع استفاده نشده ناموفق بود\",\n    \"delete-all-unused-success\": \"منابع با موفقیت حذف شدند\",\n    \"delete-resource\": \"حذف منبع\",\n    \"delete-selected-resources\": \"حذف منابع انتخاب شده\",\n    \"fetching-data\": \"در حال دریافت اطلاعات...\",\n    \"file-drag-drop-prompt\": \"فایل خود را بکشید و در اینجا رها کنید تا بارگذاری شود\",\n    \"linked-amount\": \"تعداد پیوست‌ها\",\n    \"no-files-selected\": \"فایلی انتخاب نشده\",\n    \"no-resources\": \"منبعی وجود ندارد\",\n    \"no-unused-resources\": \"منبع بدون استفاده وجود ندارد.\",\n    \"reset-link\": \"بازنشانی لینک\",\n    \"reset-link-prompt\": \"آیا از بازنشانی لینک اطمینان دارید؟ با این کار تمام استفاده‌های این لینک از بین می‌رود. این عمل غیرقابل بازگشت است\",\n    \"reset-resource-link\": \"بازنشانی لینک منبع\",\n    \"unused-resources\": \"منابع استفاده نشده\"\n  },\n  \"router\": {\n    \"back-to-top\": \"بازگشت به بالا\",\n    \"go-to-home\": \"برو به خانه\"\n  },\n  \"setting\": {\n    \"member\": {\n      \"admin\": \"مدیر\",\n      \"archive-member\": \"آرشیو کردن عضو\",\n      \"archive-success\": \"{{username}} با موفقیت آرشیو شد\",\n      \"archive-warning\": \"آیا از آرشیو کردن {{username}} اطمینان دارید؟\",\n      \"archive-warning-description\": \"آرشیو کردن حساب را غیرفعال می‌کند. می‌توانید آن را بعداً بازیابی یا حذف کنید.\",\n      \"create-a-member\": \"ایجاد عضو\",\n      \"delete-member\": \"حذف عضو\",\n      \"delete-success\": \"{{username}} با موفقیت حذف شد\",\n      \"delete-warning\": \"آیا از حذف {{username}} اطمینان دارید؟\",\n      \"delete-warning-description\": \"این عمل غیرقابل بازگشت است\",\n      \"restore-success\": \"{{username}} با موفقیت بازیابی شد\",\n      \"user\": \"کاربر\",\n      \"label\": \"عضو\",\n      \"list-title\": \"لیست اعضا\"\n    },\n    \"my-account\": {\n      \"label\": \"حساب من\"\n    },\n    \"preference\": {\n      \"default-memo-sort-option\": \"زمان نمایش یادداشت\",\n      \"default-memo-visibility\": \"قابلیت مشاهده پیش‌فرض یادداشت\",\n      \"theme\": \"پوسته\",\n      \"label\": \"ترجیحات\"\n    },\n    \"shortcut\": {\n      \"delete-confirm\": \"آیا مطمئن هستید که می‌خواهید میانبر «{{title}}» را حذف کنید؟\",\n      \"delete-success\": \"میانبر «{{title}}» با موفقیت حذف شد\"\n    },\n    \"sso\": {\n      \"authorization-endpoint\": \"آدرس نقطه مجوز\",\n      \"client-id\": \"شناسه کلاینت\",\n      \"client-secret\": \"راز کلاینت\",\n      \"confirm-delete\": \"آیا از حذف پیکربندی SSO «{{name}}» اطمینان دارید؟ این عمل غیرقابل بازگشت است\",\n      \"create-sso\": \"ایجاد SSO\",\n      \"custom\": \"سفارشی\",\n      \"delete-sso\": \"تایید حذف\",\n      \"disabled-password-login-warning\": \"ورود با گذرواژه غیرفعال است، هنگام حذف ارائه‌دهندگان هویت دقت کنید\",\n      \"display-name\": \"نام نمایشی\",\n      \"identifier\": \"شناسه\",\n      \"identifier-filter\": \"فیلتر شناسه\",\n      \"no-sso-found\": \"هیچ SSO یافت نشد.\",\n      \"redirect-url\": \"آدرس بازگشت\",\n      \"scopes\": \"دامنه‌ها\",\n      \"single-sign-on\": \"پیکربندی Single Sign-On (SSO) برای احراز هویت\",\n      \"sso-created\": \"SSO {{name}} ایجاد شد\",\n      \"sso-list\": \"لیست SSO\",\n      \"sso-updated\": \"SSO {{name}} بروزرسانی شد\",\n      \"template\": \"قالب\",\n      \"token-endpoint\": \"آدرس نقطه توکن\",\n      \"update-sso\": \"بروزرسانی SSO\",\n      \"user-endpoint\": \"آدرس نقطه کاربر\",\n      \"label\": \"SSO\"\n    },\n    \"storage\": {\n      \"accesskey\": \"کلید دسترسی\",\n      \"accesskey-placeholder\": \"کلید دسترسی / شناسه دسترسی\",\n      \"bucket\": \"باکت\",\n      \"bucket-placeholder\": \"نام باکت\",\n      \"create-a-service\": \"ایجاد سرویس\",\n      \"create-storage\": \"ایجاد ذخیره‌سازی\",\n      \"current-storage\": \"ذخیره‌سازی فعلی اشیاء\",\n      \"delete-storage\": \"حذف ذخیره‌سازی\",\n      \"endpoint\": \"نقطه پایانی\",\n      \"filepath-template\": \"قالب مسیر فایل\",\n      \"local-storage-path\": \"مسیر ذخیره‌سازی محلی\",\n      \"path\": \"مسیر ذخیره‌سازی\",\n      \"path-description\": \"می‌توانید از همان متغیرهای پویا ذخیره‌سازی محلی مانند {filename} استفاده کنید\",\n      \"path-placeholder\": \"مسیر/سفارشی\",\n      \"presign-placeholder\": \"آدرس پیش‌امضا شده، اختیاری\",\n      \"region\": \"منطقه\",\n      \"region-placeholder\": \"نام منطقه\",\n      \"s3-compatible-url\": \"آدرس سازگار با S3\",\n      \"secretkey\": \"کلید مخفی\",\n      \"secretkey-placeholder\": \"کلید مخفی / کلید دسترسی\",\n      \"storage-services\": \"سرویس‌های ذخیره‌سازی\",\n      \"type-database\": \"پایگاه داده\",\n      \"type-local\": \"سیستم فایل محلی\",\n      \"update-a-service\": \"بروزرسانی سرویس\",\n      \"update-local-path\": \"بروزرسانی مسیر محلی\",\n      \"update-local-path-description\": \"مسیر ذخیره‌سازی محلی یک مسیر نسبی به فایل پایگاه داده شما است\",\n      \"update-storage\": \"بروزرسانی ذخیره‌سازی\",\n      \"url-prefix\": \"پیشوند آدرس\",\n      \"url-prefix-placeholder\": \"پیشوند سفارشی آدرس، اختیاری\",\n      \"url-suffix\": \"پسوند آدرس\",\n      \"url-suffix-placeholder\": \"پسوند سفارشی آدرس، اختیاری\",\n      \"warning-text\": \"آیا از حذف سرویس ذخیره‌سازی «{{name}}» اطمینان دارید؟ این عمل غیرقابل بازگشت است\",\n      \"label\": \"ذخیره‌سازی\"\n    },\n    \"system\": {\n      \"additional-script\": \"اسکریپت اضافی\",\n      \"additional-script-placeholder\": \"کد جاوااسکریپت اضافی\",\n      \"additional-style\": \"استایل اضافی\",\n      \"additional-style-placeholder\": \"کد CSS اضافی\",\n      \"allow-user-signup\": \"اجازه ثبت‌نام کاربر\",\n      \"customize-server\": {\n        \"description\": \"توضیحات\",\n        \"icon-url\": \"آدرس آیکون\",\n        \"locale\": \"زبان سرور\",\n        \"title\": \"سفارشی‌سازی سرور\"\n      },\n      \"disable-password-login\": \"غیرفعال‌سازی ورود با گذرواژه\",\n      \"disable-password-login-final-warning\": \"لطفاً اگر مطمئن هستید «CONFIRM» را وارد کنید.\",\n      \"disable-password-login-warning\": \"این کار ورود با گذرواژه را برای همه کاربران غیرفعال می‌کند. اگر ارائه‌دهندگان هویت شما از کار بیفتند، بدون بازگرداندن این تنظیم در پایگاه داده، ورود ممکن نیست. هنگام حذف ارائه‌دهنده هویت دقت کنید.\",\n      \"display-with-updated-time\": \"نمایش با زمان بروزرسانی\",\n      \"enable-auto-compact\": \"فعال‌سازی فشرده‌سازی خودکار\",\n      \"enable-double-click-to-edit\": \"فعال‌سازی دوبار کلیک برای ویرایش\",\n      \"enable-password-login\": \"فعال‌سازی ورود با گذرواژه\",\n      \"enable-password-login-warning\": \"این کار ورود با گذرواژه را برای همه کاربران فعال می‌کند. فقط ادامه دهید اگر می‌خواهید کاربران بتوانند با SSO و گذرواژه وارد شوند\",\n      \"max-upload-size\": \"حداکثر حجم بارگذاری (مگابایت)\",\n      \"max-upload-size-hint\": \"مقدار پیشنهادی ۳۲ مگابایت است.\",\n      \"removed-completed-task-list-items\": \"فعال‌سازی حذف موارد تکمیل‌شده لیست کارها\",\n      \"server-name\": \"نام سرور\",\n      \"title\": \"عمومی\",\n      \"label\": \"سیستم\"\n    },\n    \"version\": \"نسخه\",\n    \"access-token\": {\n      \"access-token-copied-to-clipboard\": \"توکن دسترسی کپی شد\",\n      \"access-token-deleted\": \"توکن دسترسی «{{description}}» حذف شد\",\n      \"access-token-deletion\": \"آیا از حذف توکن دسترسی «{{description}}» اطمینان دارید؟\",\n      \"access-token-deletion-description\": \"این عمل غیرقابل بازگشت است. باید هر سرویسی که از این توکن استفاده می‌کند را به‌روزرسانی کنید تا از توکن جدید استفاده کند.\",\n      \"create-dialog\": {\n        \"access-token-created\": \"توکن دسترسی «{{description}}» ایجاد شد\",\n        \"create-access-token\": \"ایجاد توکن دسترسی\",\n        \"created-at\": \"ایجاد شده در\",\n        \"description\": \"توضیحات\",\n        \"duration-1m\": \"۱ ماه\",\n        \"duration-8h\": \"۸ ساعت\",\n        \"duration-never\": \"هرگز\",\n        \"expiration\": \"انقضا\",\n        \"expires-at\": \"منقضی می‌شود در\",\n        \"some-description\": \"توضیحی...\"\n      },\n      \"description\": \"لیست تمام توکن‌های دسترسی حساب شما.\",\n      \"title\": \"توکن‌های دسترسی\",\n      \"token\": \"توکن\"\n    },\n    \"account\": {\n      \"change-password\": \"تغییر گذرواژه\",\n      \"email-note\": \"اختیاری\",\n      \"export-memos\": \"صادرات یادداشت‌ها\",\n      \"nickname-note\": \"در بنر نمایش داده می‌شود\",\n      \"openapi-reset\": \"بازنشانی کلید OpenAPI\",\n      \"openapi-sample-post\": \"Hello #memos from {{url}}\",\n      \"openapi-title\": \"OpenAPI\",\n      \"reset-api\": \"بازنشانی API\",\n      \"title\": \"اطلاعات حساب کاربری\",\n      \"update-information\": \"بروزرسانی اطلاعات\",\n      \"username-note\": \"برای ورود استفاده می‌شود\"\n    },\n    \"instance\": {\n      \"disallow-change-nickname\": \"غیرفعال‌سازی تغییر نام مستعار\",\n      \"disallow-change-username\": \"غیرفعال‌سازی تغییر نام کاربری\",\n      \"disallow-password-auth\": \"غیرفعال‌سازی احراز هویت با گذرواژه\",\n      \"disallow-user-registration\": \"غیرفعال‌سازی ثبت‌نام کاربر\",\n      \"monday\": \"دوشنبه\",\n      \"saturday\": \"شنبه\",\n      \"sunday\": \"یکشنبه\",\n      \"week-start-day\": \"روز شروع هفته\"\n    },\n    \"memo\": {\n      \"content-length-limit\": \"محدودیت طول محتوا (بایت)\",\n      \"enable-blur-nsfw-content\": \"فعال‌سازی تار کردن محتوای حساس (NSFW)\",\n      \"enable-memo-comments\": \"فعال‌سازی نظرات یادداشت\",\n      \"enable-memo-location\": \"فعال‌سازی موقعیت یادداشت\",\n      \"reactions\": \"واکنش‌ها\",\n      \"title\": \"تنظیمات یادداشت\",\n      \"label\": \"یادداشت\"\n    },\n    \"webhook\": {\n      \"create-dialog\": {\n        \"an-easy-to-remember-name\": \"نامی آسان برای به خاطر سپردن\",\n        \"create-webhook\": \"ایجاد وب‌هوک\",\n        \"create-webhook-success\": \"وب‌هوک «{{name}}» ایجاد شد\",\n        \"edit-webhook\": \"ویرایش وب‌هوک\",\n        \"payload-url\": \"آدرس بارگذاری\",\n        \"title\": \"عنوان\",\n        \"url-example-post-receive\": \"https://example.com/postreceive\"\n      },\n      \"delete-dialog\": {\n        \"delete-webhook-description\": \"این عمل غیرقابل بازگشت است.\",\n        \"delete-webhook-success\": \"وب‌هوک «{{name}}» با موفقیت حذف شد\",\n        \"delete-webhook-title\": \"آیا مطمئن هستید که می‌خواهید وب‌هوک «{{name}}» را حذف کنید؟\"\n      },\n      \"no-webhooks-found\": \"وب‌هوکی یافت نشد.\",\n      \"title\": \"وب‌هوک‌ها\",\n      \"url\": \"آدرس\"\n    }\n  },\n  \"tag\": {\n    \"all-tags\": \"همه برچسب‌ها\",\n    \"create-tag\": \"ایجاد برچسب\",\n    \"create-tags-guide\": \"می‌توانید با وارد کردن `#برچسب` برچسب بسازید.\",\n    \"delete-confirm\": \"آیا از حذف این برچسب اطمینان دارید؟ همه یادداشت‌های مرتبط آرشیو خواهند شد.\",\n    \"delete-success\": \"برچسب با موفقیت حذف شد\",\n    \"delete-tag\": \"حذف برچسب\",\n    \"new-name\": \"نام جدید\",\n    \"no-tag-found\": \"برچسبی یافت نشد\",\n    \"old-name\": \"نام قبلی\",\n    \"rename-error-empty\": \"نام برچسب نمی‌تواند خالی باشد یا فاصله داشته باشد\",\n    \"rename-error-repeat\": \"نام جدید نمی‌تواند با نام قبلی یکسان باشد\",\n    \"rename-success\": \"برچسب با موفقیت تغییر نام یافت\",\n    \"rename-tag\": \"تغییر نام برچسب\",\n    \"rename-tip\": \"همه یادداشت‌های شما با این برچسب بروزرسانی خواهند شد.\"\n  },\n  \"tooltip\": {\n    \"link-memo\": \"پیوند یادداشت\",\n    \"markdown-menu\": \"مارک‌داون\",\n    \"select-location\": \"موقعیت\",\n    \"select-visibility\": \"قابلیت مشاهده\",\n    \"tags\": \"برچسب‌ها\",\n    \"upload-attachment\": \"بارگذاری پیوست(ها)\"\n  }\n}\n"
  },
  {
    "path": "web/src/locales/fr.json",
    "content": "{\n  \"about\": {\n    \"blogs\": \"Blogs\",\n    \"description\": \"Un service de prise de notes léger et axé sur la confidentialité. Capturez et partagez facilement vos meilleures idées.\",\n    \"documents\": \"Documents\",\n    \"github-repository\": \"Dépôt GitHub\",\n    \"official-website\": \"Site officiel\"\n  },\n  \"auth\": {\n    \"create-your-account\": \"Créez votre compte\",\n    \"host-tip\": \"Vous vous inscrivez en tant qu'hôte du site.\",\n    \"new-password\": \"Nouveau mot de passe\",\n    \"repeat-new-password\": \"Répétez le nouveau mot de passe\",\n    \"sign-in-tip\": \"Vous avez déjà un compte ?\",\n    \"sign-up-tip\": \"Vous n'avez pas encore de compte ?\"\n  },\n  \"common\": {\n    \"about\": \"À propos\",\n    \"add\": \"Ajouter\",\n    \"admin\": \"Admin\",\n    \"all\": \"Tout\",\n    \"archive\": \"Archiver\",\n    \"archived\": \"Archivé\",\n    \"attachments\": \"Pièces jointes\",\n    \"auto-expand\": \"Déplier automatiquement\",\n    \"avatar\": \"Avatar\",\n    \"basic\": \"Basique\",\n    \"beta\": \"Bêta\",\n    \"calendar\": \"Calendrier\",\n    \"cancel\": \"Annuler\",\n    \"change\": \"Changer\",\n    \"clear\": \"Effacer\",\n    \"close\": \"Fermer\",\n    \"collapse\": \"Réduire\",\n    \"confirm\": \"Confirmer\",\n    \"copy\": \"Copier\",\n    \"create\": \"Créer\",\n    \"created-at\": \"Créé le\",\n    \"database\": \"Base de données\",\n    \"day\": \"Jour\",\n    \"days\": {\n      \"fri\": \"Ven\",\n      \"mon\": \"Lun\",\n      \"sat\": \"Sam\",\n      \"sun\": \"Dim\",\n      \"thu\": \"Jeu\",\n      \"tue\": \"Mar\",\n      \"wed\": \"Mer\"\n    },\n    \"delete\": \"Supprimer\",\n    \"description\": \"Description\",\n    \"edit\": \"Éditer\",\n    \"email\": \"E-mail\",\n    \"expand\": \"Développer\",\n    \"explore\": \"Explorer\",\n    \"file\": \"Fichier\",\n    \"filter\": \"Filtrer\",\n    \"home\": \"Accueil\",\n    \"image\": \"Image\",\n    \"in\": \"Dans\",\n    \"inbox\": \"Boîte de réception\",\n    \"input\": \"Entrée\",\n    \"language\": \"Langue\",\n    \"last-updated-at\": \"Dernière mise à jour le\",\n    \"learn-more\": \"En savoir plus\",\n    \"link\": \"Lien\",\n    \"map\": \"Carte\",\n    \"mark\": \"Marquer\",\n    \"memo\": \"Memo\",\n    \"memos\": \"Memos\",\n    \"more\": \"Plus\",\n    \"name\": \"Nom\",\n    \"new\": \"Nouveau\",\n    \"nickname\": \"Surnom\",\n    \"null\": \"Nul\",\n    \"or\": \"ou\",\n    \"password\": \"Mot de passe\",\n    \"pin\": \"Épingler\",\n    \"pinned\": \"Épinglé\",\n    \"preview\": \"Aperçu\",\n    \"profile\": \"Profil\",\n    \"properties\": \"Propriétés\",\n    \"referenced-by\": \"Référencé par\",\n    \"referencing\": \"Référence\",\n    \"relations\": \"Relations\",\n    \"remember-me\": \"Se souvenir de moi\",\n    \"rename\": \"Renommer\",\n    \"reset\": \"Réinitialiser\",\n    \"resources\": \"Ressources\",\n    \"restore\": \"Restaurer\",\n    \"role\": \"Rôle\",\n    \"save\": \"Enregistrer\",\n    \"search\": \"Rechercher\",\n    \"select\": \"Sélectionner\",\n    \"settings\": \"Paramètres\",\n    \"share\": \"Partager\",\n    \"shortcut-filter\": \"Filtre de raccourcis\",\n    \"shortcuts\": \"Raccourcis\",\n    \"sign-in\": \"Connexion\",\n    \"sign-in-with\": \"Se connecter avec {{provider}}\",\n    \"sign-out\": \"Déconnexion\",\n    \"sign-up\": \"S'inscrire\",\n    \"statistics\": \"Statistiques\",\n    \"tags\": \"Tags\",\n    \"title\": \"Titre\",\n    \"today\": \"Aujourd'hui\",\n    \"tree-mode\": \"Mode arborescence\",\n    \"type\": \"Type\",\n    \"unpin\": \"Désépingler\",\n    \"update\": \"Mettre à jour\",\n    \"upload\": \"Téléverser\",\n    \"user\": \"Utilisateur\",\n    \"username\": \"Nom d'utilisateur\",\n    \"version\": \"Version\",\n    \"visibility\": \"Visibilité\",\n    \"yourself\": \"Vous-même\"\n  },\n  \"editor\": {\n    \"add-your-comment-here\": \"Ajoutez votre commentaire ici...\",\n    \"any-thoughts\": \"Des idées...\",\n    \"exit-focus-mode\": \"Quitter le mode concentration\",\n    \"focus-mode\": \"Mode concentration\",\n    \"no-changes-detected\": \"Aucun changement détecté\",\n    \"save\": \"Enregistrer\",\n    \"saving\": \"Enregistrement...\",\n    \"slash-commands\": \"Tapez `/` pour les commandes\"\n  },\n  \"inbox\": {\n    \"failed-to-load\": \"Échec du chargement de l'élément\",\n    \"memo-comment\": \"{{user}} a commenté votre {{memo}}.\",\n    \"no-archived\": \"Aucune notification archivée\",\n    \"no-unread\": \"Aucune notification non lue\",\n    \"unread\": \"Non lu\"\n  },\n  \"markdown\": {\n    \"checkbox\": \"Case à cocher\",\n    \"code-block\": \"Bloc de code\",\n    \"content-syntax\": \"Syntaxe du contenu\"\n  },\n  \"memo\": {\n    \"archived-at\": \"Archivé le\",\n    \"click-to-hide-nsfw-content\": \"Cliquez pour masquer le contenu sensible\",\n    \"click-to-show-nsfw-content\": \"Cliquez pour afficher le contenu sensible\",\n    \"code\": \"Code\",\n    \"comment\": {\n      \"self\": \"Commentaires\",\n      \"write-a-comment\": \"Écrire un commentaire\"\n    },\n    \"copy-content\": \"Copier le contenu\",\n    \"copy-link\": \"Copier le lien\",\n    \"count-memos-in-date\": \"{{count}} {{memos}} le {{date}}\",\n    \"delete-confirm\": \"Êtes-vous sûr de vouloir supprimer cette note ? CETTE ACTION EST IRRÉVERSIBLE\",\n    \"delete-confirm-description\": \"Cette action est irréversible. Les pièces jointes, liens et références seront également supprimés.\",\n    \"direction\": \"Direction\",\n    \"direction-asc\": \"Ascendant\",\n    \"direction-desc\": \"Descendant\",\n    \"display-time\": \"Afficher l'heure\",\n    \"filters\": {\n      \"has-code\": \"aCode\",\n      \"has-link\": \"aLien\",\n      \"has-task-list\": \"aListeTâches\"\n    },\n    \"links\": \"Liens\",\n    \"load-more\": \"Charger plus\",\n    \"no-archived-memos\": \"Aucune note archivée.\",\n    \"no-memos\": \"Aucune note.\",\n    \"order-by\": \"Trier par\",\n    \"search-placeholder\": \"Rechercher des notes...\",\n    \"show-less\": \"Afficher moins\",\n    \"show-more\": \"Afficher plus\",\n    \"to-do\": \"À faire\",\n    \"view-detail\": \"Voir le détail\",\n    \"visibility\": {\n      \"disabled\": \"Les notes publiques sont désactivées\",\n      \"private\": \"Privé\",\n      \"protected\": \"Protégé\",\n      \"public\": \"Public\"\n    }\n  },\n  \"message\": {\n    \"archived-successfully\": \"Archivé avec succès\",\n    \"change-memo-created-time\": \"Changer la date de création de la note\",\n    \"copied\": \"Copié\",\n    \"deleted-successfully\": \"Supprimé avec succès\",\n    \"description-is-required\": \"La description est requise\",\n    \"failed-to-embed-memo\": \"Échec de l'intégration de la note\",\n    \"fill-all\": \"Veuillez remplir tous les champs.\",\n    \"fill-all-required-fields\": \"Veuillez remplir tous les champs obligatoires\",\n    \"maximum-upload-size-is\": \"La taille maximale autorisée est de {{size}} MiB\",\n    \"memo-not-found\": \"Note introuvable.\",\n    \"new-password-not-match\": \"Les nouveaux mots de passe ne correspondent pas.\",\n    \"no-data\": \"Aucune donnée trouvée.\",\n    \"password-changed\": \"Mot de passe modifié\",\n    \"password-not-match\": \"Les mots de passe ne correspondent pas.\",\n    \"restored-successfully\": \"Restauré avec succès\",\n    \"succeed-copy-content\": \"Contenu copié avec succès.\",\n    \"succeed-copy-link\": \"Lien copié avec succès.\",\n    \"update-succeed\": \"Mise à jour réussie\",\n    \"user-not-found\": \"Utilisateur non trouvé\"\n  },\n  \"reference\": {\n    \"add-references\": \"Ajouter des références\",\n    \"embedded-usage\": \"Utiliser comme contenu intégré\",\n    \"no-memos-found\": \"Aucune note trouvée\",\n    \"search-placeholder\": \"Rechercher du contenu\"\n  },\n  \"resource\": {\n    \"clear\": \"Effacer\",\n    \"copy-link\": \"Copier le lien\",\n    \"create-dialog\": {\n      \"external-link\": {\n        \"file-name\": \"Nom du fichier\",\n        \"file-name-placeholder\": \"Nom du fichier\",\n        \"link\": \"Lien\",\n        \"link-placeholder\": \"https://le.lien.vers/votre/ressource\",\n        \"option\": \"Lien externe\",\n        \"type\": \"Type\",\n        \"type-placeholder\": \"Type de fichier\"\n      },\n      \"local-file\": {\n        \"choose\": \"Choisir un fichier…\",\n        \"option\": \"Fichier local\"\n      },\n      \"title\": \"Créer une ressource\",\n      \"upload-method\": \"Méthode de téléversement\"\n    },\n    \"delete-all-unused\": \"Supprimer toutes les ressources inutilisées\",\n    \"delete-all-unused-confirm\": \"Êtes-vous sûr de vouloir supprimer toutes les ressources inutilisées ? CETTE ACTION EST IRRÉVERSIBLE\",\n    \"delete-all-unused-error\": \"Échec de la suppression des ressources inutilisées\",\n    \"delete-all-unused-success\": \"Ressources supprimées avec succès\",\n    \"delete-resource\": \"Supprimer la ressource\",\n    \"delete-selected-resources\": \"Supprimer les ressources sélectionnées\",\n    \"fetching-data\": \"Récupération des données…\",\n    \"file-drag-drop-prompt\": \"Glissez-déposez votre fichier ici pour le téléverser\",\n    \"linked-amount\": \"Liens associés\",\n    \"no-files-selected\": \"Aucun fichier sélectionné\",\n    \"no-resources\": \"Aucune ressource.\",\n    \"no-unused-resources\": \"Aucune ressource inutilisée\",\n    \"reset-link\": \"Réinitialiser le lien\",\n    \"reset-link-prompt\": \"Êtes-vous sûr de vouloir réinitialiser le lien ? Cela rompra toutes les utilisations actuelles du lien. CETTE ACTION EST IRRÉVERSIBLE\",\n    \"reset-resource-link\": \"Réinitialiser le lien de la ressource\",\n    \"unused-resources\": \"Ressources inutilisées\"\n  },\n  \"router\": {\n    \"back-to-top\": \"Retour en haut\",\n    \"go-to-home\": \"Aller à l'accueil\"\n  },\n  \"setting\": {\n    \"member\": {\n      \"admin\": \"Admin\",\n      \"archive-member\": \"Archiver le membre\",\n      \"archive-success\": \"{{username}} archivé avec succès\",\n      \"archive-warning\": \"Êtes-vous sûr de vouloir archiver {{username}} ?\",\n      \"archive-warning-description\": \"L'archivage désactive le compte. Vous pourrez le restaurer ou le supprimer plus tard.\",\n      \"create-a-member\": \"Créer un membre\",\n      \"delete-member\": \"Supprimer le membre\",\n      \"delete-success\": \"{{username}} supprimé avec succès\",\n      \"delete-warning\": \"Êtes-vous sûr de vouloir supprimer {{username}} ? CETTE ACTION EST IRRÉVERSIBLE\",\n      \"delete-warning-description\": \"CETTE ACTION EST IRRÉVERSIBLE\",\n      \"restore-success\": \"{{username}} restauré avec succès\",\n      \"user\": \"Utilisateur\",\n      \"label\": \"Membre\",\n      \"list-title\": \"Liste des membres\"\n    },\n    \"my-account\": {\n      \"label\": \"Mon compte\"\n    },\n    \"preference\": {\n      \"default-memo-sort-option\": \"Date d'affichage de la note\",\n      \"default-memo-visibility\": \"Visibilité par défaut de la note\",\n      \"theme\": \"Thème\",\n      \"label\": \"Préférences\"\n    },\n    \"shortcut\": {\n      \"delete-confirm\": \"Êtes-vous sûr de vouloir supprimer le raccourci `{{title}}` ?\",\n      \"delete-success\": \"Raccourci `{{title}}` supprimé avec succès\"\n    },\n    \"sso\": {\n      \"authorization-endpoint\": \"Point de terminaison d'autorisation\",\n      \"client-id\": \"ID client\",\n      \"client-secret\": \"Secret client\",\n      \"confirm-delete\": \"Êtes-vous sûr de vouloir supprimer la configuration SSO \\\"{{name}}\\\" ? CETTE ACTION EST IRRÉVERSIBLE\",\n      \"create-sso\": \"Créer SSO\",\n      \"custom\": \"Personnalisé\",\n      \"delete-sso\": \"Confirmer la suppression\",\n      \"disabled-password-login-warning\": \"La connexion par mot de passe est désactivée, soyez très prudent lors de la suppression des fournisseurs d'identité\",\n      \"display-name\": \"Nom d'affichage\",\n      \"identifier\": \"Identifiant\",\n      \"identifier-filter\": \"Filtre d'identifiant\",\n      \"no-sso-found\": \"Aucun SSO trouvé.\",\n      \"redirect-url\": \"URL de redirection\",\n      \"scopes\": \"Périmètres\",\n      \"single-sign-on\": \"Configurer l'authentification unique (SSO)\",\n      \"sso-created\": \"SSO {{name}} créé\",\n      \"sso-list\": \"Liste SSO\",\n      \"sso-updated\": \"SSO {{name}} mis à jour\",\n      \"template\": \"Modèle\",\n      \"token-endpoint\": \"Point de terminaison du jeton\",\n      \"update-sso\": \"Mettre à jour SSO\",\n      \"user-endpoint\": \"Point de terminaison utilisateur\",\n      \"label\": \"SSO\"\n    },\n    \"storage\": {\n      \"accesskey\": \"Clé d'accès\",\n      \"accesskey-placeholder\": \"Clé d'accès / ID d'accès\",\n      \"bucket\": \"Bucket\",\n      \"bucket-placeholder\": \"Nom du bucket\",\n      \"create-a-service\": \"Créer un service\",\n      \"create-storage\": \"Créer un stockage\",\n      \"current-storage\": \"Stockage d'objets actuel\",\n      \"delete-storage\": \"Supprimer le stockage\",\n      \"endpoint\": \"Point de terminaison\",\n      \"filepath-template\": \"Modèle de chemin de fichier\",\n      \"local-storage-path\": \"Chemin de stockage local\",\n      \"path\": \"Chemin de stockage\",\n      \"path-description\": \"Vous pouvez utiliser les mêmes variables dynamiques que pour le stockage local, comme {filename}\",\n      \"path-placeholder\": \"chemin/personnalisé\",\n      \"presign-placeholder\": \"URL pré-signée, optionnel\",\n      \"region\": \"Région\",\n      \"region-placeholder\": \"Nom de la région\",\n      \"s3-compatible-url\": \"URL compatible S3\",\n      \"secretkey\": \"Clé secrète\",\n      \"secretkey-placeholder\": \"Clé secrète / Clé d'accès\",\n      \"storage-services\": \"Services de stockage\",\n      \"type-database\": \"Base de données\",\n      \"type-local\": \"Système de fichiers local\",\n      \"update-a-service\": \"Mettre à jour un service\",\n      \"update-local-path\": \"Mettre à jour le chemin local\",\n      \"update-local-path-description\": \"Le chemin de stockage local est un chemin relatif à votre fichier de base de données\",\n      \"update-storage\": \"Mettre à jour le stockage\",\n      \"url-prefix\": \"Préfixe d'URL\",\n      \"url-prefix-placeholder\": \"Préfixe d'URL personnalisé, optionnel\",\n      \"url-suffix\": \"Suffixe d'URL\",\n      \"url-suffix-placeholder\": \"Suffixe d'URL personnalisé, optionnel\",\n      \"warning-text\": \"Êtes-vous sûr de vouloir supprimer le service de stockage \\\"{{name}}\\\" ? CETTE ACTION EST IRRÉVERSIBLE\",\n      \"label\": \"Stockage\"\n    },\n    \"system\": {\n      \"additional-script\": \"Script additionnel\",\n      \"additional-script-placeholder\": \"Code JavaScript additionnel\",\n      \"additional-style\": \"Style additionnel\",\n      \"additional-style-placeholder\": \"Code CSS additionnel\",\n      \"allow-user-signup\": \"Autoriser l'inscription des utilisateurs\",\n      \"customize-server\": {\n        \"description\": \"Description\",\n        \"icon-url\": \"URL de l'icône\",\n        \"locale\": \"Langue du serveur\",\n        \"title\": \"Personnaliser le serveur\"\n      },\n      \"disable-password-login\": \"Désactiver la connexion par mot de passe\",\n      \"disable-password-login-final-warning\": \"Veuillez taper \\\"CONFIRM\\\" si vous savez ce que vous faites.\",\n      \"disable-password-login-warning\": \"Cela désactivera la connexion par mot de passe pour tous les utilisateurs. Il ne sera pas possible de se connecter sans annuler ce paramètre dans la base de données si vos fournisseurs d'identité échouent. Soyez très prudent lors de la suppression d'un fournisseur d'identité\",\n      \"display-with-updated-time\": \"Afficher avec l'heure de mise à jour\",\n      \"enable-auto-compact\": \"Activer la compression automatique\",\n      \"enable-double-click-to-edit\": \"Activer le double-clic pour éditer\",\n      \"enable-password-login\": \"Activer la connexion par mot de passe\",\n      \"enable-password-login-warning\": \"Cela activera la connexion par mot de passe pour tous les utilisateurs. Continuez uniquement si vous souhaitez que les utilisateurs puissent se connecter à la fois avec SSO et mot de passe\",\n      \"max-upload-size\": \"Taille maximale de téléversement (MiB)\",\n      \"max-upload-size-hint\": \"La valeur recommandée est 32 MiB.\",\n      \"removed-completed-task-list-items\": \"Activer la suppression des tâches terminées\",\n      \"server-name\": \"Nom du serveur\",\n      \"title\": \"Général\",\n      \"label\": \"Système\"\n    },\n    \"version\": \"Version\",\n    \"access-token\": {\n      \"access-token-copied-to-clipboard\": \"Jeton d'accès copié dans le presse-papiers\",\n      \"access-token-deleted\": \"Jeton d'accès `{{description}}` supprimé\",\n      \"access-token-deletion\": \"Êtes-vous sûr de vouloir supprimer le jeton d'accès `{{description}}` ?\",\n      \"access-token-deletion-description\": \"Cette action est irréversible. Vous devrez mettre à jour tous les services utilisant ce jeton.\",\n      \"create-dialog\": {\n        \"access-token-created\": \"Jeton d'accès `{{description}}` créé\",\n        \"create-access-token\": \"Créer un jeton d'accès\",\n        \"created-at\": \"Créé le\",\n        \"description\": \"Description\",\n        \"duration-1m\": \"1 mois\",\n        \"duration-8h\": \"8 heures\",\n        \"duration-never\": \"Jamais\",\n        \"expiration\": \"Expiration\",\n        \"expires-at\": \"Expire le\",\n        \"some-description\": \"Une description...\"\n      },\n      \"description\": \"Liste de tous les jetons d'accès de votre compte.\",\n      \"title\": \"Jetons d'accès\",\n      \"token\": \"Jeton\"\n    },\n    \"account\": {\n      \"change-password\": \"Changer le mot de passe\",\n      \"email-note\": \"Optionnel\",\n      \"export-memos\": \"Exporter les notes\",\n      \"nickname-note\": \"Affiché dans la bannière\",\n      \"openapi-reset\": \"Réinitialiser la clé OpenAPI\",\n      \"openapi-sample-post\": \"Bonjour #notes depuis {{url}}\",\n      \"openapi-title\": \"OpenAPI\",\n      \"reset-api\": \"Réinitialiser l'API\",\n      \"title\": \"Informations du compte\",\n      \"update-information\": \"Mettre à jour les informations\",\n      \"username-note\": \"Utilisé pour se connecter\"\n    },\n    \"instance\": {\n      \"disallow-change-nickname\": \"Interdire le changement de surnom\",\n      \"disallow-change-username\": \"Interdire le changement de nom d'utilisateur\",\n      \"disallow-password-auth\": \"Interdire l'authentification par mot de passe\",\n      \"disallow-user-registration\": \"Interdire l'inscription des utilisateurs\",\n      \"monday\": \"Lundi\",\n      \"saturday\": \"Samedi\",\n      \"sunday\": \"Dimanche\",\n      \"week-start-day\": \"Jour de début de semaine\"\n    },\n    \"memo\": {\n      \"content-length-limit\": \"Limite de longueur du contenu (octets)\",\n      \"enable-blur-nsfw-content\": \"Activer le flou pour le contenu sensible (NSFW)\",\n      \"enable-memo-comments\": \"Activer les commentaires sur les notes\",\n      \"enable-memo-location\": \"Activer la localisation des notes\",\n      \"reactions\": \"Réactions\",\n      \"title\": \"Paramètres des notes\",\n      \"label\": \"Note\"\n    },\n    \"webhook\": {\n      \"create-dialog\": {\n        \"an-easy-to-remember-name\": \"Un nom facile à retenir\",\n        \"create-webhook\": \"Créer un webhook\",\n        \"create-webhook-success\": \"Webhook `{{name}}` créé\",\n        \"edit-webhook\": \"Éditer le webhook\",\n        \"payload-url\": \"URL de la charge utile\",\n        \"title\": \"Titre\",\n        \"url-example-post-receive\": \"https://example.com/postreceive\"\n      },\n      \"delete-dialog\": {\n        \"delete-webhook-description\": \"Cette action est irréversible.\",\n        \"delete-webhook-success\": \"Webhook `{{name}}` supprimé avec succès\",\n        \"delete-webhook-title\": \"Êtes-vous sûr de vouloir supprimer le webhook `{{name}}` ?\"\n      },\n      \"no-webhooks-found\": \"Aucun webhook trouvé.\",\n      \"title\": \"Webhooks\",\n      \"url\": \"URL\"\n    }\n  },\n  \"tag\": {\n    \"all-tags\": \"Tous les tags\",\n    \"create-tag\": \"Créer un tag\",\n    \"create-tags-guide\": \"Vous pouvez créer des tags en saisissant `#tag`.\",\n    \"delete-confirm\": \"Êtes-vous sûr de vouloir supprimer ce tag ? Toutes les notes associées seront archivées.\",\n    \"delete-success\": \"Tag supprimé avec succès\",\n    \"delete-tag\": \"Supprimer le tag\",\n    \"new-name\": \"Nouveau nom\",\n    \"no-tag-found\": \"Aucun tag trouvé\",\n    \"old-name\": \"Ancien nom\",\n    \"rename-error-empty\": \"Le nom du tag ne peut pas être vide ou contenir des espaces\",\n    \"rename-error-repeat\": \"Le nouveau nom ne peut pas être identique à l'ancien\",\n    \"rename-success\": \"Tag renommé avec succès\",\n    \"rename-tag\": \"Renommer le tag\",\n    \"rename-tip\": \"Toutes vos notes avec ce tag seront mises à jour.\"\n  },\n  \"tooltip\": {\n    \"link-memo\": \"Lier une note\",\n    \"markdown-menu\": \"Markdown\",\n    \"select-location\": \"Localisation\",\n    \"select-visibility\": \"Visibilité\",\n    \"tags\": \"Tags\",\n    \"upload-attachment\": \"Téléverser des pièces jointes\"\n  }\n}\n"
  },
  {
    "path": "web/src/locales/gl.json",
    "content": "{\n  \"about\": {\n    \"blogs\": \"Blogs\",\n    \"description\": \"Servizo para tomar notas, lixeiro e orientado á privacidade. Prende e comparte as túas grandes ideas.\",\n    \"documents\": \"Documentos\",\n    \"github-repository\": \"Repositorio GitHub\",\n    \"official-website\": \"Páxina web oficial\"\n  },\n  \"auth\": {\n    \"create-your-account\": \"Crea a túa conta\",\n    \"host-tip\": \"Estás creando a conta como Site Host.\",\n    \"new-password\": \"Novo contrasinal\",\n    \"repeat-new-password\": \"Repite o novo contrasinal\",\n    \"sign-in-tip\": \"Xa tes unha conta?\",\n    \"sign-up-tip\": \"Aínda non tes unha conta?\"\n  },\n  \"common\": {\n    \"about\": \"Sobre\",\n    \"add\": \"Engadir\",\n    \"admin\": \"Admin\",\n    \"all\": \"Todo\",\n    \"archive\": \"Arquivo\",\n    \"archived\": \"Arquivada\",\n    \"attachments\": \"Anexos\",\n    \"auto-expand\": \"Depregar Auto\",\n    \"avatar\": \"Avatar\",\n    \"basic\": \"Básico\",\n    \"beta\": \"Beta\",\n    \"calendar\": \"Calendario\",\n    \"cancel\": \"Cancelar\",\n    \"change\": \"Cambiar\",\n    \"clear\": \"Limpar\",\n    \"close\": \"Fechar\",\n    \"collapse\": \"Pregar\",\n    \"confirm\": \"Confirmar\",\n    \"copy\": \"Copiar\",\n    \"create\": \"Crear\",\n    \"created-at\": \"Creada o\",\n    \"database\": \"Base de datos\",\n    \"day\": \"Día\",\n    \"days\": {\n      \"fri\": \"Ven\",\n      \"mon\": \"Lun\",\n      \"sat\": \"Sáb\",\n      \"sun\": \"Dom\",\n      \"thu\": \"Xov\",\n      \"tue\": \"Mar\",\n      \"wed\": \"Mér\"\n    },\n    \"delete\": \"Eliminar\",\n    \"description\": \"Descrición\",\n    \"edit\": \"Editar\",\n    \"email\": \"Correo-e\",\n    \"expand\": \"Despregar\",\n    \"explore\": \"Descubrir\",\n    \"file\": \"Ficheiro\",\n    \"filter\": \"Filtro\",\n    \"home\": \"Inicio\",\n    \"image\": \"Imaxe\",\n    \"in\": \"En\",\n    \"inbox\": \"Caixa de entrada\",\n    \"input\": \"Entrada\",\n    \"language\": \"Idioma\",\n    \"last-updated-at\": \"Última actualización\",\n    \"learn-more\": \"Saber máis\",\n    \"link\": \"Ligar\",\n    \"map\": \"Map\",\n    \"mark\": \"Marcar\",\n    \"memo\": \"Memo\",\n    \"memos\": \"Memos\",\n    \"more\": \"Máis\",\n    \"name\": \"Nome\",\n    \"new\": \"Novidade\",\n    \"nickname\": \"Alcume\",\n    \"null\": \"Null\",\n    \"or\": \"ou\",\n    \"password\": \"Contrasinal\",\n    \"pin\": \"Fixar\",\n    \"pinned\": \"Fixada\",\n    \"preview\": \"Vista previa\",\n    \"profile\": \"Perfil\",\n    \"properties\": \"Propiedades\",\n    \"referenced-by\": \"Referida por\",\n    \"referencing\": \"Referindo\",\n    \"relations\": \"Relacións\",\n    \"remember-me\": \"Lembrarme\",\n    \"rename\": \"Cambiar nome\",\n    \"reset\": \"Restablecer\",\n    \"resources\": \"Recursos\",\n    \"restore\": \"Recuperar\",\n    \"role\": \"Rol\",\n    \"save\": \"Gardar\",\n    \"search\": \"Buscar\",\n    \"select\": \"Seleccionar\",\n    \"settings\": \"Axustes\",\n    \"share\": \"Compartir\",\n    \"shortcut-filter\": \"Filtrar co atallo\",\n    \"shortcuts\": \"Atallos\",\n    \"sign-in\": \"Acceder\",\n    \"sign-in-with\": \"Acceder con {{provider}}\",\n    \"sign-out\": \"Fechar sesión\",\n    \"sign-up\": \"Crear conta\",\n    \"statistics\": \"Estatísticas\",\n    \"tags\": \"Etiquetas\",\n    \"title\": \"Título\",\n    \"today\": \"Hoxe\",\n    \"tree-mode\": \"Modo árbore\",\n    \"type\": \"Tipo\",\n    \"unpin\": \"Soltar\",\n    \"update\": \"Actualizar\",\n    \"upload\": \"Subir\",\n    \"user\": \"Usuaria\",\n    \"username\": \"Identificador\",\n    \"version\": \"Versión\",\n    \"visibility\": \"Visibilidade\",\n    \"yourself\": \"Ti\"\n  },\n  \"editor\": {\n    \"add-your-comment-here\": \"Engade aquí o comentario...\",\n    \"any-thoughts\": \"O que pensas...\",\n    \"exit-focus-mode\": \"Saír do modo sen distraccións\",\n    \"focus-mode\": \"Modo Sen distraccións\",\n    \"no-changes-detected\": \"Non se detectaron cambios\",\n    \"save\": \"Gardar\",\n    \"saving\": \"Gardando...\",\n    \"slash-commands\": \"Escribe `/` para ordes\"\n  },\n  \"inbox\": {\n    \"failed-to-load\": \"Fallou a carga do elemento da caixa de entrada\",\n    \"memo-comment\": \"{{user}} fixo un comentario en {{memo}}.\",\n    \"no-archived\": \"Non hai notificacións arquivadas\",\n    \"no-unread\": \"Non hai notificacións sen ler\",\n    \"unread\": \"Sen ler\"\n  },\n  \"markdown\": {\n    \"checkbox\": \"Marca de selección\",\n    \"code-block\": \"Bloque de código\",\n    \"content-syntax\": \"Sintaxe do contido\"\n  },\n  \"memo\": {\n    \"archived-at\": \"Arquivada o\",\n    \"click-to-hide-nsfw-content\": \"Preme para ocultar contido NSFW\",\n    \"click-to-show-nsfw-content\": \"Preme para mostrar contido NSFW\",\n    \"code\": \"Código\",\n    \"comment\": {\n      \"self\": \"Comentarios\",\n      \"write-a-comment\": \"Escribe un comentario\"\n    },\n    \"copy-content\": \"Copiar contido\",\n    \"copy-link\": \"Copiar ligazón\",\n    \"count-memos-in-date\": \"{{count}} {{memos}} en {{date}}\",\n    \"delete-confirm\": \"Tes certeza de querer eliminar esta memo?\",\n    \"delete-confirm-description\": \"Esta acción non se pode desfacer. Vanse eliminar tamén as ligazóns e referencias.\",\n    \"direction\": \"Dirección\",\n    \"direction-asc\": \"Ascendente\",\n    \"direction-desc\": \"Descendente\",\n    \"display-time\": \"Mostrar hora\",\n    \"filters\": {\n      \"has-code\": \"hasCode\",\n      \"has-link\": \"hasLink\",\n      \"has-task-list\": \"hasTaskList\"\n    },\n    \"links\": \"Ligazóns\",\n    \"load-more\": \"Cargar máis\",\n    \"no-archived-memos\": \"Sen memos arquivadas.\",\n    \"no-memos\": \"Sen memos.\",\n    \"order-by\": \"Orde por\",\n    \"search-placeholder\": \"Buscar memos...\",\n    \"show-less\": \"Mostrar menos\",\n    \"show-more\": \"Mostrar máis\",\n    \"to-do\": \"Tarefa\",\n    \"view-detail\": \"Ver detalle\",\n    \"visibility\": {\n      \"disabled\": \"Están desactivadas as memos públicas\",\n      \"private\": \"Privada\",\n      \"protected\": \"Protexida\",\n      \"public\": \"Pública\"\n    }\n  },\n  \"message\": {\n    \"archived-successfully\": \"Arquivada correctamente\",\n    \"change-memo-created-time\": \"Cambiar hora de creación da memo\",\n    \"copied\": \"Copiada\",\n    \"deleted-successfully\": \"Memo eliminada correctamente\",\n    \"description-is-required\": \"Requírese unha descrición\",\n    \"failed-to-embed-memo\": \"Fallou a inclusión da memo\",\n    \"fill-all\": \"Completa todos os campos.\",\n    \"fill-all-required-fields\": \"Por favor, completa todos os campos requeridos\",\n    \"maximum-upload-size-is\": \"O tamaño máximo permitido para subidas é {{size}} MiB\",\n    \"memo-not-found\": \"Non se atopa a memo.\",\n    \"new-password-not-match\": \"Os novos contrasinais non concordan.\",\n    \"no-data\": \"Non hai datos.\",\n    \"password-changed\": \"Contrasinal cambiado\",\n    \"password-not-match\": \"Non concordan os contrasinais.\",\n    \"restored-successfully\": \"Recuperación correcta\",\n    \"succeed-copy-content\": \"Contido copiado correctamente.\",\n    \"succeed-copy-link\": \"Ligazón copiada correctamente.\",\n    \"update-succeed\": \"Fíxose a actualización\",\n    \"user-not-found\": \"Non se atopa a usuaria\"\n  },\n  \"reference\": {\n    \"add-references\": \"Engadir referencias\",\n    \"embedded-usage\": \"Usar como Contido Incluído\",\n    \"no-memos-found\": \"Non hai memos\",\n    \"search-placeholder\": \"Buscar contido\"\n  },\n  \"resource\": {\n    \"clear\": \"Limpar\",\n    \"copy-link\": \"Copiar ligazón\",\n    \"create-dialog\": {\n      \"external-link\": {\n        \"file-name\": \"Nome do ficheiro\",\n        \"file-name-placeholder\": \"Nome do ficheiro\",\n        \"link\": \"Ligazón\",\n        \"link-placeholder\": \"https://ruta.ao/teu/recurso\",\n        \"option\": \"Ligazón externa\",\n        \"type\": \"Tipo\",\n        \"type-placeholder\": \"\"\n      },\n      \"local-file\": {\n        \"choose\": \"Seleccionar ficheiro…\",\n        \"option\": \"Ficheiro local\"\n      },\n      \"title\": \"Crear recurso\",\n      \"upload-method\": \"Método de subida\"\n    },\n    \"delete-all-unused\": \"Eliminar todos sen usar\",\n    \"delete-all-unused-confirm\": \"Tes certeza de querer eliminar todos os recursos que non estás a utilizar? ESTA ACCIÓN NON É REVERSIBLE\",\n    \"delete-all-unused-error\": \"Fallou a eliminación dos recursos non utilizados\",\n    \"delete-all-unused-success\": \"Recursos eliminados correctamente\",\n    \"delete-resource\": \"Eliminar recurso\",\n    \"delete-selected-resources\": \"Eliminar recursos seleccionados\",\n    \"fetching-data\": \"Obtendo datos…\",\n    \"file-drag-drop-prompt\": \"Arrastra e solta aquí o ficheiro a subir\",\n    \"linked-amount\": \"Número de ligazóns\",\n    \"no-files-selected\": \"Sen ficheiros seleccionados\",\n    \"no-resources\": \"Sen recursos.\",\n    \"no-unused-resources\": \"Sen recursos sen utilizar\",\n    \"reset-link\": \"Restablecer ligazón\",\n    \"reset-link-prompt\": \"Tes certeza de querer restablecer a ligazón? Isto estragará todos os usos actuais da ligazón. ESTA ACCIÓN NON É REVERSIBLE\",\n    \"reset-resource-link\": \"Restablecer ligazón ao recurso\",\n    \"unused-resources\": \"Recursos sen usar\"\n  },\n  \"router\": {\n    \"back-to-top\": \"Volver arriba\",\n    \"go-to-home\": \"Ir ao Inicio\"\n  },\n  \"setting\": {\n    \"member\": {\n      \"admin\": \"Admin\",\n      \"archive-member\": \"Arquivar usuaria\",\n      \"archive-success\": \"{{username}} arquivada correctamente\",\n      \"archive-warning\": \"Tes certeza de querer arquivar a {{username}}?\",\n      \"archive-warning-description\": \"Ao arquivar desactivas a conta. Podes restablecela ou eliminala máis tarde.\",\n      \"create-a-member\": \"Crear usuaria\",\n      \"delete-member\": \"Eliminar usuaria\",\n      \"delete-success\": \"Eliminouse a {{username}} correctamente\",\n      \"delete-warning\": \"Tes certeza de querer eliminar a {{username}}?\",\n      \"delete-warning-description\": \"ESTA ACCIÓN NON É REVERSIBLE\",\n      \"restore-success\": \"{{username}} restablecida correctamente\",\n      \"user\": \"Usuaria\",\n      \"label\": \"Usuaria\",\n      \"list-title\": \"Lista de usuarias\"\n    },\n    \"my-account\": {\n      \"label\": \"A miña conta\"\n    },\n    \"preference\": {\n      \"default-memo-sort-option\": \"Hora da nota mostrada\",\n      \"default-memo-visibility\": \"Visibilidade por defecto\",\n      \"theme\": \"Decorado\",\n      \"label\": \"Preferencias\"\n    },\n    \"shortcut\": {\n      \"delete-confirm\": \"Tes certeza de querer eliminar o atallo `{{title}}`?\",\n      \"delete-success\": \"Eliminouse correctamente o atallo `{{title}}`\"\n    },\n    \"sso\": {\n      \"authorization-endpoint\": \"Punto de acceso para autorización\",\n      \"client-id\": \"ID do cliente\",\n      \"client-secret\": \"Secreto do cliente\",\n      \"confirm-delete\": \"Tes certeza de querer borrar a configuración SSO de `{{name}}`? ESTA ACCIÓN NON É REVERSIBLE\",\n      \"create-sso\": \"Crear SSO\",\n      \"custom\": \"Persoal\",\n      \"delete-sso\": \"Confirma a eliminación\",\n      \"disabled-password-login-warning\": \"Está desactivado o acceso con contrasinal, pon tino ao borrar os provedores de acceso.\",\n      \"display-name\": \"Nome mostrado\",\n      \"identifier\": \"Identificador\",\n      \"identifier-filter\": \"Filtro para o identificador\",\n      \"no-sso-found\": \"Sen SSO.\",\n      \"redirect-url\": \"URL de redirección\",\n      \"scopes\": \"Ámbitos\",\n      \"single-sign-on\": \"Configurando Single Sign-On (SSO) para a autenticación\",\n      \"sso-created\": \"Creouse SSO {{name}}\",\n      \"sso-list\": \"Lista SSO\",\n      \"sso-updated\": \"Actualizouse SSO {{name}}\",\n      \"template\": \"Modelo\",\n      \"token-endpoint\": \"Token do punto de acceso\",\n      \"update-sso\": \"Actualizar SSO\",\n      \"user-endpoint\": \"Punto de acceso da usuaria\",\n      \"label\": \"SSO\"\n    },\n    \"storage\": {\n      \"accesskey\": \"Clave de acceso\",\n      \"accesskey-placeholder\": \"Clave de acceso / ID de acceso\",\n      \"bucket\": \"Bucket\",\n      \"bucket-placeholder\": \"Bucket name\",\n      \"create-a-service\": \"Crear un servizo\",\n      \"create-storage\": \"Crear almacenaxe\",\n      \"current-storage\": \"Almacenaxe actual de obxetos\",\n      \"delete-storage\": \"Eliminar almacenaxe\",\n      \"endpoint\": \"Punto de acceso\",\n      \"filepath-template\": \"Modelo de ruta ao ficheiro\",\n      \"local-storage-path\": \"Ruta local de almacenaxe\",\n      \"path\": \"Ruta de almacenaxe\",\n      \"path-description\": \"Podes usar as mesmas variables dinámicas do almacenaxe local, como {filename}\",\n      \"path-placeholder\": \"persoal/ruta\",\n      \"presign-placeholder\": \"URL pre-acceso, optativo\",\n      \"region\": \"Rexión\",\n      \"region-placeholder\": \"Nome da rexión\",\n      \"s3-compatible-url\": \"URL S3 compatible\",\n      \"secretkey\": \"Clave secreta\",\n      \"secretkey-placeholder\": \"Clave secreta / Clave de acceso\",\n      \"storage-services\": \"Servizos de almacenaxe\",\n      \"type-database\": \"Base de datos\",\n      \"type-local\": \"Sistema local de ficheiros\",\n      \"update-a-service\": \"Actualizar un servizo\",\n      \"update-local-path\": \"Actualizar ruta de almacenaxe local\",\n      \"update-local-path-description\": \"A ruta local de almacenaxe é a ruta relativa ao teu ficheiro de base de datos\",\n      \"update-storage\": \"Actualizar almacenaxe\",\n      \"url-prefix\": \"Prefixo do URL\",\n      \"url-prefix-placeholder\": \"Prefixo personalizado do URL, optativo\",\n      \"url-suffix\": \"Sufixo do URL\",\n      \"url-suffix-placeholder\": \"Sufixo personalizado do URL, optativo\",\n      \"warning-text\": \"Tes certeza de querer eliminar o servizo de almacenaxe `{{name}}`? ESTA ACCIÓN NON É REVERSIBLE.\",\n      \"label\": \"Almacenaxe\"\n    },\n    \"system\": {\n      \"additional-script\": \"Script adicional\",\n      \"additional-script-placeholder\": \"Código JavaScript adicional\",\n      \"additional-style\": \"Estilo adicional\",\n      \"additional-style-placeholder\": \"Código CSS adicional\",\n      \"allow-user-signup\": \"Permitir creación de contas\",\n      \"customize-server\": {\n        \"description\": \"Descrición\",\n        \"icon-url\": \"URL da icona\",\n        \"locale\": \"Idioma do servidor\",\n        \"title\": \"Personalizar servidor\"\n      },\n      \"disable-password-login\": \"Desactivar acceso con contrasinal\",\n      \"disable-password-login-final-warning\": \"Escribe `CONFIRM` se sabes o que estás a facer.\",\n      \"disable-password-login-warning\": \"Isto desactiva o acceso con contrasinal para todas as usuarias. Non é posible acceder sen reverter este axuste na base de datos se os provedores de acceso configurados fallan. Pon moito coidado cando elimines un provedor de identidades\",\n      \"display-with-updated-time\": \"Mostrar coa hora actualizada\",\n      \"enable-auto-compact\": \"Activar auto compatacta\",\n      \"enable-double-click-to-edit\": \"Activar dobre pulsación para editar\",\n      \"enable-password-login\": \"Activar acceso con contrasinal\",\n      \"enable-password-login-warning\": \"Isto vai activar o acceso con contrasinal para todas as usuarias. Continúa só se queres que as usuarias poidan acceder usando tanto SSO como contrasinal\",\n      \"max-upload-size\": \"Tamaño máximo de subida (MiB)\",\n      \"max-upload-size-hint\": \"O valor recomendado é 32 MiB.\",\n      \"removed-completed-task-list-items\": \"Activar a eliminación dos elementos da lista de tarefas completadas\",\n      \"server-name\": \"Nome do servidor\",\n      \"title\": \"Xeral\",\n      \"label\": \"Sistema\"\n    },\n    \"version\": \"Versión\",\n    \"access-token\": {\n      \"access-token-copied-to-clipboard\": \"Token de acceso copiado ao portapapeis\",\n      \"access-token-deleted\": \"Eliminouse o token de acceso `{{description}}`\",\n      \"access-token-deletion\": \"Tes certeza de querer eliminar o toke de acceso `{{description}}`?\",\n      \"access-token-deletion-description\": \"Esta acción non é reversible. Terás que actualizar o token de acceso en todos os servizos que utilicen o actual.\",\n      \"create-dialog\": {\n        \"access-token-created\": \"Creouse o token de acceso `{{description}}`\",\n        \"create-access-token\": \"Crear token de acceso\",\n        \"created-at\": \"Creado o\",\n        \"description\": \"Descrición\",\n        \"duration-1m\": \"1 mes\",\n        \"duration-8h\": \"8 horas\",\n        \"duration-never\": \"Nunca\",\n        \"expiration\": \"Caducidade\",\n        \"expires-at\": \"Caduca o\",\n        \"some-description\": \"Descrición...\"\n      },\n      \"description\": \"Lista con todos os tokens de acceso da túa conta.\",\n      \"title\": \"Tokens de acceso\",\n      \"token\": \"Token\"\n    },\n    \"account\": {\n      \"change-password\": \"Cambiar contrasinal\",\n      \"email-note\": \"Optativo\",\n      \"export-memos\": \"Exportar Memos\",\n      \"nickname-note\": \"Mostrado na cabeceira\",\n      \"openapi-reset\": \"Restablecer clave OpenAPI\",\n      \"openapi-sample-post\": \"Ola #memos desde {{url}}\",\n      \"openapi-title\": \"OpenAPI\",\n      \"reset-api\": \"Restablecer API\",\n      \"title\": \"Información da conta\",\n      \"update-information\": \"Actualizar información\",\n      \"username-note\": \"Utilizada para acceder\"\n    },\n    \"instance\": {\n      \"disallow-change-nickname\": \"Non permitir cambiar o alcume\",\n      \"disallow-change-username\": \"Non permitir cambiar o identificador\",\n      \"disallow-password-auth\": \"Desactivar acceso con contrasinal\",\n      \"disallow-user-registration\": \"Non permitir a creación de contas\",\n      \"monday\": \"Luns\",\n      \"saturday\": \"Sábado\",\n      \"sunday\": \"Domingo\",\n      \"week-start-day\": \"Comezo da semana\"\n    },\n    \"memo\": {\n      \"content-length-limit\": \"Límite de lonxitude do contido (Byte)\",\n      \"enable-blur-nsfw-content\": \"Activar esvaecemento do contido sensible (NSFW)\",\n      \"enable-memo-comments\": \"Activar comentarios nas notas\",\n      \"enable-memo-location\": \"Activar localización nas notas\",\n      \"reactions\": \"Reaccións\",\n      \"title\": \"Axustes relacionados coas notas\",\n      \"label\": \"Memo\"\n    },\n    \"webhook\": {\n      \"create-dialog\": {\n        \"an-easy-to-remember-name\": \"Un nome doado de lembrar\",\n        \"create-webhook\": \"Crear webhook\",\n        \"create-webhook-success\": \"Webhook `{{name}}` creado\",\n        \"edit-webhook\": \"Editar webhook\",\n        \"payload-url\": \"URL de carga\",\n        \"title\": \"Título\",\n        \"url-example-post-receive\": \"https://example.com/postreceive\"\n      },\n      \"delete-dialog\": {\n        \"delete-webhook-description\": \"Esta acción non é reversible.\",\n        \"delete-webhook-success\": \"Eliminouse correctamente o webhook `{{name}}`\",\n        \"delete-webhook-title\": \"Tes certeza de querer eliminar o webhook `{{name}}`?\"\n      },\n      \"no-webhooks-found\": \"Non hai webhooks.\",\n      \"title\": \"Webhooks\",\n      \"url\": \"URL\"\n    }\n  },\n  \"tag\": {\n    \"all-tags\": \"Todas as etiquetas\",\n    \"create-tag\": \"Crear etiqueta\",\n    \"create-tags-guide\": \"Podes crear etiquetas escribindo `#etiqueta`.\",\n    \"delete-confirm\": \"Tes certeza de querer eliminar esta etiqueta? Todas as notas afectadas vanse arquivar.\",\n    \"delete-success\": \"Etiqueta eliminada corretamente\",\n    \"delete-tag\": \"Eliminar etiqueta\",\n    \"new-name\": \"Novo nome\",\n    \"no-tag-found\": \"Sen etiquetas\",\n    \"old-name\": \"Nome antigo\",\n    \"rename-error-empty\": \"O nome da etiqueta non pode quedar baleiro ou conter espazos\",\n    \"rename-error-repeat\": \"O novo nome non pode ser igual ao antigo\",\n    \"rename-success\": \"Cambiouse correctamente o nome da etiqueta\",\n    \"rename-tag\": \"Cambiar nome á etiqueta\",\n    \"rename-tip\": \"Vanse actualizar todas as notas que teñen esta etiqueta.\"\n  },\n  \"tooltip\": {\n    \"link-memo\": \"Ligazón á nota\",\n    \"markdown-menu\": \"Markdown\",\n    \"select-location\": \"Localización\",\n    \"select-visibility\": \"Visibilidade\",\n    \"tags\": \"Etiquetas\",\n    \"upload-attachment\": \"Anexo(s) subido(s)\"\n  }\n}\n"
  },
  {
    "path": "web/src/locales/hi.json",
    "content": "{\n  \"about\": {\n    \"blogs\": \"ब्लॉग्स\",\n    \"description\": \"एक गोपनीयता-प्रथम, हल्की नोट लेने की सेवा। अपनी बेहतरीन सोच को आसानी से कैप्चर और साझा करें।\",\n    \"documents\": \"दस्तावेज़\",\n    \"github-repository\": \"GitHub रिपॉजिटरी\",\n    \"official-website\": \"आधिकारिक वेबसाइट\"\n  },\n  \"auth\": {\n    \"create-your-account\": \"अपना खाता बनाएं\",\n    \"host-tip\": \"आप साइट होस्ट के रूप में पंजीकृत हो रहे हैं।\",\n    \"new-password\": \"नया पासवर्ड\",\n    \"repeat-new-password\": \"नया पासवर्ड दोहराएं\",\n    \"sign-in-tip\": \"क्या आपके पास पहले से खाता है?\",\n    \"sign-up-tip\": \"अभी तक खाता नहीं है?\"\n  },\n  \"common\": {\n    \"about\": \"के बारे में\",\n    \"add\": \"जोड़ें\",\n    \"admin\": \"एडमिन\",\n    \"all\": \"सभी\",\n    \"archive\": \"संग्रहीत करें\",\n    \"archived\": \"संग्रहीत\",\n    \"attachments\": \"संलग्नक\",\n    \"auto-expand\": \"स्वतः विस्तार\",\n    \"avatar\": \"अवतार\",\n    \"basic\": \"मूल\",\n    \"beta\": \"बीटा\",\n    \"calendar\": \"कैलेंडर\",\n    \"cancel\": \"रद्द करें\",\n    \"change\": \"बदलें\",\n    \"clear\": \"साफ़ करें\",\n    \"close\": \"बंद करें\",\n    \"collapse\": \"संकुचित करें\",\n    \"confirm\": \"पुष्टि करें\",\n    \"copy\": \"कॉपी करें\",\n    \"create\": \"बनाएँ\",\n    \"created-at\": \"बनाया गया\",\n    \"database\": \"डेटाबेस\",\n    \"day\": \"दिन\",\n    \"days\": {\n      \"fri\": \"शुक्रवार\",\n      \"mon\": \"सोमवार\",\n      \"sat\": \"शनिवार\",\n      \"sun\": \"रविवार\",\n      \"thu\": \"गुरुवार\",\n      \"tue\": \"मंगलवार\",\n      \"wed\": \"बुधवार\"\n    },\n    \"delete\": \"हटाएँ\",\n    \"description\": \"विवरण\",\n    \"edit\": \"संपादित करें\",\n    \"email\": \"ईमेल\",\n    \"expand\": \"विस्तार करें\",\n    \"explore\": \"अन्वेषण करें\",\n    \"file\": \"फ़ाइल\",\n    \"filter\": \"फ़िल्टर\",\n    \"home\": \"होम\",\n    \"image\": \"तस्वीर\",\n    \"in\": \"में\",\n    \"inbox\": \"इनबॉक्स\",\n    \"input\": \"इनपुट\",\n    \"language\": \"भाषा\",\n    \"last-updated-at\": \"अंतिम अपडेट\",\n    \"learn-more\": \"अधिक जानें\",\n    \"link\": \"लिंक\",\n    \"map\": \"नक्शा\",\n    \"mark\": \"चिह्नित\",\n    \"memo\": \"मेमो\",\n    \"memos\": \"मेमो\",\n    \"more\": \"अधिक\",\n    \"name\": \"नाम\",\n    \"new\": \"नया\",\n    \"nickname\": \"उपनाम\",\n    \"null\": \"शून्य\",\n    \"or\": \"या\",\n    \"password\": \"पासवर्ड\",\n    \"pin\": \"पिन करें\",\n    \"pinned\": \"पिन किया गया\",\n    \"preview\": \"पूर्वावलोकन\",\n    \"profile\": \"प्रोफ़ाइल\",\n    \"properties\": \"गुण\",\n    \"referenced-by\": \"द्वारा संदर्भित\",\n    \"referencing\": \"संदर्भित कर रहा है\",\n    \"relations\": \"संबंध\",\n    \"remember-me\": \"मुझे याद रखें\",\n    \"rename\": \"नाम बदलें\",\n    \"reset\": \"रीसेट करें\",\n    \"resources\": \"संसाधन\",\n    \"restore\": \"पुनर्स्थापित करें\",\n    \"role\": \"भूमिका\",\n    \"save\": \"सहेजें\",\n    \"search\": \"खोजें\",\n    \"select\": \"चयन करें\",\n    \"settings\": \"सेटिंग्स\",\n    \"share\": \"साझा करें\",\n    \"shortcut-filter\": \"शॉर्टकट फ़िल्टर\",\n    \"shortcuts\": \"शॉर्टकट्स\",\n    \"sign-in\": \"साइन इन करें\",\n    \"sign-in-with\": \"{{provider}} के साथ साइन इन करें\",\n    \"sign-out\": \"साइन आउट करें\",\n    \"sign-up\": \"साइन अप करें\",\n    \"statistics\": \"आँकड़े\",\n    \"tags\": \"टैग\",\n    \"title\": \"शीर्षक\",\n    \"today\": \"आज\",\n    \"tree-mode\": \"ट्री मोड\",\n    \"type\": \"प्रकार\",\n    \"unpin\": \"अनपिन करें\",\n    \"update\": \"अपडेट करें\",\n    \"upload\": \"अपलोड करें\",\n    \"user\": \"उपयोगकर्ता\",\n    \"username\": \"उपयोगकर्ता नाम\",\n    \"version\": \"संस्करण\",\n    \"visibility\": \"दृश्यता\",\n    \"yourself\": \"खुद\"\n  },\n  \"editor\": {\n    \"add-your-comment-here\": \"अपनी टिप्पणी यहाँ जोड़ें...\",\n    \"any-thoughts\": \"कोई विचार...\",\n    \"exit-focus-mode\": \"फोकस मोड से बाहर निकलें\",\n    \"focus-mode\": \"फोकस मोड\",\n    \"no-changes-detected\": \"कोई बदलाव नहीं पाया गया\",\n    \"save\": \"सहेजें\",\n    \"saving\": \"सहेज रहा है...\",\n    \"slash-commands\": \"कमांड के लिए `/` टाइप करें\"\n  },\n  \"inbox\": {\n    \"failed-to-load\": \"इनबॉक्स आइटम लोड करने में विफल\",\n    \"memo-comment\": \"{{user}} ने आपकी {{memo}} पर टिप्पणी की है।\",\n    \"no-archived\": \"कोई संग्रहीत सूचनाएँ नहीं\",\n    \"no-unread\": \"कोई अपठित सूचनाएँ नहीं\",\n    \"unread\": \"अपठित\"\n  },\n  \"markdown\": {\n    \"checkbox\": \"चेकबॉक्स\",\n    \"code-block\": \"कोड ब्लॉक\",\n    \"content-syntax\": \"सामग्री सिंटैक्स\"\n  },\n  \"memo\": {\n    \"archived-at\": \"संग्रहीत किया गया\",\n    \"click-to-hide-nsfw-content\": \"संवेदनशील सामग्री छुपाने के लिए क्लिक करें\",\n    \"click-to-show-nsfw-content\": \"संवेदनशील सामग्री दिखाने के लिए क्लिक करें\",\n    \"code\": \"कोड\",\n    \"comment\": {\n      \"self\": \"टिप्पणियाँ\",\n      \"write-a-comment\": \"टिप्पणी लिखें\"\n    },\n    \"copy-content\": \"सामग्री कॉपी करें\",\n    \"copy-link\": \"लिंक कॉपी करें\",\n    \"count-memos-in-date\": \"{{count}} {{memos}} {{date}} को\",\n    \"delete-confirm\": \"क्या आप इस मेमो को हटाने के लिए सुनिश्चित हैं? यह कार्रवाई वापस नहीं की जा सकती।\",\n    \"delete-confirm-description\": \"यह कार्रवाई वापस नहीं की जा सकती। संलग्नक, लिंक और संदर्भ भी हटा दिए जाएंगे।\",\n    \"direction\": \"दिशा\",\n    \"direction-asc\": \"आरोही\",\n    \"direction-desc\": \"अवरोही\",\n    \"display-time\": \"प्रदर्शन समय\",\n    \"filters\": {\n      \"has-code\": \"कोड है\",\n      \"has-link\": \"लिंक है\",\n      \"has-task-list\": \"टास्क लिस्ट है\"\n    },\n    \"links\": \"लिंक\",\n    \"load-more\": \"और लोड करें\",\n    \"no-archived-memos\": \"कोई संग्रहीत मेमो नहीं।\",\n    \"no-memos\": \"कोई मेमो नहीं।\",\n    \"order-by\": \"क्रमबद्ध करें\",\n    \"search-placeholder\": \"मेमो खोजें...\",\n    \"show-less\": \"कम दिखाएँ\",\n    \"show-more\": \"और दिखाएँ\",\n    \"to-do\": \"कार्य\",\n    \"view-detail\": \"विवरण देखें\",\n    \"visibility\": {\n      \"disabled\": \"सार्वजनिक मेमो अक्षम हैं\",\n      \"private\": \"निजी\",\n      \"protected\": \"सदस्य\",\n      \"public\": \"सार्वजनिक\"\n    }\n  },\n  \"message\": {\n    \"archived-successfully\": \"सफलतापूर्वक संग्रहीत किया गया\",\n    \"change-memo-created-time\": \"मेमो बनाने का समय बदलें\",\n    \"copied\": \"कॉपी किया गया\",\n    \"deleted-successfully\": \"सफलतापूर्वक हटाया गया\",\n    \"description-is-required\": \"विवरण आवश्यक है\",\n    \"failed-to-embed-memo\": \"मेमो एम्बेड करने में विफल\",\n    \"fill-all\": \"कृपया सभी फ़ील्ड भरें।\",\n    \"fill-all-required-fields\": \"कृपया सभी आवश्यक फ़ील्ड भरें\",\n    \"maximum-upload-size-is\": \"अधिकतम अपलोड आकार {{size}} MiB है\",\n    \"memo-not-found\": \"मेमो नहीं मिला।\",\n    \"new-password-not-match\": \"नए पासवर्ड मेल नहीं खाते।\",\n    \"no-data\": \"कोई डेटा नहीं मिला।\",\n    \"password-changed\": \"पासवर्ड बदल दिया गया\",\n    \"password-not-match\": \"पासवर्ड मेल नहीं खाते।\",\n    \"restored-successfully\": \"सफलतापूर्वक पुनर्स्थापित किया गया\",\n    \"succeed-copy-content\": \"सामग्री सफलतापूर्वक कॉपी की गई।\",\n    \"succeed-copy-link\": \"लिंक सफलतापूर्वक कॉपी किया गया।\",\n    \"update-succeed\": \"अपडेट सफल हुआ\",\n    \"user-not-found\": \"उपयोगकर्ता नहीं मिला\"\n  },\n  \"reference\": {\n    \"add-references\": \"संदर्भ जोड़ें\",\n    \"embedded-usage\": \"एम्बेडेड सामग्री के रूप में उपयोग करें\",\n    \"no-memos-found\": \"कोई मेमो नहीं मिला\",\n    \"search-placeholder\": \"सामग्री खोजें\"\n  },\n  \"resource\": {\n    \"clear\": \"साफ़ करें\",\n    \"copy-link\": \"लिंक कॉपी करें\",\n    \"create-dialog\": {\n      \"external-link\": {\n        \"file-name\": \"फ़ाइल का नाम\",\n        \"file-name-placeholder\": \"फ़ाइल का नाम\",\n        \"link\": \"लिंक\",\n        \"link-placeholder\": \"https://the.link.to/your/resource\",\n        \"option\": \"बाहरी लिंक\",\n        \"type\": \"प्रकार\",\n        \"type-placeholder\": \"फ़ाइल प्रकार\"\n      },\n      \"local-file\": {\n        \"choose\": \"फ़ाइल चुनें...\",\n        \"option\": \"स्थानीय फ़ाइल\"\n      },\n      \"title\": \"संसाधन बनाएँ\",\n      \"upload-method\": \"अपलोड विधि\"\n    },\n    \"delete-all-unused\": \"सभी अप्रयुक्त हटाएं\",\n    \"delete-all-unused-confirm\": \"क्या आप वाकई सभी अप्रयुक्त संसाधन हटाना चाहते हैं? यह कार्रवाई वापस नहीं की जा सकती\",\n    \"delete-all-unused-error\": \"अप्रयुक्त संसाधन हटाने में विफल\",\n    \"delete-all-unused-success\": \"संसाधन सफलतापूर्वक हटाए गए\",\n    \"delete-resource\": \"संसाधन हटाएँ\",\n    \"delete-selected-resources\": \"चयनित संसाधनों को हटाएँ\",\n    \"fetching-data\": \"डेटा लाया जा रहा है...\",\n    \"file-drag-drop-prompt\": \"अपलोड करने के लिए अपनी फ़ाइल यहाँ खींचें और छोड़ें\",\n    \"linked-amount\": \"लिंक की गई राशि\",\n    \"no-files-selected\": \"कोई फ़ाइल चयनित नहीं है\",\n    \"no-resources\": \"कोई संसाधन नहीं।\",\n    \"no-unused-resources\": \"कोई अप्रयुक्त संसाधन नहीं हैं\",\n    \"reset-link\": \"लिंक रीसेट करें\",\n    \"reset-link-prompt\": \"क्या आप लिंक को रीसेट करना चाहते हैं? इससे सभी मौजूदा लिंक उपयोग टूट जाएंगे। यह कार्रवाई वापस नहीं की जा सकती।\",\n    \"reset-resource-link\": \"संसाधन लिंक रीसेट करें\",\n    \"unused-resources\": \"अप्रयुक्त संसाधन\"\n  },\n  \"router\": {\n    \"back-to-top\": \"ऊपर जाएँ\",\n    \"go-to-home\": \"होम पर जाएँ\"\n  },\n  \"setting\": {\n    \"member\": {\n      \"admin\": \"एडमिन\",\n      \"archive-member\": \"सदस्य को संग्रहित करें\",\n      \"archive-success\": \"{{username}} सफलतापूर्वक संग्रहित किया गया\",\n      \"archive-warning\": \"क्या आप वाकई {{username}} को संग्रहित करना चाहते हैं?\",\n      \"archive-warning-description\": \"संग्रहण खाते को अक्षम करता है। आप इसे बाद में पुनर्स्थापित या हटा सकते हैं।\",\n      \"create-a-member\": \"सदस्य बनाएँ\",\n      \"delete-member\": \"सदस्य हटाएँ\",\n      \"delete-success\": \"{{username}} सफलतापूर्वक हटाया गया\",\n      \"delete-warning\": \"क्या आप वाकई {{username}} को हटाना चाहते हैं?\",\n      \"delete-warning-description\": \"यह कार्रवाई वापस नहीं की जा सकती\",\n      \"restore-success\": \"{{username}} सफलतापूर्वक पुनर्स्थापित किया गया\",\n      \"user\": \"उपयोगकर्ता\",\n      \"label\": \"सदस्य\",\n      \"list-title\": \"सदस्य सूची\"\n    },\n    \"my-account\": {\n      \"label\": \"मेरा खाता\"\n    },\n    \"preference\": {\n      \"default-memo-sort-option\": \"मेमो प्रदर्शन समय\",\n      \"default-memo-visibility\": \"डिफ़ॉल्ट मेमो दृश्यता\",\n      \"theme\": \"थीम\",\n      \"label\": \"प्राथमिकता\"\n    },\n    \"shortcut\": {\n      \"delete-confirm\": \"क्या आप वाकई शॉर्टकट `{{title}}` हटाना चाहते हैं?\",\n      \"delete-success\": \"शॉर्टकट `{{title}}` सफलतापूर्वक हटाया गया\"\n    },\n    \"sso\": {\n      \"authorization-endpoint\": \"प्राधिकरण एंडपॉइंट\",\n      \"client-id\": \"क्लाइंट आईडी\",\n      \"client-secret\": \"क्लाइंट सीक्रेट\",\n      \"confirm-delete\": \"क्या आप वाकई \\\"{{name}}\\\" SSO कॉन्फ़िगरेशन हटाना चाहते हैं? यह कार्रवाई वापस नहीं की जा सकती\",\n      \"create-sso\": \"SSO बनाएँ\",\n      \"custom\": \"कस्टम\",\n      \"delete-sso\": \"हटाना पुष्टि करें\",\n      \"disabled-password-login-warning\": \"पासवर्ड-लॉगिन अक्षम है, पहचान प्रदाताओं को हटाते समय सावधान रहें\",\n      \"display-name\": \"प्रदर्शित नाम\",\n      \"identifier\": \"पहचानकर्ता\",\n      \"identifier-filter\": \"पहचानकर्ता फ़िल्टर\",\n      \"no-sso-found\": \"कोई SSO नहीं मिला।\",\n      \"redirect-url\": \"रीडायरेक्ट URL\",\n      \"scopes\": \"स्कोप्स\",\n      \"single-sign-on\": \"प्रमाणीकरण के लिए सिंगल साइन-ऑन (SSO) कॉन्फ़िगर करें\",\n      \"sso-created\": \"SSO {{name}} बनाया गया\",\n      \"sso-list\": \"SSO सूची\",\n      \"sso-updated\": \"SSO {{name}} अपडेट किया गया\",\n      \"template\": \"टेम्पलेट\",\n      \"token-endpoint\": \"टोकन एंडपॉइंट\",\n      \"update-sso\": \"SSO अपडेट करें\",\n      \"user-endpoint\": \"यूज़र एंडपॉइंट\",\n      \"label\": \"एसएसओ\"\n    },\n    \"storage\": {\n      \"accesskey\": \"एक्सेस कुंजी\",\n      \"accesskey-placeholder\": \"एक्सेस कुंजी / एक्सेस आईडी\",\n      \"bucket\": \"बकेट\",\n      \"bucket-placeholder\": \"बकेट नाम\",\n      \"create-a-service\": \"सेवा बनाएँ\",\n      \"create-storage\": \"संग्रहण बनाएँ\",\n      \"current-storage\": \"वर्तमान ऑब्जेक्ट स्टोरेज\",\n      \"delete-storage\": \"संग्रहण हटाएँ\",\n      \"endpoint\": \"एंडपॉइंट\",\n      \"filepath-template\": \"फ़ाइलपथ टेम्पलेट\",\n      \"local-storage-path\": \"स्थानीय संग्रहण पथ\",\n      \"path\": \"संग्रहण पथ\",\n      \"path-description\": \"आप स्थानीय संग्रहण की वही डायनामिक वेरिएबल्स उपयोग कर सकते हैं, जैसे {filename}\",\n      \"path-placeholder\": \"कस्टम/पथ\",\n      \"presign-placeholder\": \"प्री-साइन URL, वैकल्पिक\",\n      \"region\": \"क्षेत्र\",\n      \"region-placeholder\": \"क्षेत्र का नाम\",\n      \"s3-compatible-url\": \"S3 संगत URL\",\n      \"secretkey\": \"सीक्रेट कुंजी\",\n      \"secretkey-placeholder\": \"सीक्रेट कुंजी / एक्सेस कुंजी\",\n      \"storage-services\": \"संग्रहण सेवाएँ\",\n      \"type-database\": \"डेटाबेस\",\n      \"type-local\": \"स्थानीय फ़ाइल सिस्टम\",\n      \"update-a-service\": \"सेवा अपडेट करें\",\n      \"update-local-path\": \"स्थानीय संग्रहण पथ अपडेट करें\",\n      \"update-local-path-description\": \"स्थानीय संग्रहण पथ आपके डेटाबेस फ़ाइल के लिए एक सापेक्ष पथ है\",\n      \"update-storage\": \"संग्रहण अपडेट करें\",\n      \"url-prefix\": \"URL उपसर्ग\",\n      \"url-prefix-placeholder\": \"कस्टम URL उपसर्ग, वैकल्पिक\",\n      \"url-suffix\": \"URL प्रत्यय\",\n      \"url-suffix-placeholder\": \"कस्टम URL प्रत्यय, वैकल्पिक\",\n      \"warning-text\": \"क्या आप वाकई संग्रहण सेवा \\\"{{name}}\\\" हटाना चाहते हैं? यह कार्रवाई वापस नहीं की जा सकती\",\n      \"label\": \"संग्रहण\"\n    },\n    \"system\": {\n      \"additional-script\": \"अतिरिक्त स्क्रिप्ट\",\n      \"additional-script-placeholder\": \"अतिरिक्त JavaScript कोड\",\n      \"additional-style\": \"अतिरिक्त स्टाइल\",\n      \"additional-style-placeholder\": \"अतिरिक्त CSS कोड\",\n      \"allow-user-signup\": \"यूज़र साइनअप की अनुमति दें\",\n      \"customize-server\": {\n        \"description\": \"विवरण\",\n        \"icon-url\": \"आइकन URL\",\n        \"locale\": \"सर्वर लोकेल\",\n        \"title\": \"सर्वर अनुकूलित करें\"\n      },\n      \"disable-password-login\": \"पासवर्ड लॉगिन अक्षम करें\",\n      \"disable-password-login-final-warning\": \"यदि आप जानते हैं कि आप क्या कर रहे हैं तो कृपया \\\"CONFIRM\\\" टाइप करें।\",\n      \"disable-password-login-warning\": \"यह सभी उपयोगकर्ताओं के लिए पासवर्ड लॉगिन को अक्षम कर देगा। यदि आपके पहचान प्रदाता विफल हो जाते हैं तो डेटाबेस में इस सेटिंग को वापस किए बिना लॉगिन संभव नहीं होगा। पहचान प्रदाता हटाते समय सावधानी बरतें।\",\n      \"display-with-updated-time\": \"अपडेटेड समय के साथ दिखाएँ\",\n      \"enable-auto-compact\": \"ऑटो-कॉम्पैक्ट सक्षम करें\",\n      \"enable-double-click-to-edit\": \"डबल क्लिक से संपादन सक्षम करें\",\n      \"enable-password-login\": \"पासवर्ड लॉगिन सक्षम करें\",\n      \"enable-password-login-warning\": \"यह सभी उपयोगकर्ताओं के लिए पासवर्ड लॉगिन सक्षम करेगा। केवल तभी जारी रखें जब आप चाहते हैं कि उपयोगकर्ता SSO और पासवर्ड दोनों से लॉगिन कर सकें\",\n      \"max-upload-size\": \"अधिकतम अपलोड आकार (MiB)\",\n      \"max-upload-size-hint\": \"अनुशंसित मान 32 MiB है।\",\n      \"removed-completed-task-list-items\": \"पूर्ण कार्य सूची आइटम हटाना सक्षम करें\",\n      \"server-name\": \"सर्वर नाम\",\n      \"title\": \"सामान्य\",\n      \"label\": \"सिस्टम\"\n    },\n    \"version\": \"संस्करण\",\n    \"access-token\": {\n      \"access-token-copied-to-clipboard\": \"एक्सेस टोकन क्लिपबोर्ड में कॉपी किया गया\",\n      \"access-token-deleted\": \"एक्सेस टोकन `{{description}}` हटाया गया\",\n      \"access-token-deletion\": \"क्या आप वाकई एक्सेस टोकन `{{description}}` हटाना चाहते हैं?\",\n      \"access-token-deletion-description\": \"यह कार्रवाई वापस नहीं की जा सकती। आपको इस टोकन का उपयोग करने वाली सभी सेवाओं को एक नया टोकन उपयोग करने के लिए अपडेट करना होगा।\",\n      \"create-dialog\": {\n        \"access-token-created\": \"एक्सेस टोकन `{{description}}` बनाया गया\",\n        \"create-access-token\": \"एक्सेस टोकन बनाएँ\",\n        \"created-at\": \"बनाया गया\",\n        \"description\": \"विवरण\",\n        \"duration-1m\": \"1 महीना\",\n        \"duration-8h\": \"8 घंटे\",\n        \"duration-never\": \"कभी नहीं\",\n        \"expiration\": \"समाप्ति\",\n        \"expires-at\": \"समाप्ति तिथि\",\n        \"some-description\": \"कुछ विवरण...\"\n      },\n      \"description\": \"आपके खाते के सभी एक्सेस टोकन की सूची।\",\n      \"title\": \"एक्सेस टोकन\",\n      \"token\": \"टोकन\"\n    },\n    \"account\": {\n      \"change-password\": \"पासवर्ड बदलें\",\n      \"email-note\": \"वैकल्पिक\",\n      \"export-memos\": \"मेमो निर्यात करें\",\n      \"nickname-note\": \"बैनर में दिखाया गया\",\n      \"openapi-reset\": \"OpenAPI कुंजी रीसेट करें\",\n      \"openapi-sample-post\": \"नमस्ते #memos से {{url}}\",\n      \"openapi-title\": \"OpenAPI\",\n      \"reset-api\": \"API रीसेट करें\",\n      \"title\": \"खाता जानकारी\",\n      \"update-information\": \"जानकारी अपडेट करें\",\n      \"username-note\": \"साइन इन के लिए उपयोग किया जाता है\"\n    },\n    \"instance\": {\n      \"disallow-change-nickname\": \"उपनाम बदलने की अनुमति न दें\",\n      \"disallow-change-username\": \"उपयोगकर्ता नाम बदलने की अनुमति न दें\",\n      \"disallow-password-auth\": \"पासवर्ड प्रमाणीकरण की अनुमति न दें\",\n      \"disallow-user-registration\": \"यूज़र पंजीकरण की अनुमति न दें\",\n      \"monday\": \"सोमवार\",\n      \"saturday\": \"शनिवार\",\n      \"sunday\": \"रविवार\",\n      \"week-start-day\": \"सप्ताह प्रारंभ दिन\"\n    },\n    \"memo\": {\n      \"content-length-limit\": \"सामग्री की अधिकतम लंबाई (बाइट)\",\n      \"enable-blur-nsfw-content\": \"संवेदनशील (NSFW) सामग्री धुंधला करें सक्षम करें\",\n      \"enable-memo-comments\": \"मेमो टिप्पणियाँ सक्षम करें\",\n      \"enable-memo-location\": \"मेमो स्थान सक्षम करें\",\n      \"reactions\": \"प्रतिक्रियाएँ\",\n      \"title\": \"मेमो संबंधित सेटिंग्स\",\n      \"label\": \"मेमो\"\n    },\n    \"webhook\": {\n      \"create-dialog\": {\n        \"an-easy-to-remember-name\": \"आसान नाम\",\n        \"create-webhook\": \"Webhook बनाएँ\",\n        \"create-webhook-success\": \"Webhook `{{name}}` सफलतापूर्वक बनाया गया\",\n        \"edit-webhook\": \"Webhook संपादित करें\",\n        \"payload-url\": \"Payload URL\",\n        \"title\": \"शीर्षक\",\n        \"url-example-post-receive\": \"https://example.com/postreceive\"\n      },\n      \"delete-dialog\": {\n        \"delete-webhook-description\": \"यह कार्रवाई वापस नहीं की जा सकती।\",\n        \"delete-webhook-success\": \"Webhook `{{name}}` सफलतापूर्वक हटाया गया\",\n        \"delete-webhook-title\": \"क्या आप वाकई webhook `{{name}}` हटाना चाहते हैं?\"\n      },\n      \"no-webhooks-found\": \"कोई वेबहुक नहीं मिला।\",\n      \"title\": \"Webhooks\",\n      \"url\": \"URL\"\n    }\n  },\n  \"tag\": {\n    \"all-tags\": \"सभी टैग\",\n    \"create-tag\": \"टैग बनाएँ\",\n    \"create-tags-guide\": \"आप `#tag` दर्ज करके टैग बना सकते हैं।\",\n    \"delete-confirm\": \"क्या आप वाकई इस टैग को हटाना चाहते हैं? सभी संबंधित मेमो संग्रहीत हो जाएंगे।\",\n    \"delete-success\": \"टैग सफलतापूर्वक हटाया गया\",\n    \"delete-tag\": \"टैग हटाएँ\",\n    \"new-name\": \"नया नाम\",\n    \"no-tag-found\": \"कोई टैग नहीं मिला\",\n    \"old-name\": \"पुराना नाम\",\n    \"rename-error-empty\": \"टैग नाम खाली नहीं हो सकता या उसमें स्पेस नहीं हो सकता\",\n    \"rename-error-repeat\": \"नया नाम पुराने नाम के समान नहीं हो सकता\",\n    \"rename-success\": \"टैग सफलतापूर्वक नाम बदला गया\",\n    \"rename-tag\": \"टैग का नाम बदलें\",\n    \"rename-tip\": \"आपके सभी मेमो इस टैग के साथ अपडेट किए जाएंगे।\"\n  },\n  \"tooltip\": {\n    \"link-memo\": \"मेमो लिंक करें\",\n    \"markdown-menu\": \"Markdown\",\n    \"select-location\": \"स्थान चुनें\",\n    \"select-visibility\": \"दृश्यता चुनें\",\n    \"tags\": \"टैग\",\n    \"upload-attachment\": \"संलग्नक अपलोड करें\"\n  }\n}\n"
  },
  {
    "path": "web/src/locales/hr.json",
    "content": "{\n  \"about\": {\n    \"blogs\": \"Blogovi\",\n    \"description\": \"Lagana usluga za bilješke s naglaskom na privatnost. Jednostavno zabilježite i podijelite svoje sjajne misli.\",\n    \"documents\": \"Dokumenti\",\n    \"github-repository\": \"GitHub repozitorij\",\n    \"official-website\": \"Službena stranica\"\n  },\n  \"auth\": {\n    \"create-your-account\": \"Stvori svoj račun\",\n    \"host-tip\": \"Registriraš se kao Vlasnik sajta.\",\n    \"new-password\": \"Nova lozinka\",\n    \"repeat-new-password\": \"Ponovi novu lozinku\",\n    \"sign-in-tip\": \"Račun već postoji?\",\n    \"sign-up-tip\": \"Još nemaš račun?\"\n  },\n  \"common\": {\n    \"about\": \"O aplikaciji\",\n    \"add\": \"Dodaj\",\n    \"admin\": \"Admin\",\n    \"all\": \"Svi\",\n    \"archive\": \"Arhiviraj\",\n    \"archived\": \"Arhivirano\",\n    \"attachments\": \"Privici\",\n    \"auto-expand\": \"Automatski proširi\",\n    \"avatar\": \"Avatar\",\n    \"basic\": \"Osnovno\",\n    \"beta\": \"Beta\",\n    \"calendar\": \"Kalendar\",\n    \"cancel\": \"Otkaži\",\n    \"change\": \"Promijeni\",\n    \"clear\": \"Očisti\",\n    \"close\": \"Zatvori\",\n    \"collapse\": \"Sažmi\",\n    \"confirm\": \"Potvrdi\",\n    \"copy\": \"Kopiraj\",\n    \"create\": \"Stvori\",\n    \"created-at\": \"Stvoreno\",\n    \"database\": \"Baza podataka\",\n    \"day\": \"Dan\",\n    \"days\": {\n      \"fri\": \"Pet\",\n      \"mon\": \"Pon\",\n      \"sat\": \"Sub\",\n      \"sun\": \"Ned\",\n      \"thu\": \"Čet\",\n      \"tue\": \"Uto\",\n      \"wed\": \"Sri\"\n    },\n    \"delete\": \"Obriši\",\n    \"description\": \"Opis\",\n    \"edit\": \"Uredi\",\n    \"email\": \"Email\",\n    \"expand\": \"Proširi\",\n    \"explore\": \"Istraži\",\n    \"file\": \"Datoteka\",\n    \"filter\": \"Filtriraj\",\n    \"home\": \"Glavna\",\n    \"image\": \"Slika\",\n    \"in\": \"U\",\n    \"inbox\": \"Ulazna pošta\",\n    \"input\": \"Unos\",\n    \"language\": \"Jezik\",\n    \"last-updated-at\": \"Zadnje ažurirano\",\n    \"learn-more\": \"Nauči više\",\n    \"link\": \"Link\",\n    \"map\": \"Karta\",\n    \"mark\": \"Označi\",\n    \"memo\": \"Memo\",\n    \"memos\": \"Memos\",\n    \"more\": \"Više\",\n    \"name\": \"Ime\",\n    \"new\": \"Novi\",\n    \"nickname\": \"Nadimak\",\n    \"null\": \"Ništa\",\n    \"or\": \"ili\",\n    \"password\": \"Lozinka\",\n    \"pin\": \"Prikvači\",\n    \"pinned\": \"Prikvačeno\",\n    \"preview\": \"Pregled\",\n    \"profile\": \"Profil\",\n    \"properties\": \"Svojstva\",\n    \"referenced-by\": \"Referencirano od\",\n    \"referencing\": \"Referencira\",\n    \"relations\": \"Odnosi\",\n    \"remember-me\": \"Zapamti me\",\n    \"rename\": \"Preimenuj\",\n    \"reset\": \"Resetiraj\",\n    \"resources\": \"Resursi\",\n    \"restore\": \"Vrati\",\n    \"role\": \"Uloga\",\n    \"save\": \"Sačuvaj\",\n    \"search\": \"Pretraži\",\n    \"select\": \"Odaberi\",\n    \"settings\": \"Postavke\",\n    \"share\": \"Podijeli\",\n    \"shortcut-filter\": \"Filter prečaca\",\n    \"shortcuts\": \"Prečaci\",\n    \"sign-in\": \"Prijavi se\",\n    \"sign-in-with\": \"Prijavi se s {{provider}}\",\n    \"sign-out\": \"Odjavi se\",\n    \"sign-up\": \"Registriraj se\",\n    \"statistics\": \"Statistika\",\n    \"tags\": \"Tagovi\",\n    \"title\": \"Naslov\",\n    \"today\": \"Danas\",\n    \"tree-mode\": \"Način stabla\",\n    \"type\": \"Tip\",\n    \"unpin\": \"Makni pin\",\n    \"update\": \"Ažuriraj\",\n    \"upload\": \"Prenesi\",\n    \"user\": \"Korisnik\",\n    \"username\": \"Korisničko ime\",\n    \"version\": \"Verzija\",\n    \"visibility\": \"Vidljivost\",\n    \"yourself\": \"Ti\"\n  },\n  \"editor\": {\n    \"add-your-comment-here\": \"Dodaj svoj komentar ovdje...\",\n    \"any-thoughts\": \"Imaš li misli...\",\n    \"exit-focus-mode\": \"Izlazi iz načina rada za fokus\",\n    \"focus-mode\": \"Način rada za fokus\",\n    \"no-changes-detected\": \"Nema promjena\",\n    \"save\": \"Sačuvaj\",\n    \"saving\": \"Spremanje...\",\n    \"slash-commands\": \"Upisi `/` za naredbe\"\n  },\n  \"inbox\": {\n    \"failed-to-load\": \"Neuspjelo učitavanje stavke pristigle pošte\",\n    \"memo-comment\": \"{{user}} je komentirao tvoj {{memo}}.\",\n    \"no-archived\": \"Nema arhiviranih obavijesti\",\n    \"no-unread\": \"Nema nepročitanih obavijesti\",\n    \"unread\": \"Nepročitano\"\n  },\n  \"markdown\": {\n    \"checkbox\": \"Potvrdni okvir\",\n    \"code-block\": \"Blok koda\",\n    \"content-syntax\": \"Sintaksa sadržaja\"\n  },\n  \"memo\": {\n    \"archived-at\": \"Arhivirano u\",\n    \"click-to-hide-nsfw-content\": \"Klikni za skrivanje osjetljivog sadržaja\",\n    \"click-to-show-nsfw-content\": \"Klikni za prikaz osjetljivog sadržaja\",\n    \"code\": \"Kod\",\n    \"comment\": {\n      \"self\": \"Komentari\",\n      \"write-a-comment\": \"Napiši komentar\"\n    },\n    \"copy-content\": \"Kopiraj sadržaj\",\n    \"copy-link\": \"Kopiraj link\",\n    \"count-memos-in-date\": \"{{count}} {{memos}} na {{date}}\",\n    \"delete-confirm\": \"Jesi li siguran da želiš obrisati ovaj memo? OVA AKCIJA JE NEPOVRATNA\",\n    \"delete-confirm-description\": \"Ova akcija je nepovratna. Privici, linkovi i reference će također biti uklonjeni.\",\n    \"direction\": \"Smjer\",\n    \"direction-asc\": \"Uzlazno\",\n    \"direction-desc\": \"Silazno\",\n    \"display-time\": \"Vrijeme prikaza\",\n    \"filters\": {\n      \"has-code\": \"imaKod\",\n      \"has-link\": \"imaLink\",\n      \"has-task-list\": \"imaZadatke\"\n    },\n    \"links\": \"Linkovi\",\n    \"load-more\": \"Učitaj više\",\n    \"no-archived-memos\": \"Nema arhiviranih memoa.\",\n    \"no-memos\": \"Nema memoa.\",\n    \"order-by\": \"Sortiraj po\",\n    \"search-placeholder\": \"Pretraži memoe...\",\n    \"show-less\": \"Prikaži manje\",\n    \"show-more\": \"Prikaži više\",\n    \"to-do\": \"Zadaci\",\n    \"view-detail\": \"Vidi detalje\",\n    \"visibility\": {\n      \"disabled\": \"Javni memoi su onemogućeni\",\n      \"private\": \"Privatno\",\n      \"protected\": \"Za članove\",\n      \"public\": \"Javno\"\n    }\n  },\n  \"message\": {\n    \"archived-successfully\": \"Uspješno arhivirano\",\n    \"change-memo-created-time\": \"Promijeni vrijeme stvaranja memoa\",\n    \"copied\": \"Kopirano\",\n    \"deleted-successfully\": \"Uspješno obrisano\",\n    \"description-is-required\": \"Opis je obavezan\",\n    \"failed-to-embed-memo\": \"Neuspjelo umetanje memoa\",\n    \"fill-all\": \"Molimo, popuni sva polja.\",\n    \"fill-all-required-fields\": \"Molimo, popuni sva obavezna polja\",\n    \"maximum-upload-size-is\": \"Maksimalna dopuštena veličina prijenosa je {{size}} MiB\",\n    \"memo-not-found\": \"Memo nije pronađen.\",\n    \"new-password-not-match\": \"Nove lozinke se ne podudaraju.\",\n    \"no-data\": \"Nema podataka.\",\n    \"password-changed\": \"Lozinka je promijenjena\",\n    \"password-not-match\": \"Lozinke se ne podudaraju.\",\n    \"restored-successfully\": \"Uspješno vraćeno\",\n    \"succeed-copy-content\": \"Sadržaj uspješno kopiran.\",\n    \"succeed-copy-link\": \"Link je uspješno kopiran.\",\n    \"update-succeed\": \"Ažuriranje uspješno\",\n    \"user-not-found\": \"Korisnik nije pronađen\"\n  },\n  \"reference\": {\n    \"add-references\": \"Dodaj reference\",\n    \"embedded-usage\": \"Koristi kao ugrađeni sadržaj\",\n    \"no-memos-found\": \"Nema pronađenih memoa\",\n    \"search-placeholder\": \"Pretraži sadržaj\"\n  },\n  \"resource\": {\n    \"clear\": \"Očisti\",\n    \"copy-link\": \"Kopiraj link\",\n    \"create-dialog\": {\n      \"external-link\": {\n        \"file-name\": \"Naziv datoteke\",\n        \"file-name-placeholder\": \"Naziv datoteke\",\n        \"link\": \"Link\",\n        \"link-placeholder\": \"https://link.do/tvog/resursa\",\n        \"option\": \"Vanjski link\",\n        \"type\": \"Tip\",\n        \"type-placeholder\": \"Tip datoteke\"\n      },\n      \"local-file\": {\n        \"choose\": \"Odaberi datoteku...\",\n        \"option\": \"Lokalna datoteka\"\n      },\n      \"title\": \"Stvori resurs\",\n      \"upload-method\": \"Način prijenosa\"\n    },\n    \"delete-all-unused\": \"Obriši sve neiskorištene\",\n    \"delete-all-unused-confirm\": \"Jesi li siguran da želiš obrisati sve neiskorištene resurse? OVA AKCIJA JE NEPOVRATNA\",\n    \"delete-all-unused-error\": \"Neuspjelo brisanje neiskorištenih resursa\",\n    \"delete-all-unused-success\": \"Resursi uspješno obrisani\",\n    \"delete-resource\": \"Obriši resurs\",\n    \"delete-selected-resources\": \"Obriši odabrane resurse\",\n    \"fetching-data\": \"Dohvaćanje podataka...\",\n    \"file-drag-drop-prompt\": \"Povuci i ispusti svoju datoteku ovdje za prijenos\",\n    \"linked-amount\": \"Broj povezanih\",\n    \"no-files-selected\": \"Nema odabranih datoteka\",\n    \"no-resources\": \"Nema resursa.\",\n    \"no-unused-resources\": \"Nema neiskorištenih resursa\",\n    \"reset-link\": \"Resetiraj link\",\n    \"reset-link-prompt\": \"Jesi li siguran da želiš resetirati link? Ovo će prekinuti sve trenutne upotrebe linka. OVA AKCIJA JE NEPOVRATNA\",\n    \"reset-resource-link\": \"Resetiraj link resursa\",\n    \"unused-resources\": \"Neiskorišteni resursi\"\n  },\n  \"router\": {\n    \"back-to-top\": \"Na vrh\",\n    \"go-to-home\": \"Idi na početnu\"\n  },\n  \"setting\": {\n    \"member\": {\n      \"admin\": \"Admin\",\n      \"archive-member\": \"Arhiviraj člana\",\n      \"archive-success\": \"{{username}} uspješno arhiviran\",\n      \"archive-warning\": \"Jesi li siguran da želiš arhivirati {{username}}?\",\n      \"archive-warning-description\": \"Arhiviranje onemogućuje račun. Možeš ga kasnije vratiti ili obrisati.\",\n      \"create-a-member\": \"Stvori člana\",\n      \"delete-member\": \"Obriši člana\",\n      \"delete-success\": \"{{username}} uspješno obrisan\",\n      \"delete-warning\": \"Jesi li siguran da želiš obrisati {{username}}?\",\n      \"delete-warning-description\": \"OVA AKCIJA JE NEPOVRATNA\",\n      \"restore-success\": \"{{username}} uspješno vraćen\",\n      \"user\": \"Korisnik\",\n      \"label\": \"Član\",\n      \"list-title\": \"Popis članova\"\n    },\n    \"my-account\": {\n      \"label\": \"Moj račun\"\n    },\n    \"preference\": {\n      \"default-memo-sort-option\": \"Vrijeme prikaza memoa\",\n      \"default-memo-visibility\": \"Zadana vidljivost memoa\",\n      \"theme\": \"Tema\",\n      \"label\": \"Postavke\"\n    },\n    \"shortcut\": {\n      \"delete-confirm\": \"Jesi li siguran da želiš obrisati prečac `{{title}}`?\",\n      \"delete-success\": \"Prečac `{{title}}` uspješno obrisan\"\n    },\n    \"sso\": {\n      \"authorization-endpoint\": \"Krajnja točka autorizacije\",\n      \"client-id\": \"ID klijenta\",\n      \"client-secret\": \"Tajna klijenta\",\n      \"confirm-delete\": \"Jesi li siguran da želiš obrisati SSO konfiguraciju \\\"{{name}}\\\"? OVA AKCIJA JE NEPOVRATNA\",\n      \"create-sso\": \"Stvori SSO\",\n      \"custom\": \"Prilagođeno\",\n      \"delete-sso\": \"Potvrdi brisanje\",\n      \"disabled-password-login-warning\": \"Prijava lozinkom je onemogućena, budi posebno pažljiv kad uklanjaš pružatelje identiteta\",\n      \"display-name\": \"Prikazano ime\",\n      \"identifier\": \"Identifikator\",\n      \"identifier-filter\": \"Filter identifikatora\",\n      \"no-sso-found\": \"Nema pronađenih SSO.\",\n      \"redirect-url\": \"URL preusmjeravanja\",\n      \"scopes\": \"Opseg\",\n      \"single-sign-on\": \"Konfiguracija Single Sign-On (SSO) za autentikaciju\",\n      \"sso-created\": \"SSO {{name}} je stvoren\",\n      \"sso-list\": \"SSO popis\",\n      \"sso-updated\": \"SSO {{name}} ažuriran\",\n      \"template\": \"Predložak\",\n      \"token-endpoint\": \"Krajnja točka tokena\",\n      \"update-sso\": \"Ažuriraj SSO\",\n      \"user-endpoint\": \"Krajnja točka korisnika\",\n      \"label\": \"SSO\"\n    },\n    \"storage\": {\n      \"accesskey\": \"Pristupni ključ\",\n      \"accesskey-placeholder\": \"Pristupni ključ / ID pristupa\",\n      \"bucket\": \"Bucket\",\n      \"bucket-placeholder\": \"Naziv bucketa\",\n      \"create-a-service\": \"Stvori uslugu\",\n      \"create-storage\": \"Stvori skladište\",\n      \"current-storage\": \"Trenutno skladište objekata\",\n      \"delete-storage\": \"Obriši skladište\",\n      \"endpoint\": \"Krajnja točka\",\n      \"filepath-template\": \"Predložak putanje datoteke\",\n      \"local-storage-path\": \"Putanja lokalnog skladišta\",\n      \"path\": \"Putanja skladišta\",\n      \"path-description\": \"Možeš koristiti iste dinamičke varijable kao i lokalno skladište, npr. {filename}\",\n      \"path-placeholder\": \"prilagođena/putanja\",\n      \"presign-placeholder\": \"Pre-sign URL, opcionalno\",\n      \"region\": \"Regija\",\n      \"region-placeholder\": \"Naziv regije\",\n      \"s3-compatible-url\": \"S3 kompatibilan URL\",\n      \"secretkey\": \"Tajni ključ\",\n      \"secretkey-placeholder\": \"Tajni ključ / Pristupni ključ\",\n      \"storage-services\": \"Usluge skladišta\",\n      \"type-database\": \"Baza podataka\",\n      \"type-local\": \"Lokalni datotečni sustav\",\n      \"update-a-service\": \"Ažuriraj uslugu\",\n      \"update-local-path\": \"Ažuriraj lokalnu putanju\",\n      \"update-local-path-description\": \"Putanja lokalnog skladišta je relativna putanja do vaše datoteke baze podataka\",\n      \"update-storage\": \"Ažuriraj skladište\",\n      \"url-prefix\": \"URL prefiks\",\n      \"url-prefix-placeholder\": \"Prilagođeni URL prefiks, opcionalno\",\n      \"url-suffix\": \"URL sufiks\",\n      \"url-suffix-placeholder\": \"Prilagođeni URL sufiks, opcionalno\",\n      \"warning-text\": \"Jesi li siguran da želiš obrisati skladišnu uslugu \\\"{{name}}\\\"? OVA AKCIJA JE NEPOVRATNA\",\n      \"label\": \"Skladište\"\n    },\n    \"system\": {\n      \"additional-script\": \"Dodatna skripta\",\n      \"additional-script-placeholder\": \"Dodatni JavaScript kod\",\n      \"additional-style\": \"Dodatni stil\",\n      \"additional-style-placeholder\": \"Dodatni CSS kod\",\n      \"allow-user-signup\": \"Dopusti registraciju korisnika\",\n      \"customize-server\": {\n        \"description\": \"Opis\",\n        \"icon-url\": \"URL ikone\",\n        \"locale\": \"Lokalizacija servera\",\n        \"title\": \"Prilagodi server\"\n      },\n      \"disable-password-login\": \"Onemogući prijavu lozinkom\",\n      \"disable-password-login-final-warning\": \"Upiši \\\"CONFIRM\\\" ako znaš što radiš.\",\n      \"disable-password-login-warning\": \"Ovo će onemogućiti prijavu lozinkom za sve korisnike. Nije moguće prijaviti se bez vraćanja ove postavke u bazi podataka ako vaši pružatelji identiteta zakažu. Budite posebno oprezni pri uklanjanju pružatelja identiteta.\",\n      \"display-with-updated-time\": \"Prikaži s ažuriranim vremenom\",\n      \"enable-auto-compact\": \"Omogući automatsko sažimanje\",\n      \"enable-double-click-to-edit\": \"Omogući dvostruki klik za uređivanje\",\n      \"enable-password-login\": \"Omogući prijavu lozinkom\",\n      \"enable-password-login-warning\": \"Ovo će omogućiti prijavu lozinkom za sve korisnike. Nastavite samo ako želite da se korisnici mogu prijaviti i putem SSO i lozinke\",\n      \"max-upload-size\": \"Maksimalna veličina prijenosa (MiB)\",\n      \"max-upload-size-hint\": \"Preporučena vrijednost je 32 MiB.\",\n      \"removed-completed-task-list-items\": \"Omogući uklanjanje završenih zadataka\",\n      \"server-name\": \"Naziv servera\",\n      \"title\": \"Općenito\",\n      \"label\": \"Sustav\"\n    },\n    \"version\": \"Verzija\",\n    \"access-token\": {\n      \"access-token-copied-to-clipboard\": \"Token pristupa kopiran u međuspremnik\",\n      \"access-token-deleted\": \"Token pristupa `{{description}}` obrisan\",\n      \"access-token-deletion\": \"Jesi li siguran da želiš obrisati token pristupa `{{description}}`?\",\n      \"access-token-deletion-description\": \"Ova akcija je nepovratna. Morat ćeš ažurirati sve usluge koje koriste ovaj token da koriste novi token.\",\n      \"create-dialog\": {\n        \"access-token-created\": \"Token pristupa `{{description}}` stvoren\",\n        \"create-access-token\": \"Stvori token pristupa\",\n        \"created-at\": \"Stvoreno\",\n        \"description\": \"Opis\",\n        \"duration-1m\": \"1 mjesec\",\n        \"duration-8h\": \"8 sati\",\n        \"duration-never\": \"Nikada\",\n        \"expiration\": \"Istječe\",\n        \"expires-at\": \"Istječe na\",\n        \"some-description\": \"Neki opis...\"\n      },\n      \"description\": \"Popis svih tokena pristupa za tvoj račun.\",\n      \"title\": \"Tokeni pristupa\",\n      \"token\": \"Token\"\n    },\n    \"account\": {\n      \"change-password\": \"Promijeni lozinku\",\n      \"email-note\": \"Neobavezno\",\n      \"export-memos\": \"Izvezi memoe\",\n      \"nickname-note\": \"Prikazano u banneru\",\n      \"openapi-reset\": \"Resetiraj OpenAPI ključ\",\n      \"openapi-sample-post\": \"Pozdrav #memos od {{url}}\",\n      \"openapi-title\": \"OpenAPI\",\n      \"reset-api\": \"Resetiraj API\",\n      \"title\": \"Informacije o računu\",\n      \"update-information\": \"Ažuriraj informacije\",\n      \"username-note\": \"Koristi se za prijavu\"\n    },\n    \"instance\": {\n      \"disallow-change-nickname\": \"Onemogući promjenu nadimka\",\n      \"disallow-change-username\": \"Onemogući promjenu korisničkog imena\",\n      \"disallow-password-auth\": \"Onemogući autentikaciju lozinkom\",\n      \"disallow-user-registration\": \"Onemogući registraciju korisnika\",\n      \"monday\": \"Ponedjeljak\",\n      \"saturday\": \"Subota\",\n      \"sunday\": \"Nedjelja\",\n      \"week-start-day\": \"Dan početka tjedna\"\n    },\n    \"memo\": {\n      \"content-length-limit\": \"Ograničenje duljine sadržaja (Bajt)\",\n      \"enable-blur-nsfw-content\": \"Omogući zamućenje osjetljivog sadržaja (NSFW)\",\n      \"enable-memo-comments\": \"Omogući komentare na memoima\",\n      \"enable-memo-location\": \"Omogući lokaciju memoa\",\n      \"reactions\": \"Reakcije\",\n      \"title\": \"Postavke vezane uz memo\",\n      \"label\": \"Memo\"\n    },\n    \"webhook\": {\n      \"create-dialog\": {\n        \"an-easy-to-remember-name\": \"Jednostavno ime za pamćenje\",\n        \"create-webhook\": \"Stvori webhook\",\n        \"create-webhook-success\": \"Webhook `{{name}}` stvoren\",\n        \"edit-webhook\": \"Uredi webhook\",\n        \"payload-url\": \"Payload URL\",\n        \"title\": \"Naslov\",\n        \"url-example-post-receive\": \"https://example.com/postreceive\"\n      },\n      \"delete-dialog\": {\n        \"delete-webhook-description\": \"Ova akcija je nepovratna.\",\n        \"delete-webhook-success\": \"Webhook `{{name}}` uspješno obrisan\",\n        \"delete-webhook-title\": \"Jesi li siguran da želiš obrisati webhook `{{name}}`?\"\n      },\n      \"no-webhooks-found\": \"Nema pronađenih webhooks.\",\n      \"title\": \"Webhooks\",\n      \"url\": \"URL\"\n    }\n  },\n  \"tag\": {\n    \"all-tags\": \"Svi tagovi\",\n    \"create-tag\": \"Stvori tag\",\n    \"create-tags-guide\": \"Možete stvoriti tagove unosom `#tag`.\",\n    \"delete-confirm\": \"Jesi li siguran da želiš obrisati ovaj tag? Svi povezani memoi će biti arhivirani.\",\n    \"delete-success\": \"Tag uspješno obrisan\",\n    \"delete-tag\": \"Obriši tag\",\n    \"new-name\": \"Novo ime\",\n    \"no-tag-found\": \"Nema pronađenih tagova\",\n    \"old-name\": \"Staro ime\",\n    \"rename-error-empty\": \"Ime taga ne može biti prazno ili sadržavati razmake\",\n    \"rename-error-repeat\": \"Novo ime ne može biti isto kao staro ime\",\n    \"rename-success\": \"Tag uspješno preimenovan\",\n    \"rename-tag\": \"Preimenuj tag\",\n    \"rename-tip\": \"Svi vaši memoi s ovim tagom će biti ažurirani.\"\n  },\n  \"tooltip\": {\n    \"link-memo\": \"Linkaj memo\",\n    \"markdown-menu\": \"Markdown\",\n    \"select-location\": \"Lokacija\",\n    \"select-visibility\": \"Vidljivost\",\n    \"tags\": \"Tagovi\",\n    \"upload-attachment\": \"Prenesi privitak/e\"\n  }\n}\n"
  },
  {
    "path": "web/src/locales/hu.json",
    "content": "{\n  \"about\": {\n    \"blogs\": \"Blogok\",\n    \"description\": \"Egy adatvédelmi elsőbbségű, könnyű jegyzetelési szolgáltatás. Rögzítsd és oszd meg nagyszerű gondolataidat egyszerűen.\",\n    \"documents\": \"Dokumentumok\",\n    \"github-repository\": \"GitHub repó\",\n    \"official-website\": \"Hivatalos weboldal\"\n  },\n  \"auth\": {\n    \"create-your-account\": \"Fiók létrehozása\",\n    \"host-tip\": \"A weboldal rendszergazdájaként regisztrál.\",\n    \"new-password\": \"Új jelszó\",\n    \"repeat-new-password\": \"Jelszó ismét\",\n    \"sign-in-tip\": \"Már van fiókod?\",\n    \"sign-up-tip\": \"Még nincs fiókod?\"\n  },\n  \"common\": {\n    \"about\": \"Névjegy\",\n    \"add\": \"Hozzáadás\",\n    \"admin\": \"Admin\",\n    \"all\": \"Összes\",\n    \"archive\": \"Archiválás\",\n    \"archived\": \"Archívum\",\n    \"attachments\": \"Mellékletek\",\n    \"auto-expand\": \"Automatikus kinyitás\",\n    \"avatar\": \"Avatar\",\n    \"basic\": \"Általános\",\n    \"beta\": \"Béta\",\n    \"calendar\": \"Naptár\",\n    \"cancel\": \"Mégsem\",\n    \"change\": \"Módosítás\",\n    \"clear\": \"Törlés\",\n    \"close\": \"Bezárás\",\n    \"collapse\": \"Összecsukás\",\n    \"confirm\": \"Megerősít\",\n    \"copy\": \"Másolás\",\n    \"create\": \"Létrehozás\",\n    \"created-at\": \"Létrehozva\",\n    \"database\": \"Adatbázis\",\n    \"day\": \"Nap\",\n    \"days\": {\n      \"fri\": \"Pé\",\n      \"mon\": \"Hé\",\n      \"sat\": \"Szo\",\n      \"sun\": \"Vas\",\n      \"thu\": \"Csü\",\n      \"tue\": \"Ke\",\n      \"wed\": \"Sze\"\n    },\n    \"delete\": \"Törlés\",\n    \"description\": \"Leírás\",\n    \"edit\": \"Szerkesztés\",\n    \"email\": \"Email\",\n    \"expand\": \"Kinyitás\",\n    \"explore\": \"Felfedezés\",\n    \"file\": \"Fájl\",\n    \"filter\": \"Szűrő\",\n    \"home\": \"Főoldal\",\n    \"image\": \"Kép\",\n    \"in\": \"Benne\",\n    \"inbox\": \"Értesítések\",\n    \"input\": \"Bemenet\",\n    \"language\": \"Nyelv\",\n    \"last-updated-at\": \"Utoljára frissítve\",\n    \"learn-more\": \"További információ\",\n    \"link\": \"Hivatkozás\",\n    \"map\": \"Térkép\",\n    \"mark\": \"Megjelölés\",\n    \"memo\": \"Jegyzet\",\n    \"memos\": \"Jegyzetek\",\n    \"more\": \"Több\",\n    \"name\": \"Név\",\n    \"new\": \"Új\",\n    \"nickname\": \"Becenév\",\n    \"null\": \"Null\",\n    \"or\": \"vagy\",\n    \"password\": \"Jelszó\",\n    \"pin\": \"Kitűzés\",\n    \"pinned\": \"Kitűzött\",\n    \"preview\": \"Előnézet\",\n    \"profile\": \"Profil\",\n    \"properties\": \"Tulajdonságok\",\n    \"referenced-by\": \"Hivatkozta\",\n    \"referencing\": \"Hivatkozik\",\n    \"relations\": \"Kapcsolatok\",\n    \"remember-me\": \"Megjegyzés\",\n    \"rename\": \"Átnevezés\",\n    \"reset\": \"Visszaállítás\",\n    \"resources\": \"Állományok\",\n    \"restore\": \"Helyreállítás\",\n    \"role\": \"Jogosultság\",\n    \"save\": \"Mentés\",\n    \"search\": \"Keresés\",\n    \"select\": \"Kijelölés\",\n    \"settings\": \"Beállítások\",\n    \"share\": \"Megosztás\",\n    \"shortcut-filter\": \"Gyorsbillentyű szűrő\",\n    \"shortcuts\": \"Gyorsbillentyűk\",\n    \"sign-in\": \"Bejelentkezés\",\n    \"sign-in-with\": \"Bejelentkezés {{provider}} használatával\",\n    \"sign-out\": \"Kijelentkezés\",\n    \"sign-up\": \"Regisztráció\",\n    \"statistics\": \"Statisztikák\",\n    \"tags\": \"Címkék\",\n    \"title\": \"Cím\",\n    \"today\": \"Ma\",\n    \"tree-mode\": \"Fa nézet\",\n    \"type\": \"Típus\",\n    \"unpin\": \"Feloldás\",\n    \"update\": \"Frissítés\",\n    \"upload\": \"Feltöltés\",\n    \"user\": \"Felhasználó\",\n    \"username\": \"Felhasználónév\",\n    \"version\": \"Verzió\",\n    \"visibility\": \"Láthatóság\",\n    \"yourself\": \"Magad\"\n  },\n  \"editor\": {\n    \"add-your-comment-here\": \"Írd ide a megjegyzésed...\",\n    \"any-thoughts\": \"Bármi ami a fejedben jár...\",\n    \"exit-focus-mode\": \"Kilépés a fókusz módból\",\n    \"focus-mode\": \"Fókusz mód\",\n    \"no-changes-detected\": \"Nincs változás\",\n    \"save\": \"Mentés\",\n    \"saving\": \"Mentés...\",\n    \"slash-commands\": \"Írj `/` parancsokat\"\n  },\n  \"inbox\": {\n    \"failed-to-load\": \"Nem sikerült betölteni az értesítést\",\n    \"memo-comment\": \"{{user}} hozzászólt ehhez: {{memo}}.\",\n    \"no-archived\": \"Nincsenek archivált értesítések\",\n    \"no-unread\": \"Nincsenek olvasatlan értesítések\",\n    \"unread\": \"Olvasatlan\"\n  },\n  \"markdown\": {\n    \"checkbox\": \"Jelölőnégyzet\",\n    \"code-block\": \"Kódblokk\",\n    \"content-syntax\": \"Tartalom szintaxis\"\n  },\n  \"memo\": {\n    \"archived-at\": \"Archiválva:\",\n    \"click-to-hide-nsfw-content\": \"Kattints a kényes tartalom elrejtéséhez\",\n    \"click-to-show-nsfw-content\": \"Kattints a kényes tartalom megjelenítéséhez\",\n    \"code\": \"Kód\",\n    \"comment\": {\n      \"self\": \"Hozzászólások\",\n      \"write-a-comment\": \"Írj hozzászólást\"\n    },\n    \"copy-content\": \"Tartalom másolása\",\n    \"copy-link\": \"Hivatkozás másolása\",\n    \"count-memos-in-date\": \"{{count}} {{memos}} ezen a napon: {{date}}\",\n    \"delete-confirm\": \"Biztosan törlöd ezt a jegyzetet? EZ A MŰVELET VÉGLEGES\",\n    \"delete-confirm-description\": \"Ez a művelet visszafordíthatatlan. A mellékletek, hivatkozások és referenciák is törlődnek.\",\n    \"direction\": \"Irány\",\n    \"direction-asc\": \"Növekvő\",\n    \"direction-desc\": \"Csökkenő\",\n    \"display-time\": \"Megjelenítési idő\",\n    \"filters\": {\n      \"has-code\": \"vanKód\",\n      \"has-link\": \"vanLink\",\n      \"has-task-list\": \"vanFeladatLista\"\n    },\n    \"links\": \"Hivatkozások\",\n    \"load-more\": \"Több betöltése\",\n    \"no-archived-memos\": \"Nincsenek archivált jegyzetek.\",\n    \"no-memos\": \"Nincsenek jegyzetek.\",\n    \"order-by\": \"Rendezés\",\n    \"search-placeholder\": \"Jegyzetek keresése...\",\n    \"show-less\": \"Kevesebb mutatása\",\n    \"show-more\": \"Több mutatása\",\n    \"to-do\": \"Teendő\",\n    \"view-detail\": \"Részletek\",\n    \"visibility\": {\n      \"disabled\": \"A nyilvános jegyzetek le vannak tiltva\",\n      \"private\": \"Privát\",\n      \"protected\": \"Munkatér\",\n      \"public\": \"Nyilvános\"\n    }\n  },\n  \"message\": {\n    \"archived-successfully\": \"Sikeres archiválás\",\n    \"change-memo-created-time\": \"Jegyzet létrehozási idejének változtatása\",\n    \"copied\": \"Másolva\",\n    \"deleted-successfully\": \"Sikeres törlés\",\n    \"description-is-required\": \"Leírás szükséges\",\n    \"failed-to-embed-memo\": \"Nem sikerült beágyazni a jegyzetet\",\n    \"fill-all\": \"Töltsd ki az összes mezőt.\",\n    \"fill-all-required-fields\": \"Töltsd ki az összes kötelező mezőt\",\n    \"maximum-upload-size-is\": \"A maximálisan megengedett feltöltési méret {{size}} MiB\",\n    \"memo-not-found\": \"Jegyzet nem található.\",\n    \"new-password-not-match\": \"Az új jelszavak nem egyeznek.\",\n    \"no-data\": \"Nem található adat.\",\n    \"password-changed\": \"Jelszó megváltoztatva\",\n    \"password-not-match\": \"A jelszavak nem egyeznek.\",\n    \"restored-successfully\": \"Sikeres visszaállítás\",\n    \"succeed-copy-content\": \"Tartalom sikeresen másolva.\",\n    \"succeed-copy-link\": \"Hivatkozás sikeresen másolva.\",\n    \"update-succeed\": \"Sikeres frissítés\",\n    \"user-not-found\": \"Felhasználó nem található\"\n  },\n  \"reference\": {\n    \"add-references\": \"Referenciák hozzáadása\",\n    \"embedded-usage\": \"Használat beágyazott tartalomként\",\n    \"no-memos-found\": \"Nem található jegyzet\",\n    \"search-placeholder\": \"Tartalom keresése\"\n  },\n  \"resource\": {\n    \"clear\": \"Törlés\",\n    \"copy-link\": \"Hivatkozás másolása\",\n    \"create-dialog\": {\n      \"external-link\": {\n        \"file-name\": \"Fájlnév\",\n        \"file-name-placeholder\": \"Fájlnév\",\n        \"link\": \"Link\",\n        \"link-placeholder\": \"https://a.fajlhoz.mutato/link\",\n        \"option\": \"Külső link\",\n        \"type\": \"Típus\",\n        \"type-placeholder\": \"Fájltípus\"\n      },\n      \"local-file\": {\n        \"choose\": \"Fájl kiválasztása…\",\n        \"option\": \"Helyi fájl\"\n      },\n      \"title\": \"Állomány létrehozása\",\n      \"upload-method\": \"Feltöltési mód\"\n    },\n    \"delete-all-unused\": \"Nem használtak törlése\",\n    \"delete-all-unused-confirm\": \"Biztosan törlöd az összes nem használt állományt? EZ A MŰVELET VÉGLEGES\",\n    \"delete-all-unused-error\": \"Nem sikerült törölni a nem használt állományokat\",\n    \"delete-all-unused-success\": \"Állományok sikeresen törölve\",\n    \"delete-resource\": \"Állomány törlése\",\n    \"delete-selected-resources\": \"Kijelölt állományok törlése\",\n    \"fetching-data\": \"Adatok lekérése…\",\n    \"file-drag-drop-prompt\": \"Húzd ide a fájlt a feltöltéshez\",\n    \"linked-amount\": \"Kapcsolódó mennyiség\",\n    \"no-files-selected\": \"Nincsenek kijelölt fájlok\",\n    \"no-resources\": \"Nincsenek állományok.\",\n    \"no-unused-resources\": \"Nincsenek nem használt állományok\",\n    \"reset-link\": \"Hivatkozás visszaállítása\",\n    \"reset-link-prompt\": \"Biztosan visszaállítod ezt a hivatkozást? Ez minden létező linket érvénytelenít. EZ A MŰVELET VÉGLEGES\",\n    \"reset-resource-link\": \"Állomány hivatkozásának visszaállítása\",\n    \"unused-resources\": \"Nem használt állományok\"\n  },\n  \"router\": {\n    \"back-to-top\": \"Vissza az oldal tetejére\",\n    \"go-to-home\": \"Vissza a főoldalra\"\n  },\n  \"setting\": {\n    \"member\": {\n      \"admin\": \"Admin\",\n      \"archive-member\": \"Tag archiválása\",\n      \"archive-success\": \"{{username}} sikeresen archiválva\",\n      \"archive-warning\": \"Biztosan archiválod {{username}} tagot?\",\n      \"archive-warning-description\": \"Az archiválás letiltja a fiókot. Később visszaállíthatod vagy törölheted.\",\n      \"create-a-member\": \"Tag létrehozása\",\n      \"delete-member\": \"Tag törlése\",\n      \"delete-success\": \"{{username}} sikeresen törölve\",\n      \"delete-warning\": \"Biztosan törlöd {{username}} tagot?\",\n      \"delete-warning-description\": \"EZ A MŰVELET VÉGLEGES\",\n      \"restore-success\": \"{{username}} sikeresen visszaállítva\",\n      \"user\": \"Felhasználó\",\n      \"label\": \"Tag\",\n      \"list-title\": \"Taglista\"\n    },\n    \"my-account\": {\n      \"label\": \"Fiókom\"\n    },\n    \"preference\": {\n      \"default-memo-sort-option\": \"Jegyzet megjelenítési ideje\",\n      \"default-memo-visibility\": \"Jegyzetek alapértelmezett láthatósága\",\n      \"theme\": \"Téma\",\n      \"label\": \"Preferenciák\"\n    },\n    \"shortcut\": {\n      \"delete-confirm\": \"Biztosan törlöd a `{{title}}` gyorsbillentyűt?\",\n      \"delete-success\": \"`{{title}}` gyorsbillentyű sikeresen törölve\"\n    },\n    \"sso\": {\n      \"authorization-endpoint\": \"Hitelesítési végpont\",\n      \"client-id\": \"Kliens ID\",\n      \"client-secret\": \"Kliens titok\",\n      \"confirm-delete\": \"Biztosan törlöd a(z) \\\"{{name}}\\\" nevű SSO konfigurációt? EZ A MŰVELET VÉGLEGES\",\n      \"create-sso\": \"SSO létrehozása\",\n      \"custom\": \"Egyedi\",\n      \"delete-sso\": \"Törlés megerősítése\",\n      \"disabled-password-login-warning\": \"A jelszavas bejelentkezés le van tiltva, légy óvatos az identitásszolgáltatók eltávolításakor\",\n      \"display-name\": \"Megjelenített név\",\n      \"identifier\": \"Azonosító\",\n      \"identifier-filter\": \"Azonosító szűrője\",\n      \"no-sso-found\": \"Nincs SSO.\",\n      \"redirect-url\": \"Átirányítási URL\",\n      \"scopes\": \"Hatókörök\",\n      \"single-sign-on\": \"Single Sign-On (SSO) konfigurálása hitelesítéshez\",\n      \"sso-created\": \"{{name}} nevű SSO létrehozva\",\n      \"sso-list\": \"SSO lista\",\n      \"sso-updated\": \"{{name}} nevű SSO frissítve\",\n      \"template\": \"Sablon\",\n      \"token-endpoint\": \"Token végpont\",\n      \"update-sso\": \"SSO frissítése\",\n      \"user-endpoint\": \"Felhasználói végpont\",\n      \"label\": \"SSO\"\n    },\n    \"storage\": {\n      \"accesskey\": \"Hozzáférési kulcs\",\n      \"accesskey-placeholder\": \"Access key / Access ID\",\n      \"bucket\": \"Bucket\",\n      \"bucket-placeholder\": \"Bucket neve\",\n      \"create-a-service\": \"Szolgáltatás létrehozása\",\n      \"create-storage\": \"Tárhely létrehozása\",\n      \"current-storage\": \"Jelenlegi objektumtár\",\n      \"delete-storage\": \"Tárhely törlése\",\n      \"endpoint\": \"Végpont\",\n      \"filepath-template\": \"Fájlútvonal sablon\",\n      \"local-storage-path\": \"Helyi tárhely útvonala\",\n      \"path\": \"Tárhely útvonala\",\n      \"path-description\": \"Használhatod ugyanazokat a dinamikus változókat a helyi tárhelyből, mint a {filename}\",\n      \"path-placeholder\": \"egyedi/útvonal\",\n      \"presign-placeholder\": \"Előre aláírt URL, opcionális\",\n      \"region\": \"Régió\",\n      \"region-placeholder\": \"Régió neve\",\n      \"s3-compatible-url\": \"S3 kompatibilis URL\",\n      \"secretkey\": \"Titkos kulcs\",\n      \"secretkey-placeholder\": \"Secret key / Access Key\",\n      \"storage-services\": \"Tárhelyszolgáltatások\",\n      \"type-database\": \"Adatbázis\",\n      \"type-local\": \"Helyi fájlrendszer\",\n      \"update-a-service\": \"Szolgáltatás módosítása\",\n      \"update-local-path\": \"Helyi tárhely útvonalának módosítása\",\n      \"update-local-path-description\": \"A helyi tárhely útvonala egy relatív útvonal az adatbázis fájlhoz\",\n      \"update-storage\": \"Tárhely módosítása\",\n      \"url-prefix\": \"URL előtag\",\n      \"url-prefix-placeholder\": \"Egyedi URL előtag, nem kötelező\",\n      \"url-suffix\": \"URL utótag\",\n      \"url-suffix-placeholder\": \"Egyedi URL utótag, nem kötelező\",\n      \"warning-text\": \"Biztosan törlöd a(z) \\\"{{name}}\\\" tárhelyszolgáltatást? EZ A MŰVELET VÉGLEGES\",\n      \"label\": \"Tárhely\"\n    },\n    \"system\": {\n      \"additional-script\": \"Egyedi script\",\n      \"additional-script-placeholder\": \"Egyedi JavaScript kód\",\n      \"additional-style\": \"Egyedi stílus\",\n      \"additional-style-placeholder\": \"Egyedi CSS kód\",\n      \"allow-user-signup\": \"Regisztráció engedélyezése\",\n      \"customize-server\": {\n        \"description\": \"Leírás\",\n        \"icon-url\": \"Ikon URL\",\n        \"locale\": \"Szerver nyelve\",\n        \"title\": \"Szerver személyre szabása\"\n      },\n      \"disable-password-login\": \"Jelszavas bejelentkezés letiltása\",\n      \"disable-password-login-final-warning\": \"Írd be, hogy \\\"CONFIRM\\\" ha tudod mit csinálsz.\",\n      \"disable-password-login-warning\": \"Ez letiltja a jelszavas bejelentkezést minden felhasználó számára. Ha a konfigurált identitásszolgáltatók nem érhetők el, a bejelentkezés nem lehetséges ezen beállítás kikapcsolása nélkül az adatbázisban. Ezen kívül fokozott figyelem szükséges az identitásszolgáltatók eltávolítása során is\",\n      \"display-with-updated-time\": \"Megjelenítés frissített idővel\",\n      \"enable-auto-compact\": \"Automatikus tömörítés engedélyezése\",\n      \"enable-double-click-to-edit\": \"Dupla kattintásos szerkesztés engedélyezése\",\n      \"enable-password-login\": \"Jelszavas bejelentkezés engedélyezése\",\n      \"enable-password-login-warning\": \"Ez engedélyezi a jelszavas bejelentkezést minden felhasználó számára. Csak akkor folytasd, ha szeretnéd, ha a felhasználók SSO és jelszó használatával is be tudjanak jelentkezni\",\n      \"max-upload-size\": \"Maximális feltöltési méret (MiB)\",\n      \"max-upload-size-hint\": \"Az ajánlott érték 32 MiB.\",\n      \"removed-completed-task-list-items\": \"Kész feladatok törlésének engedélyezése\",\n      \"server-name\": \"Szerver neve\",\n      \"title\": \"Általános\",\n      \"label\": \"Rendszer\"\n    },\n    \"version\": \"Verzió\",\n    \"access-token\": {\n      \"access-token-copied-to-clipboard\": \"Hozzáférési token vágólapra másolva\",\n      \"access-token-deleted\": \"Hozzáférési token `{{description}}` törölve\",\n      \"access-token-deletion\": \"Biztosan törlöd a hozzáférési tokent `{{description}}`?\",\n      \"access-token-deletion-description\": \"Ez a művelet visszafordíthatatlan. Frissítened kell minden szolgáltatást, amely ezt a tokent használja, hogy új tokent használjanak.\",\n      \"create-dialog\": {\n        \"access-token-created\": \"Hozzáférési token `{{description}}` létrehozva\",\n        \"create-access-token\": \"Hozzáférési token létrehozása\",\n        \"created-at\": \"Létrehozva\",\n        \"description\": \"Leírás\",\n        \"duration-1m\": \"1 hónap\",\n        \"duration-8h\": \"8 óra\",\n        \"duration-never\": \"Soha\",\n        \"expiration\": \"Lejárat\",\n        \"expires-at\": \"Lejárat dátuma\",\n        \"some-description\": \"Néhány leírás...\"\n      },\n      \"description\": \"A fiókodhoz tartozó összes hozzáférési token listája.\",\n      \"title\": \"Hozzáférési tokenek\",\n      \"token\": \"Token\"\n    },\n    \"account\": {\n      \"change-password\": \"Jelszó megváltoztatása\",\n      \"email-note\": \"Nem kötelező\",\n      \"export-memos\": \"Jegyzetek exportálása\",\n      \"nickname-note\": \"A bannerben megjelenített\",\n      \"openapi-reset\": \"OpenAPI kulcs visszaállítása\",\n      \"openapi-sample-post\": \"Hello #memos innen: {{url}}\",\n      \"openapi-title\": \"OpenAPI\",\n      \"reset-api\": \"API visszaállítása\",\n      \"title\": \"Fiókinformáció\",\n      \"update-information\": \"Információ frissítése\",\n      \"username-note\": \"Bejelentkezéshez használt\"\n    },\n    \"instance\": {\n      \"disallow-change-nickname\": \"Becenév módosításának tiltása\",\n      \"disallow-change-username\": \"Felhasználónév módosításának tiltása\",\n      \"disallow-password-auth\": \"Jelszavas hitelesítés tiltása\",\n      \"disallow-user-registration\": \"Felhasználó regisztráció tiltása\",\n      \"monday\": \"Hétfő\",\n      \"saturday\": \"Szombat\",\n      \"sunday\": \"Vasárnap\",\n      \"week-start-day\": \"A hét kezdőnapja\"\n    },\n    \"memo\": {\n      \"content-length-limit\": \"Tartalom hosszának korlátja (bájt)\",\n      \"enable-blur-nsfw-content\": \"Érzékeny (NSFW) tartalom elhomályosításának engedélyezése\",\n      \"enable-memo-comments\": \"Jegyzet hozzászólások engedélyezése\",\n      \"enable-memo-location\": \"Jegyzet helyének engedélyezése\",\n      \"reactions\": \"Reakciók\",\n      \"title\": \"Jegyzethez kapcsolódó beállítások\",\n      \"label\": \"Jegyzet\"\n    },\n    \"webhook\": {\n      \"create-dialog\": {\n        \"an-easy-to-remember-name\": \"Könnyen megjegyezhető név\",\n        \"create-webhook\": \"Webhook létrehozása\",\n        \"create-webhook-success\": \"`{{name}}` webhook létrehozva\",\n        \"edit-webhook\": \"Webhook szerkesztése\",\n        \"payload-url\": \"Payload URL\",\n        \"title\": \"Cím\",\n        \"url-example-post-receive\": \"https://example.com/postreceive\"\n      },\n      \"delete-dialog\": {\n        \"delete-webhook-description\": \"Ez a művelet visszafordíthatatlan.\",\n        \"delete-webhook-success\": \"`{{name}}` webhook sikeresen törölve\",\n        \"delete-webhook-title\": \"Biztosan törlöd a `{{name}}` webhookot?\"\n      },\n      \"no-webhooks-found\": \"Nincs webhook.\",\n      \"title\": \"Webhooks\",\n      \"url\": \"URL\"\n    }\n  },\n  \"tag\": {\n    \"all-tags\": \"Minden címke\",\n    \"create-tag\": \"Címke létrehozása\",\n    \"create-tags-guide\": \"Létrehozhatsz címkéket `#címke` beírásával.\",\n    \"delete-confirm\": \"Biztosan törlöd ezt a címkét? Minden kapcsolódó jegyzet archiválva lesz.\",\n    \"delete-success\": \"Címke sikeresen törölve\",\n    \"delete-tag\": \"Címke törlése\",\n    \"new-name\": \"Új név\",\n    \"no-tag-found\": \"Nem található címke\",\n    \"old-name\": \"Régi név\",\n    \"rename-error-empty\": \"A címke neve nem lehet üres vagy tartalmazhat szóközt\",\n    \"rename-error-repeat\": \"Az új név nem lehet azonos a régi névvel\",\n    \"rename-success\": \"Címke sikeresen átnevezve\",\n    \"rename-tag\": \"Címke átnevezése\",\n    \"rename-tip\": \"Minden jegyzeted ezzel a címkével frissülni fog.\"\n  },\n  \"tooltip\": {\n    \"link-memo\": \"Jegyzet hivatkozása\",\n    \"markdown-menu\": \"Markdown\",\n    \"select-location\": \"Helyszín\",\n    \"select-visibility\": \"Láthatóság\",\n    \"tags\": \"Címkék\",\n    \"upload-attachment\": \"Melléklet(ek) feltöltése\"\n  }\n}\n"
  },
  {
    "path": "web/src/locales/id.json",
    "content": "{\n  \"about\": {\n    \"blogs\": \"Blog\",\n    \"description\": \"Layanan pencatatan ringan yang mengutamakan privasi. Abadikan dan bagikan pemikiran hebat Anda dengan mudah.\",\n    \"documents\": \"Dokumen\",\n    \"github-repository\": \"Repositori GitHub\",\n    \"official-website\": \"Situs Web Resmi\"\n  },\n  \"auth\": {\n    \"create-your-account\": \"Buat akun Anda\",\n    \"host-tip\": \"Anda mendaftar sebagai Host Situs.\",\n    \"new-password\": \"Kata sandi baru\",\n    \"repeat-new-password\": \"Ulangi kata sandi baru\",\n    \"sign-in-tip\": \"Sudah memiliki akun?\",\n    \"sign-up-tip\": \"Belum memiliki akun?\"\n  },\n  \"common\": {\n    \"about\": \"Tentang\",\n    \"add\": \"Tambah\",\n    \"admin\": \"Admin\",\n    \"all\": \"Semua\",\n    \"archive\": \"Arsip\",\n    \"archived\": \"Diarsipkan\",\n    \"attachments\": \"Lampiran\",\n    \"auto-expand\": \"Perluas otomatis\",\n    \"avatar\": \"Avatar\",\n    \"basic\": \"Dasar\",\n    \"beta\": \"Beta\",\n    \"calendar\": \"Kalender\",\n    \"cancel\": \"Batal\",\n    \"change\": \"Ubah\",\n    \"clear\": \"Bersihkan\",\n    \"close\": \"Tutup\",\n    \"collapse\": \"Kempiskan\",\n    \"confirm\": \"Konfirmasi\",\n    \"copy\": \"Salin\",\n    \"create\": \"Buat\",\n    \"created-at\": \"Dibuat pada\",\n    \"database\": \"Basis Data\",\n    \"day\": \"Hari\",\n    \"days\": {\n      \"fri\": \"Jum\",\n      \"mon\": \"Sen\",\n      \"sat\": \"Sab\",\n      \"sun\": \"Min\",\n      \"thu\": \"Kam\",\n      \"tue\": \"Sel\",\n      \"wed\": \"Rab\"\n    },\n    \"delete\": \"Hapus\",\n    \"description\": \"Deskripsi\",\n    \"edit\": \"Edit\",\n    \"email\": \"Email\",\n    \"expand\": \"Perluas\",\n    \"explore\": \"Jelajahi\",\n    \"file\": \"Berkas\",\n    \"filter\": \"Saring\",\n    \"home\": \"Beranda\",\n    \"image\": \"Gambar\",\n    \"in\": \"Dalam\",\n    \"inbox\": \"Kotak Masuk\",\n    \"input\": \"Masukan\",\n    \"language\": \"Bahasa\",\n    \"last-updated-at\": \"Terakhir diperbarui pada\",\n    \"learn-more\": \"Pelajari lebih lanjut\",\n    \"link\": \"Tautan\",\n    \"map\": \"Peta\",\n    \"mark\": \"Tandai\",\n    \"memo\": \"Memo\",\n    \"memos\": \"Memo\",\n    \"more\": \"Lainnya\",\n    \"name\": \"Nama\",\n    \"new\": \"Baru\",\n    \"nickname\": \"Nama Panggilan\",\n    \"null\": \"Null\",\n    \"or\": \"atau\",\n    \"password\": \"Kata sandi\",\n    \"pin\": \"Jepit\",\n    \"pinned\": \"Dijepit\",\n    \"preview\": \"Pratampil\",\n    \"profile\": \"Profil\",\n    \"properties\": \"Properti\",\n    \"referenced-by\": \"Dirujuk oleh\",\n    \"referencing\": \"Merujuk\",\n    \"relations\": \"Relasi\",\n    \"remember-me\": \"Ingat saya\",\n    \"rename\": \"Ubah nama\",\n    \"reset\": \"Atur ulang\",\n    \"resources\": \"Sumber Daya\",\n    \"restore\": \"Pulihkan\",\n    \"role\": \"Peran\",\n    \"save\": \"Simpan\",\n    \"search\": \"Cari\",\n    \"select\": \"Pilih\",\n    \"settings\": \"Pengaturan\",\n    \"share\": \"Bagikan\",\n    \"shortcut-filter\": \"Saring pintasan\",\n    \"shortcuts\": \"Pintasan\",\n    \"sign-in\": \"Masuk\",\n    \"sign-in-with\": \"Masuk dengan {{provider}}\",\n    \"sign-out\": \"Keluar\",\n    \"sign-up\": \"Daftar\",\n    \"statistics\": \"Statistik\",\n    \"tags\": \"Tag\",\n    \"title\": \"Judul\",\n    \"today\": \"Hari ini\",\n    \"tree-mode\": \"Mode pohon\",\n    \"type\": \"Tipe\",\n    \"unpin\": \"Lepaskan\",\n    \"update\": \"Perbarui\",\n    \"upload\": \"Unggah\",\n    \"user\": \"Pengguna\",\n    \"username\": \"Nama pengguna\",\n    \"version\": \"Versi\",\n    \"visibility\": \"Visibilitas\",\n    \"yourself\": \"Anda sendiri\"\n  },\n  \"editor\": {\n    \"add-your-comment-here\": \"Tambahkan komentar Anda di sini...\",\n    \"any-thoughts\": \"Punya pemikiran...\",\n    \"exit-focus-mode\": \"Keluar dari Mode Fokus\",\n    \"focus-mode\": \"Mode Fokus\",\n    \"no-changes-detected\": \"Tidak ada perubahan yang terdeteksi\",\n    \"save\": \"Simpan\",\n    \"saving\": \"Menyimpan...\",\n    \"slash-commands\": \"Ketik `/` untuk perintah\"\n  },\n  \"inbox\": {\n    \"failed-to-load\": \"Gagal memuat item kotak masuk\",\n    \"memo-comment\": \"{{user}} memiliki komentar di {{memo}} Anda.\",\n    \"no-archived\": \"Tidak ada notifikasi yang diarsipkan\",\n    \"no-unread\": \"Tidak ada notifikasi yang belum dibaca\",\n    \"unread\": \"Belum dibaca\"\n  },\n  \"markdown\": {\n    \"checkbox\": \"Kotak Centang\",\n    \"code-block\": \"Blok Kode\",\n    \"content-syntax\": \"Sintaks Konten\"\n  },\n  \"memo\": {\n    \"archived-at\": \"Diarsipkan pada\",\n    \"click-to-hide-nsfw-content\": \"Klik untuk menyembunyikan konten NSFW\",\n    \"click-to-show-nsfw-content\": \"Klik untuk menampilkan konten NSFW\",\n    \"code\": \"Kode\",\n    \"comment\": {\n      \"self\": \"Komentar\",\n      \"write-a-comment\": \"Tulis komentar\"\n    },\n    \"copy-content\": \"Salin Konten\",\n    \"copy-link\": \"Salin Tautan\",\n    \"count-memos-in-date\": \"{{count}} {{memos}} di {{date}}\",\n    \"delete-confirm\": \"Apakah Anda yakin ingin menghapus memo ini? TINDAKAN INI TIDAK DAPAT DIBATALKAN\",\n    \"delete-confirm-description\": \"Tindakan ini tidak dapat dibatalkan. Lampiran, tautan, dan referensi juga akan dihapus.\",\n    \"direction\": \"Arah\",\n    \"direction-asc\": \"Menaik\",\n    \"direction-desc\": \"Menurun\",\n    \"display-time\": \"Waktu Tampil\",\n    \"filters\": {\n      \"has-code\": \"Memiliki kode\",\n      \"has-link\": \"Memiliki tautan\",\n      \"has-task-list\": \"Memiliki daftar tugas\"\n    },\n    \"links\": \"Tautan\",\n    \"load-more\": \"Muat lebih banyak\",\n    \"no-archived-memos\": \"Tidak ada memo yang diarsipkan.\",\n    \"no-memos\": \"Tidak ada memo.\",\n    \"order-by\": \"Urutkan Berdasarkan\",\n    \"search-placeholder\": \"Cari memo\",\n    \"show-less\": \"Tampilkan lebih sedikit\",\n    \"show-more\": \"Tampilkan lebih banyak\",\n    \"to-do\": \"Daftar tugas\",\n    \"view-detail\": \"Lihat Detail\",\n    \"visibility\": {\n      \"disabled\": \"Memo publik dinonaktifkan\",\n      \"private\": \"Pribadi\",\n      \"protected\": \"Terlindungi\",\n      \"public\": \"Publik\"\n    }\n  },\n  \"message\": {\n    \"archived-successfully\": \"Berhasil diarsipkan\",\n    \"change-memo-created-time\": \"Ubah waktu pembuatan memo\",\n    \"copied\": \"Disalin\",\n    \"deleted-successfully\": \"Berhasil dihapus\",\n    \"description-is-required\": \"Deskripsi wajib diisi\",\n    \"failed-to-embed-memo\": \"Gagal menyematkan memo\",\n    \"fill-all\": \"Harap isi semua kolom.\",\n    \"fill-all-required-fields\": \"Harap isi semua kolom wajib\",\n    \"maximum-upload-size-is\": \"Ukuran unggahan maksimum yang diizinkan adalah {{size}} MiB\",\n    \"memo-not-found\": \"Memo tidak ditemukan.\",\n    \"new-password-not-match\": \"Kata sandi baru tidak cocok.\",\n    \"no-data\": \"Data tidak ditemukan.\",\n    \"password-changed\": \"Kata sandi diubah\",\n    \"password-not-match\": \"Kata sandi tidak cocok.\",\n    \"restored-successfully\": \"Berhasil dipulihkan\",\n    \"succeed-copy-content\": \"Konten berhasil disalin.\",\n    \"succeed-copy-link\": \"Tautan berhasil disalin.\",\n    \"update-succeed\": \"Pembaruan berhasil\",\n    \"user-not-found\": \"Pengguna tidak ditemukan\"\n  },\n  \"reference\": {\n    \"add-references\": \"Tambah referensi\",\n    \"embedded-usage\": \"Gunakan sebagai Konten Tertanam\",\n    \"no-memos-found\": \"Tidak ada memo yang ditemukan\",\n    \"search-placeholder\": \"Cari konten\"\n  },\n  \"resource\": {\n    \"clear\": \"Bersihkan\",\n    \"copy-link\": \"Salin Tautan\",\n    \"create-dialog\": {\n      \"external-link\": {\n        \"file-name\": \"Nama berkas\",\n        \"file-name-placeholder\": \"Nama berkas\",\n        \"link\": \"Tautan\",\n        \"link-placeholder\": \"https://the.link.to/your/resource\",\n        \"option\": \"Tautan eksternal\",\n        \"type\": \"Tipe\",\n        \"type-placeholder\": \"Tipe berkas\"\n      },\n      \"local-file\": {\n        \"choose\": \"Pilih berkas…\",\n        \"option\": \"Berkas lokal\"\n      },\n      \"title\": \"Buat Sumber Daya\",\n      \"upload-method\": \"Metode unggah\"\n    },\n    \"delete-all-unused\": \"Hapus semua yang tidak terpakai\",\n    \"delete-all-unused-confirm\": \"Apakah Anda yakin ingin menghapus semua sumber daya yang tidak terpakai? TINDAKAN INI TIDAK DAPAT DIBATALKAN\",\n    \"delete-all-unused-error\": \"Gagal menghapus sumber daya yang tidak terpakai\",\n    \"delete-all-unused-success\": \"Sumber daya berhasil dihapus\",\n    \"delete-resource\": \"Hapus Sumber Daya\",\n    \"delete-selected-resources\": \"Hapus Sumber Daya yang Dipilih\",\n    \"fetching-data\": \"Mengambil data…\",\n    \"file-drag-drop-prompt\": \"Seret dan jatuhkan berkas Anda di sini untuk mengunggah berkas\",\n    \"linked-amount\": \"Jumlah yang terhubung\",\n    \"no-files-selected\": \"Tidak ada berkas yang dipilih\",\n    \"no-resources\": \"Tidak ada sumber daya.\",\n    \"no-unused-resources\": \"Tidak ada sumber daya yang tidak terpakai\",\n    \"reset-link\": \"Atur ulang Tautan\",\n    \"reset-link-prompt\": \"Apakah Anda yakin untuk mengatur ulang tautan? Ini akan memutus semua penggunaan tautan yang ada. TINDAKAN INI TIDAK DAPAT DIBATALKAN\",\n    \"reset-resource-link\": \"Atur ulang Tautan Sumber Daya\",\n    \"unused-resources\": \"Sumber daya tidak terpakai\"\n  },\n  \"router\": {\n    \"back-to-top\": \"Kembali ke Atas\",\n    \"go-to-home\": \"Ke Beranda\"\n  },\n  \"setting\": {\n    \"member\": {\n      \"admin\": \"Admin\",\n      \"archive-member\": \"Arsip anggota\",\n      \"archive-success\": \"{{username}} berhasil diarsipkan\",\n      \"archive-warning\": \"Apakah Anda yakin untuk mengarsipkan {{username}}?\",\n      \"archive-warning-description\": \"Mengarsipkan menonaktifkan akun. Anda dapat memulihkan atau menghapusnya nanti.\",\n      \"create-a-member\": \"Buat anggota\",\n      \"delete-member\": \"Hapus Anggota\",\n      \"delete-success\": \"{{username}} berhasil dihapus\",\n      \"delete-warning\": \"Apakah Anda yakin untuk menghapus {{username}}? TINDAKAN INI TIDAK DAPAT DIBATALKAN\",\n      \"delete-warning-description\": \"TINDAKAN INI TIDAK DAPAT DIBATALKAN\",\n      \"restore-success\": \"{{username}} berhasil dipulihkan\",\n      \"user\": \"Pengguna\",\n      \"label\": \"Anggota\",\n      \"list-title\": \"Daftar Anggota\"\n    },\n    \"my-account\": {\n      \"label\": \"Akun Saya\"\n    },\n    \"preference\": {\n      \"default-memo-sort-option\": \"Waktu tampil memo\",\n      \"default-memo-visibility\": \"Visibilitas memo default\",\n      \"theme\": \"Tema\",\n      \"label\": \"Preferensi\"\n    },\n    \"shortcut\": {\n      \"delete-confirm\": \"Apakah Anda yakin ingin menghapus pintasan `{{title}}`?\",\n      \"delete-success\": \"Pintasan `{{title}}` berhasil dihapus\"\n    },\n    \"sso\": {\n      \"authorization-endpoint\": \"Akhir Otorisasi\",\n      \"client-id\": \"ID Klien\",\n      \"client-secret\": \"Kunci Rahasia Klien\",\n      \"confirm-delete\": \"Apakah Anda yakin ingin menghapus konfigurasi SSO \\\"{{name}}\\\"? TINDAKAN INI TIDAK DAPAT DIBATALKAN.\",\n      \"create-sso\": \"Buat SSO\",\n      \"custom\": \"Kustom\",\n      \"delete-sso\": \"Konfirmasi penghapusan\",\n      \"disabled-password-login-warning\": \"Login dengan kata sandi dinonaktifkan, berhati-hatilah saat menghapus penyedia identitas.\",\n      \"display-name\": \"Nama Tampilan\",\n      \"identifier\": \"Pengenal\",\n      \"identifier-filter\": \"Filter Pengenal\",\n      \"no-sso-found\": \"Tidak ada SSO yang ditemukan.\",\n      \"redirect-url\": \"URL Pengalihan\",\n      \"scopes\": \"Lingkup\",\n      \"single-sign-on\": \"Mengonfigurasi Single Sign-On (SSO) untuk Autentikasi\",\n      \"sso-created\": \"SSO {{name}} dibuat\",\n      \"sso-list\": \"Daftar SSO\",\n      \"sso-updated\": \"SSO {{name}} diperbarui\",\n      \"template\": \"Templat\",\n      \"token-endpoint\": \"Akhir Token\",\n      \"update-sso\": \"Perbarui SSO\",\n      \"user-endpoint\": \"Akhir Pengguna\",\n      \"label\": \"SSO\"\n    },\n    \"storage\": {\n      \"accesskey\": \"Kunci Akses\",\n      \"accesskey-placeholder\": \"Kunci Akses / ID Akses\",\n      \"bucket\": \"Bucket\",\n      \"bucket-placeholder\": \"Nama Bucket\",\n      \"create-a-service\": \"Buat layanan\",\n      \"create-storage\": \"Buat Penyimpanan\",\n      \"current-storage\": \"Penyimpanan objek saat ini\",\n      \"delete-storage\": \"Hapus Penyimpanan\",\n      \"endpoint\": \"Akhir\",\n      \"filepath-template\": \"Templat jalur berkas\",\n      \"local-storage-path\": \"Jalur penyimpanan lokal\",\n      \"path\": \"Jalur Penyimpanan\",\n      \"path-description\": \"Anda dapat menggunakan variabel dinamis yang sama dari penyimpanan lokal, seperti {filename}\",\n      \"path-placeholder\": \"custom/path\",\n      \"presign-placeholder\": \"URL pra-tanda tangan, opsional\",\n      \"region\": \"Wilayah\",\n      \"region-placeholder\": \"Nama Wilayah\",\n      \"s3-compatible-url\": \"URL Kompatibel S3\",\n      \"secretkey\": \"Kunci Rahasia\",\n      \"secretkey-placeholder\": \"Kunci Rahasia / Kunci Akses\",\n      \"storage-services\": \"Layanan Penyimpanan\",\n      \"type-database\": \"Basis Data\",\n      \"type-local\": \"Sistem file lokal\",\n      \"update-a-service\": \"Perbarui layanan\",\n      \"update-local-path\": \"Perbarui Jalur Penyimpanan Lokal\",\n      \"update-local-path-description\": \"Jalur penyimpanan lokal adalah jalur relatif ke file basis data Anda\",\n      \"update-storage\": \"Perbarui Penyimpanan\",\n      \"url-prefix\": \"Prefiks URL\",\n      \"url-prefix-placeholder\": \"Prefiks URL kustom, opsional\",\n      \"url-suffix\": \"Sufiks URL\",\n      \"url-suffix-placeholder\": \"Sufiks URL kustom, opsional\",\n      \"warning-text\": \"Apakah Anda yakin ingin menghapus layanan penyimpanan \\\"{{name}}\\\"? TINDAKAN INI TIDAK DAPAT DIBATALKAN.\",\n      \"label\": \"Penyimpanan\"\n    },\n    \"system\": {\n      \"additional-script\": \"Skrip tambahan\",\n      \"additional-script-placeholder\": \"Kode JavaScript tambahan\",\n      \"additional-style\": \"Gaya tambahan\",\n      \"additional-style-placeholder\": \"Kode CSS tambahan\",\n      \"allow-user-signup\": \"Izinkan pendaftaran pengguna\",\n      \"customize-server\": {\n        \"description\": \"Deskripsi\",\n        \"icon-url\": \"URL Ikon\",\n        \"locale\": \"Locale Server\",\n        \"title\": \"Sesuaikan Server\"\n      },\n      \"disable-password-login\": \"Nonaktifkan login kata sandi\",\n      \"disable-password-login-final-warning\": \"Silakan ketik \\\"KONFIRMASI\\\" jika Anda tahu apa yang Anda lakukan.\",\n      \"disable-password-login-warning\": \"Ini akan menonaktifkan login kata sandi untuk semua pengguna. Tidak mungkin untuk login tanpa mengembalikan pengaturan ini di basis data jika penyedia identitas yang Anda konfigurasikan gagal. Anda juga harus lebih berhati-hati saat menghapus penyedia identitas.\",\n      \"display-with-updated-time\": \"Tampilkan dengan waktu terbaru\",\n      \"enable-auto-compact\": \"Aktifkan kompak otomatis\",\n      \"enable-double-click-to-edit\": \"Aktifkan klik ganda untuk mengedit\",\n      \"enable-password-login\": \"Aktifkan login kata sandi\",\n      \"enable-password-login-warning\": \"Ini akan mengaktifkan login kata sandi untuk semua pengguna. Lanjutkan hanya jika Anda ingin pengguna dapat login menggunakan SSO dan kata sandi.\",\n      \"max-upload-size\": \"Ukuran unggah maksimum (MiB)\",\n      \"max-upload-size-hint\": \"Nilai yang direkomendasikan adalah 32 MiB.\",\n      \"removed-completed-task-list-items\": \"Aktifkan penghapusan item daftar tugas yang selesai\",\n      \"server-name\": \"Nama Server\",\n      \"title\": \"Umum\",\n      \"label\": \"Sistem\"\n    },\n    \"version\": \"Versi\",\n    \"access-token\": {\n      \"access-token-copied-to-clipboard\": \"Token akses disalin ke papan klip\",\n      \"access-token-deleted\": \"Token akses `{{description}}` dihapus\",\n      \"access-token-deletion\": \"Anda yakin ingin menghapus token akses {{description}}? TINDAKAN INI TIDAK DAPAT DIBATALKAN.\",\n      \"access-token-deletion-description\": \"Tindakan ini tidak dapat dibatalkan. Anda perlu memperbarui layanan yang menggunakan token ini untuk menggunakan token baru.\",\n      \"create-dialog\": {\n        \"access-token-created\": \"Token akses `{{description}}` dibuat\",\n        \"create-access-token\": \"Buat Token Akses\",\n        \"created-at\": \"Dibuat pada\",\n        \"description\": \"Deskripsi\",\n        \"duration-1m\": \"1 bulan\",\n        \"duration-8h\": \"8 jam\",\n        \"duration-never\": \"Tidak Pernah\",\n        \"expiration\": \"Kedaluwarsa\",\n        \"expires-at\": \"Berakhir pada\",\n        \"some-description\": \"Beberapa deskripsi...\"\n      },\n      \"description\": \"Daftar semua token akses untuk akun Anda.\",\n      \"title\": \"Token Akses\",\n      \"token\": \"Token\"\n    },\n    \"account\": {\n      \"change-password\": \"Ubah kata sandi\",\n      \"email-note\": \"Opsional\",\n      \"export-memos\": \"Ekspor Memo\",\n      \"nickname-note\": \"Ditampilkan di banner\",\n      \"openapi-reset\": \"Atur ulang Kunci OpenAPI\",\n      \"openapi-sample-post\": \"Halo #memos dari {{url}}\",\n      \"openapi-title\": \"OpenAPI\",\n      \"reset-api\": \"Atur ulang API\",\n      \"title\": \"Informasi Akun\",\n      \"update-information\": \"Perbarui Informasi\",\n      \"username-note\": \"Digunakan untuk masuk\"\n    },\n    \"instance\": {\n      \"disallow-change-nickname\": \"Larangan mengubah nama panggilan\",\n      \"disallow-change-username\": \"Larangan mengubah nama pengguna\",\n      \"disallow-password-auth\": \"Larangan autentikasi kata sandi\",\n      \"disallow-user-registration\": \"Larangan pendaftaran pengguna\",\n      \"monday\": \"Senin\",\n      \"saturday\": \"Sabtu\",\n      \"sunday\": \"Minggu\",\n      \"week-start-day\": \"Hari awal pekan\"\n    },\n    \"memo\": {\n      \"content-length-limit\": \"Batas panjang konten (Byte)\",\n      \"enable-blur-nsfw-content\": \"Aktifkan pengaburan konten sensitif (NSFW)\",\n      \"enable-memo-comments\": \"Aktifkan komentar memo\",\n      \"enable-memo-location\": \"Aktifkan lokasi memo\",\n      \"reactions\": \"Reaksi\",\n      \"title\": \"Pengaturan terkait Memo\",\n      \"label\": \"Memo\"\n    },\n    \"webhook\": {\n      \"create-dialog\": {\n        \"an-easy-to-remember-name\": \"Nama yang mudah diingat\",\n        \"create-webhook\": \"Buat webhook\",\n        \"create-webhook-success\": \"Webhook `{{name}}` dibuat\",\n        \"edit-webhook\": \"Edit webhook\",\n        \"payload-url\": \"URL Payload\",\n        \"title\": \"Judul\",\n        \"url-example-post-receive\": \"https://example.com/postreceive\"\n      },\n      \"delete-dialog\": {\n        \"delete-webhook-description\": \"Tindakan ini tidak dapat dibatalkan.\",\n        \"delete-webhook-success\": \"Webhook `{{name}}` berhasil dihapus\",\n        \"delete-webhook-title\": \"Apakah Anda yakin ingin menghapus webhook `{{name}}`?\"\n      },\n      \"no-webhooks-found\": \"Tidak ada webhook yang ditemukan.\",\n      \"title\": \"Webhook\",\n      \"url\": \"URL\"\n    }\n  },\n  \"tag\": {\n    \"all-tags\": \"Semua Tag\",\n    \"create-tag\": \"Buat Tag\",\n    \"create-tags-guide\": \"Anda dapat membuat tag dengan memasukkan `#tag`.\",\n    \"delete-confirm\": \"Apakah Anda yakin ingin menghapus tag ini? Semua memo terkait akan diarsipkan.\",\n    \"delete-success\": \"Tag berhasil dihapus\",\n    \"delete-tag\": \"Hapus Tag\",\n    \"new-name\": \"Nama Baru\",\n    \"no-tag-found\": \"Tidak ada tag yang ditemukan\",\n    \"old-name\": \"Nama Lama\",\n    \"rename-error-empty\": \"Nama tag tidak boleh kosong atau mengandung spasi\",\n    \"rename-error-repeat\": \"Nama baru tidak boleh sama dengan nama lama\",\n    \"rename-success\": \"Berhasil mengganti nama tag\",\n    \"rename-tag\": \"Ganti nama tag\",\n    \"rename-tip\": \"Semua memo Anda dengan tag ini akan diperbarui.\"\n  },\n  \"tooltip\": {\n    \"link-memo\": \"Tautkan Memo\",\n    \"markdown-menu\": \"Markdown\",\n    \"select-location\": \"Pilih Lokasi\",\n    \"select-visibility\": \"Pilih Visibilitas\",\n    \"tags\": \"Tag\",\n    \"upload-attachment\": \"Unggah Lampiran\"\n  }\n}\n"
  },
  {
    "path": "web/src/locales/it.json",
    "content": "{\n  \"about\": {\n    \"blogs\": \"Blog\",\n    \"description\": \"Un servizio di note leggero e incentrato sulla privacy. Cattura e condividi facilmente i tuoi pensieri migliori.\",\n    \"documents\": \"Documenti\",\n    \"github-repository\": \"Repo GitHub\",\n    \"official-website\": \"Sito ufficiale\"\n  },\n  \"auth\": {\n    \"create-your-account\": \"Crea il tuo account\",\n    \"host-tip\": \"Ti stai registrando come proprietario del sito.\",\n    \"new-password\": \"Nuova password\",\n    \"repeat-new-password\": \"Ripeti la nuova password\",\n    \"sign-in-tip\": \"Hai già un account?\",\n    \"sign-up-tip\": \"Non hai ancora un account?\"\n  },\n  \"common\": {\n    \"about\": \"Informazioni\",\n    \"add\": \"Aggiungi\",\n    \"admin\": \"Admin\",\n    \"all\": \"Tutti\",\n    \"archive\": \"Archivia\",\n    \"archived\": \"Archiviati\",\n    \"attachments\": \"Allegati\",\n    \"auto-expand\": \"Espandi automaticamente\",\n    \"avatar\": \"Avatar\",\n    \"basic\": \"Base\",\n    \"beta\": \"Beta\",\n    \"calendar\": \"Calendario\",\n    \"cancel\": \"Cancella\",\n    \"change\": \"Cambia\",\n    \"clear\": \"Pulisci\",\n    \"close\": \"Chiudi\",\n    \"collapse\": \"Comprimi\",\n    \"confirm\": \"Conferma\",\n    \"copy\": \"Copia\",\n    \"create\": \"Crea\",\n    \"created-at\": \"Creato il\",\n    \"database\": \"Database\",\n    \"day\": \"Giorno\",\n    \"days\": {\n      \"fri\": \"Ven\",\n      \"mon\": \"Lun\",\n      \"sat\": \"Sab\",\n      \"sun\": \"Dom\",\n      \"thu\": \"Gio\",\n      \"tue\": \"Mar\",\n      \"wed\": \"Mer\"\n    },\n    \"delete\": \"Elimina\",\n    \"description\": \"Descrizione\",\n    \"edit\": \"Modifica\",\n    \"email\": \"Email\",\n    \"expand\": \"Espandi\",\n    \"explore\": \"Esplora\",\n    \"file\": \"File\",\n    \"filter\": \"Filtro\",\n    \"home\": \"Home\",\n    \"image\": \"Immagine\",\n    \"in\": \"In\",\n    \"inbox\": \"Notifiche\",\n    \"input\": \"Input\",\n    \"language\": \"Lingua\",\n    \"last-updated-at\": \"Ultimo aggiornamento\",\n    \"learn-more\": \"Scopri di più\",\n    \"link\": \"Link\",\n    \"map\": \"Mappa\",\n    \"mark\": \"Cita\",\n    \"memo\": \"Memo\",\n    \"memos\": \"Memo\",\n    \"more\": \"Altro\",\n    \"name\": \"Nome\",\n    \"new\": \"Nuovo\",\n    \"nickname\": \"Nickname\",\n    \"null\": \"Null\",\n    \"or\": \"oppure\",\n    \"password\": \"Password\",\n    \"pin\": \"Fissa\",\n    \"pinned\": \"Fissato\",\n    \"preview\": \"Anteprima\",\n    \"profile\": \"Profilo\",\n    \"properties\": \"Proprietà\",\n    \"referenced-by\": \"Referenziato da\",\n    \"referencing\": \"Referenzia\",\n    \"relations\": \"Relazioni\",\n    \"remember-me\": \"Ricordami\",\n    \"rename\": \"Rinomina\",\n    \"reset\": \"Reset\",\n    \"resources\": \"Risorse\",\n    \"restore\": \"Ripristina\",\n    \"role\": \"Ruolo\",\n    \"save\": \"Salva\",\n    \"search\": \"Cerca\",\n    \"select\": \"Seleziona\",\n    \"settings\": \"Impostazioni\",\n    \"share\": \"Condividi\",\n    \"shortcut-filter\": \"Filtro scorciatoie\",\n    \"shortcuts\": \"Scorciatoie\",\n    \"sign-in\": \"Accedi\",\n    \"sign-in-with\": \"Accedi con {{provider}}\",\n    \"sign-out\": \"Esci\",\n    \"sign-up\": \"Registrati\",\n    \"statistics\": \"Statistiche\",\n    \"tags\": \"Tags\",\n    \"title\": \"Titolo\",\n    \"today\": \"Oggi\",\n    \"tree-mode\": \"Vista ad albero\",\n    \"type\": \"Tipo\",\n    \"unpin\": \"Rimuovi\",\n    \"update\": \"Aggiorna\",\n    \"upload\": \"Carica\",\n    \"user\": \"Utente\",\n    \"username\": \"Nome utente\",\n    \"version\": \"Versione\",\n    \"visibility\": \"Visibilità\",\n    \"yourself\": \"Te stesso\"\n  },\n  \"editor\": {\n    \"add-your-comment-here\": \"Aggiungi qui il tuo commento...\",\n    \"any-thoughts\": \"Qualsiasi cosa pensi...\",\n    \"exit-focus-mode\": \"Esci Modalità Focus\",\n    \"focus-mode\": \"Modalità Focus\",\n    \"no-changes-detected\": \"Nessuna modifica rilevata\",\n    \"save\": \"Salva\",\n    \"saving\": \"Salvataggio...\",\n    \"slash-commands\": \"Digita `/` per i comandi\"\n  },\n  \"inbox\": {\n    \"failed-to-load\": \"Impossibile caricare l'elemento della posta in arrivo\",\n    \"memo-comment\": \"{{user}} ha commentato il tuo {{memo}}.\",\n    \"no-archived\": \"Nessuna notifica archiviata\",\n    \"no-unread\": \"Nessuna notifica non letta\",\n    \"unread\": \"Non letta\"\n  },\n  \"markdown\": {\n    \"checkbox\": \"Casella di controllo\",\n    \"code-block\": \"Blocco di codice\",\n    \"content-syntax\": \"Sintassi contenuto\"\n  },\n  \"memo\": {\n    \"archived-at\": \"Archiviato il\",\n    \"click-to-hide-nsfw-content\": \"Clicca per nascondere contenuti sensibili\",\n    \"click-to-show-nsfw-content\": \"Clicca per mostrare contenuti sensibili\",\n    \"code\": \"Codice\",\n    \"comment\": {\n      \"self\": \"Commenti\",\n      \"write-a-comment\": \"Scrivi un commento\"\n    },\n    \"copy-content\": \"Copia contenuto\",\n    \"copy-link\": \"Copia link\",\n    \"count-memos-in-date\": \"{{count}} {{memos}} in data {{date}}\",\n    \"delete-confirm\": \"Confermi di voler eliminare questo memo?\",\n    \"delete-confirm-description\": \"Questa azione è irreversibile. Allegati, link e riferimenti saranno rimossi.\",\n    \"direction\": \"Direzione\",\n    \"direction-asc\": \"Crescente\",\n    \"direction-desc\": \"Decrescente\",\n    \"display-time\": \"Orario di visualizzazione\",\n    \"filters\": {\n      \"has-code\": \"haCodice\",\n      \"has-link\": \"haLink\",\n      \"has-task-list\": \"haListaCompiti\"\n    },\n    \"links\": \"Link\",\n    \"load-more\": \"Carica altro\",\n    \"no-archived-memos\": \"Nessun memo archiviato.\",\n    \"no-memos\": \"Nessun memo.\",\n    \"order-by\": \"Ordina per\",\n    \"search-placeholder\": \"Cerca memo...\",\n    \"show-less\": \"Mostra meno\",\n    \"show-more\": \"Mostra di più\",\n    \"to-do\": \"Da fare\",\n    \"view-detail\": \"Vedi dettagli\",\n    \"visibility\": {\n      \"disabled\": \"I memo pubblici sono disattivati\",\n      \"private\": \"Privato\",\n      \"protected\": \"Protetto\",\n      \"public\": \"Pubblico\"\n    }\n  },\n  \"message\": {\n    \"archived-successfully\": \"Archiviato con successo\",\n    \"change-memo-created-time\": \"Cambia data creazione memo\",\n    \"copied\": \"Copiato\",\n    \"deleted-successfully\": \"Eliminato con successo\",\n    \"description-is-required\": \"La descrizione è obbligatoria\",\n    \"failed-to-embed-memo\": \"Impossibile incorporare il memo\",\n    \"fill-all\": \"Compila tutti i campi\",\n    \"fill-all-required-fields\": \"Compila tutti i campi obbligatori\",\n    \"maximum-upload-size-is\": \"La dimensione massima di upload è {{size}} MiB\",\n    \"memo-not-found\": \"Memo non trovato\",\n    \"new-password-not-match\": \"Le nuove password non corrispondono\",\n    \"no-data\": \"Nessun dato\",\n    \"password-changed\": \"Password cambiata\",\n    \"password-not-match\": \"Le password non corrispondono\",\n    \"restored-successfully\": \"Ripristinato con successo\",\n    \"succeed-copy-content\": \"Contenuto copiato con successo.\",\n    \"succeed-copy-link\": \"Link copiato.\",\n    \"update-succeed\": \"Aggiornato con successo\",\n    \"user-not-found\": \"Utente non trovato\"\n  },\n  \"reference\": {\n    \"add-references\": \"Aggiungi riferimenti\",\n    \"embedded-usage\": \"Usa come contenuto incorporato\",\n    \"no-memos-found\": \"Nessun memo trovato\",\n    \"search-placeholder\": \"Cerca contenuto\"\n  },\n  \"resource\": {\n    \"clear\": \"Cancella\",\n    \"copy-link\": \"Copia Link\",\n    \"create-dialog\": {\n      \"external-link\": {\n        \"file-name\": \"Nome file\",\n        \"file-name-placeholder\": \"Nome file\",\n        \"link\": \"Link\",\n        \"link-placeholder\": \"https://the.link.to/your/resource\",\n        \"option\": \"Link esterno\",\n        \"type\": \"Tipo\",\n        \"type-placeholder\": \"Tipo file\"\n      },\n      \"local-file\": {\n        \"choose\": \"Carica file...\",\n        \"option\": \"File locale\"\n      },\n      \"title\": \"Nuova risorsa\",\n      \"upload-method\": \"Metodo di caricamento\"\n    },\n    \"delete-all-unused\": \"Elimina tutte le risorse inutilizzate\",\n    \"delete-all-unused-confirm\": \"Confermi di voler eliminare tutte le risorse inutilizzate? QUESTA AZIONE È IRREVERSIBILE\",\n    \"delete-all-unused-error\": \"Impossibile eliminare le risorse inutilizzate\",\n    \"delete-all-unused-success\": \"Risorse eliminate con successo\",\n    \"delete-resource\": \"Elimina risorsa\",\n    \"delete-selected-resources\": \"Elimina risorse selezionate\",\n    \"fetching-data\": \"Recupero dati...\",\n    \"file-drag-drop-prompt\": \"Trascina qui il file per caricarlo\",\n    \"linked-amount\": \"Quantità collegata\",\n    \"no-files-selected\": \"Nessun file selezionato\",\n    \"no-resources\": \"Nessuna risorsa.\",\n    \"no-unused-resources\": \"Nessuna risorsa inutilizzata\",\n    \"reset-link\": \"Ripristina Link\",\n    \"reset-link-prompt\": \"Confermi di voler ripristinare questo link? Questo invaliderà tutti i link esistenti. QUESTA AZIONE È IRREVERSIBILE\",\n    \"reset-resource-link\": \"Ripristina link risorse\",\n    \"unused-resources\": \"Risorse inutilizzate\"\n  },\n  \"router\": {\n    \"back-to-top\": \"Torna su\",\n    \"go-to-home\": \"Vai alla home\"\n  },\n  \"setting\": {\n    \"member\": {\n      \"admin\": \"Admin\",\n      \"archive-member\": \"Archivia membro\",\n      \"archive-success\": \"{{username}} archiviato con successo\",\n      \"archive-warning\": \"Confermi di voler archiviare {{username}}?\",\n      \"archive-warning-description\": \"Archiviare disattiva l'account. Puoi ripristinarlo o eliminarlo in seguito.\",\n      \"create-a-member\": \"Crea un membro\",\n      \"delete-member\": \"Elimina membro\",\n      \"delete-success\": \"{{username}} eliminato con successo\",\n      \"delete-warning\": \"Confermi di voler eliminare {{username}}?\",\n      \"delete-warning-description\": \"QUESTA AZIONE È IRREVERSIBILE\",\n      \"restore-success\": \"{{username}} ripristinato con successo\",\n      \"user\": \"Utente\",\n      \"label\": \"Membri\",\n      \"list-title\": \"Lista membri\"\n    },\n    \"my-account\": {\n      \"label\": \"Il mio account\"\n    },\n    \"preference\": {\n      \"default-memo-sort-option\": \"Ordinamento memo predefinito\",\n      \"default-memo-visibility\": \"Visibilità memo predefinita\",\n      \"theme\": \"Tema\",\n      \"label\": \"Preferenze\"\n    },\n    \"shortcut\": {\n      \"delete-confirm\": \"Confermi di voler eliminare la scorciatoia `{{title}}`?\",\n      \"delete-success\": \"Scorciatoia `{{title}}` eliminata con successo\"\n    },\n    \"sso\": {\n      \"authorization-endpoint\": \"Endpoint autorizzazione\",\n      \"client-id\": \"Client ID\",\n      \"client-secret\": \"Client secret\",\n      \"confirm-delete\": \"Confermi di voler eliminare la configurazione SSO di `{{name}}`? QUESTA AZIONE È IRREVERSIBILE\",\n      \"create-sso\": \"Crea SSO\",\n      \"custom\": \"Custom\",\n      \"delete-sso\": \"Elimina SSO\",\n      \"disabled-password-login-warning\": \"Login con password disabilitato. Fai molta attenzione quando rimuovi un provider di identità.\",\n      \"display-name\": \"Nome visualizzato\",\n      \"identifier\": \"Identificatore\",\n      \"identifier-filter\": \"Filtro identificatore\",\n      \"no-sso-found\": \"Nessun SSO trovato.\",\n      \"redirect-url\": \"URL reindirizzamento\",\n      \"scopes\": \"Scopes\",\n      \"single-sign-on\": \"Configurazione Single Sign-On (SSO) per autenticazione\",\n      \"sso-created\": \"Creato SSO {{name}}\",\n      \"sso-list\": \"Lista SSO\",\n      \"sso-updated\": \"Aggiornato SSO {{name}}\",\n      \"template\": \"Template\",\n      \"token-endpoint\": \"Token endpoint\",\n      \"update-sso\": \"Aggiorna SSO\",\n      \"user-endpoint\": \"User endpoint\",\n      \"label\": \"SSO\"\n    },\n    \"storage\": {\n      \"accesskey\": \"Access key\",\n      \"accesskey-placeholder\": \"Access key / Access ID\",\n      \"bucket\": \"Bucket\",\n      \"bucket-placeholder\": \"Nome bucket\",\n      \"create-a-service\": \"Crea un servizio\",\n      \"create-storage\": \"Crea archiviazione\",\n      \"current-storage\": \"Archiviazione corrente\",\n      \"delete-storage\": \"Elimina archiviazione\",\n      \"endpoint\": \"Endpoint\",\n      \"filepath-template\": \"Template percorso file\",\n      \"local-storage-path\": \"Percorso archiviazione locale\",\n      \"path\": \"Percorso\",\n      \"path-description\": \"Puoi usare le stesse variabili dinamiche dell'archiviazione locale, come {filename}\",\n      \"path-placeholder\": \"custom/path\",\n      \"presign-placeholder\": \"URL pre-firmato, opzionale\",\n      \"region\": \"Regione\",\n      \"region-placeholder\": \"Nome regione\",\n      \"s3-compatible-url\": \"URL compatibile S3\",\n      \"secretkey\": \"Secret key\",\n      \"secretkey-placeholder\": \"Secret key / Access Key\",\n      \"storage-services\": \"Lista servizi di archiviazione\",\n      \"type-database\": \"Database\",\n      \"type-local\": \"Locale\",\n      \"update-a-service\": \"Aggiorna un servizio\",\n      \"update-local-path\": \"Aggiorna percorso archiviazione locale\",\n      \"update-local-path-description\": \"Il percorso di archiviazione locale è un percorso relativo al tuo file database\",\n      \"update-storage\": \"Aggiorna archiviazione\",\n      \"url-prefix\": \"Prefisso URL\",\n      \"url-prefix-placeholder\": \"Prefisso URL custom, opzionale\",\n      \"url-suffix\": \"Suffisso URL\",\n      \"url-suffix-placeholder\": \"Suffisso URL custom, opzionale\",\n      \"warning-text\": \"Confermi di voler eliminare questo servizio di archiviazione \\\"{{name}}\\\"? QUESTA AZIONE È IRREVERSIBILE\",\n      \"label\": \"Archiviazione\"\n    },\n    \"system\": {\n      \"additional-script\": \"Script aggiuntivo\",\n      \"additional-script-placeholder\": \"Codice JS aggiuntivo\",\n      \"additional-style\": \"Stile aggiuntivo\",\n      \"additional-style-placeholder\": \"Codice CSS aggiuntivo\",\n      \"allow-user-signup\": \"Consenti registrazione utente\",\n      \"customize-server\": {\n        \"description\": \"Descrizione\",\n        \"icon-url\": \"URL icona\",\n        \"locale\": \"Locale server\",\n        \"title\": \"Personalizza server\"\n      },\n      \"disable-password-login\": \"Disabilita login password\",\n      \"disable-password-login-final-warning\": \"Digita \\\"CONFIRM\\\" se sai cosa stai facendo.\",\n      \"disable-password-login-warning\": \"Questo disattiverà il login con password per tutti gli utenti. Se i provider di identità smettono di funzionare, non potrai accedere senza ripristinare manualmente l'impostazione nel database. Fai molta attenzione quando rimuovi un provider di identità.\",\n      \"display-with-updated-time\": \"Mostra data/ora ultimo aggiornamento\",\n      \"enable-auto-compact\": \"Abilita auto-compatto\",\n      \"enable-double-click-to-edit\": \"Abilita doppio click per modificare\",\n      \"enable-password-login\": \"Abilita login password\",\n      \"enable-password-login-warning\": \"Questo consentirà l'accesso con password per tutti gli utenti. Continua solo se desideri che gli utenti possano accedere utilizzando sia SSO che password\",\n      \"max-upload-size\": \"Dimensione massima caricamento (MiB)\",\n      \"max-upload-size-hint\": \"Valore consigliato di 32 MiB.\",\n      \"removed-completed-task-list-items\": \"Abilita rimozione dei compiti completati\",\n      \"server-name\": \"Nome server\",\n      \"title\": \"Generale\",\n      \"label\": \"Sistema\"\n    },\n    \"version\": \"Versione\",\n    \"access-token\": {\n      \"access-token-copied-to-clipboard\": \"Token di accesso copiato negli appunti\",\n      \"access-token-deleted\": \"Token di accesso `{{description}}` eliminato\",\n      \"access-token-deletion\": \"Confermi di voler eliminare il token di accesso `{{description}}`?\",\n      \"access-token-deletion-description\": \"Questa azione è irreversibile. Dovrai aggiornare tutti i servizi che utilizzano questo token per usare un nuovo token.\",\n      \"create-dialog\": {\n        \"access-token-created\": \"Token di accesso `{{description}}` creato\",\n        \"create-access-token\": \"Crea token di accesso\",\n        \"created-at\": \"Creato il\",\n        \"description\": \"Descrizione\",\n        \"duration-1m\": \"1 mese\",\n        \"duration-8h\": \"8 ore\",\n        \"duration-never\": \"Mai\",\n        \"expiration\": \"Scadenza\",\n        \"expires-at\": \"Scade il\",\n        \"some-description\": \"Qualche descrizione...\"\n      },\n      \"description\": \"Elenco di tutti i token di accesso del tuo account.\",\n      \"title\": \"Token di accesso\",\n      \"token\": \"Token\"\n    },\n    \"account\": {\n      \"change-password\": \"Cambia password\",\n      \"email-note\": \"Opzionale\",\n      \"export-memos\": \"Esporta memo\",\n      \"nickname-note\": \"Mostrato nel banner\",\n      \"openapi-reset\": \"Ripristina key OpenAPI\",\n      \"openapi-sample-post\": \"Ciao #memos da {{url}}\",\n      \"openapi-title\": \"OpenAPI\",\n      \"reset-api\": \"Ripristina API\",\n      \"title\": \"Il mio account\",\n      \"update-information\": \"Aggiorna informazioni\",\n      \"username-note\": \"Usato per il login\"\n    },\n    \"instance\": {\n      \"disallow-change-nickname\": \"Impedisci cambio nickname\",\n      \"disallow-change-username\": \"Impedisci cambio nome utente\",\n      \"disallow-password-auth\": \"Impedisci autenticazione password\",\n      \"disallow-user-registration\": \"Impedisci registrazione utente\",\n      \"monday\": \"Lunedì\",\n      \"saturday\": \"Sabato\",\n      \"sunday\": \"Domenica\",\n      \"week-start-day\": \"Giorno inizio settimana\"\n    },\n    \"memo\": {\n      \"content-length-limit\": \"Massima lunghezza contenuto (byte)\",\n      \"enable-blur-nsfw-content\": \"Abilita sfocatura contenuti sensibili (NSFW)\",\n      \"enable-memo-comments\": \"Abilita commenti memo\",\n      \"enable-memo-location\": \"Abilita posizione memo\",\n      \"reactions\": \"Reazioni\",\n      \"title\": \"Impostazioni memo\",\n      \"label\": \"Memo\"\n    },\n    \"webhook\": {\n      \"create-dialog\": {\n        \"an-easy-to-remember-name\": \"Nome facile da ricordare\",\n        \"create-webhook\": \"Crea webhook\",\n        \"create-webhook-success\": \"Webhook `{{name}}` creato\",\n        \"edit-webhook\": \"Modifica webhook\",\n        \"payload-url\": \"URL payload\",\n        \"title\": \"Titolo\",\n        \"url-example-post-receive\": \"https://example.com/postreceive\"\n      },\n      \"delete-dialog\": {\n        \"delete-webhook-description\": \"Questa azione è irreversibile.\",\n        \"delete-webhook-success\": \"Webhook `{{name}}` eliminato con successo\",\n        \"delete-webhook-title\": \"Confermi di voler eliminare il webhook `{{name}}`?\"\n      },\n      \"no-webhooks-found\": \"Nessun webhook trovato.\",\n      \"title\": \"Webhooks\",\n      \"url\": \"URL\"\n    }\n  },\n  \"tag\": {\n    \"all-tags\": \"Tutti i Tags\",\n    \"create-tag\": \"Crea Tag\",\n    \"create-tags-guide\": \"Puoi creare tag inserendo `#tag`.\",\n    \"delete-confirm\": \"Confermi di voler eliminare questo tag? Tutti i memo collegati saranno archiviati.\",\n    \"delete-success\": \"Tag eliminato con successo\",\n    \"delete-tag\": \"Elimina Tag\",\n    \"new-name\": \"Nuovo nome\",\n    \"no-tag-found\": \"Nessun tag trovato\",\n    \"old-name\": \"Vecchio nome\",\n    \"rename-error-empty\": \"Il nome del tag non può essere vuoto o contenere spazi\",\n    \"rename-error-repeat\": \"Il nuovo nome non può essere uguale al vecchio\",\n    \"rename-success\": \"Tag rinominato con successo\",\n    \"rename-tag\": \"Rinomina tag\",\n    \"rename-tip\": \"Tutti i tuoi memo con questo tag saranno aggiornati.\"\n  },\n  \"tooltip\": {\n    \"link-memo\": \"Link Memo\",\n    \"markdown-menu\": \"Markdown\",\n    \"select-location\": \"Posizione\",\n    \"select-visibility\": \"Visibilità\",\n    \"tags\": \"Tag\",\n    \"upload-attachment\": \"Carica allegato(i)\"\n  }\n}\n"
  },
  {
    "path": "web/src/locales/ja.json",
    "content": "{\n  \"about\": {\n    \"blogs\": \"ブログ\",\n    \"description\": \"プライバシー重視の軽量ノートサービス。あなたの素晴らしいアイデアを簡単に記録・共有できます。\",\n    \"documents\": \"ドキュメント\",\n    \"github-repository\": \"GitHubリポジトリ\",\n    \"official-website\": \"公式サイト\"\n  },\n  \"auth\": {\n    \"create-your-account\": \"アカウントを作成\",\n    \"host-tip\": \"管理者として登録されます。\",\n    \"new-password\": \"新しいパスワード\",\n    \"repeat-new-password\": \"新しいパスワードを確認(繰り返し)\",\n    \"sign-in-tip\": \"すでにアカウントを持っていますか?\",\n    \"sign-up-tip\": \"アカウントを持っていませんか?\"\n  },\n  \"common\": {\n    \"about\": \"Memos について\",\n    \"add\": \"追加\",\n    \"admin\": \"管理者設定\",\n    \"all\": \"すべて\",\n    \"archive\": \"アーカイブにする\",\n    \"archived\": \"ゴミ箱\",\n    \"attachments\": \"添付ファイル\",\n    \"auto-expand\": \"自動展開\",\n    \"avatar\": \"アイコン\",\n    \"basic\": \"基本設定\",\n    \"beta\": \"ベータ\",\n    \"calendar\": \"カレンダー\",\n    \"cancel\": \"キャンセル\",\n    \"change\": \"変更\",\n    \"clear\": \"クリア\",\n    \"close\": \"閉じる\",\n    \"collapse\": \"折りたたむ\",\n    \"confirm\": \"確認する\",\n    \"copy\": \"コピー\",\n    \"create\": \"作成する\",\n    \"created-at\": \"作成日時\",\n    \"database\": \"データベース\",\n    \"day\": \"日\",\n    \"days\": {\n      \"fri\": \"金\",\n      \"mon\": \"月\",\n      \"sat\": \"土\",\n      \"sun\": \"日\",\n      \"thu\": \"木\",\n      \"tue\": \"火\",\n      \"wed\": \"水\"\n    },\n    \"delete\": \"削除する\",\n    \"description\": \"説明\",\n    \"edit\": \"編集する\",\n    \"email\": \"Eメール\",\n    \"expand\": \"展開\",\n    \"explore\": \"共有メモ\",\n    \"file\": \"ファイル\",\n    \"filter\": \"フィルター\",\n    \"home\": \"ホーム\",\n    \"image\": \"画像\",\n    \"in\": \"内\",\n    \"inbox\": \"受信トレイ\",\n    \"input\": \"入力\",\n    \"language\": \"言語\",\n    \"last-updated-at\": \"最終更新日時\",\n    \"learn-more\": \"さらに詳しく\",\n    \"link\": \"リンク\",\n    \"map\": \"マップ\",\n    \"mark\": \"マーク\",\n    \"memo\": \"メモ\",\n    \"memos\": \"メモ\",\n    \"more\": \"もっと\",\n    \"name\": \"名前\",\n    \"new\": \"新しく追加\",\n    \"nickname\": \"ニックネーム\",\n    \"null\": \"null\",\n    \"or\": \"もしくは\",\n    \"password\": \"パスワード\",\n    \"pin\": \"ピン\",\n    \"pinned\": \"ピン留め\",\n    \"preview\": \"プレビュー\",\n    \"profile\": \"プロファイル\",\n    \"properties\": \"プロパティ\",\n    \"referenced-by\": \"参照元\",\n    \"referencing\": \"参照先\",\n    \"relations\": \"関連\",\n    \"remember-me\": \"パスワードを保存する\",\n    \"rename\": \"リネーム\",\n    \"reset\": \"リセット\",\n    \"resources\": \"ファイル\",\n    \"restore\": \"戻す\",\n    \"role\": \"ロール\",\n    \"save\": \"保存する\",\n    \"search\": \"検索\",\n    \"select\": \"選択\",\n    \"settings\": \"設定\",\n    \"share\": \"シェアする\",\n    \"shortcut-filter\": \"ショートカットフィルター\",\n    \"shortcuts\": \"ショートカット\",\n    \"sign-in\": \"サインイン\",\n    \"sign-in-with\": \"{{provider}}でサインイン\",\n    \"sign-out\": \"ログアウト\",\n    \"sign-up\": \"登録\",\n    \"statistics\": \"統計\",\n    \"tags\": \"タグ\",\n    \"title\": \"タイトル\",\n    \"today\": \"今日\",\n    \"tree-mode\": \"ツリーモード\",\n    \"type\": \"タイプ\",\n    \"unpin\": \"ピンを外す\",\n    \"update\": \"上書き\",\n    \"upload\": \"アップロード\",\n    \"user\": \"ユーザー\",\n    \"username\": \"ユーザーネーム\",\n    \"version\": \"バージョン\",\n    \"visibility\": \"公開範囲\",\n    \"yourself\": \"あなた自身の\"\n  },\n  \"editor\": {\n    \"add-your-comment-here\": \"ここにコメントを追加...\",\n    \"any-thoughts\": \"今思ったことは...\",\n    \"exit-focus-mode\": \"フォーカスを終了\",\n    \"focus-mode\": \"フォーカスモード\",\n    \"no-changes-detected\": \"変更はありません\",\n    \"save\": \"保存\",\n    \"saving\": \"保存中...\",\n    \"slash-commands\": \"コマンドを入力するには `/` を入力\"\n  },\n  \"inbox\": {\n    \"failed-to-load\": \"受信トレイアイテムの読み込みに失敗しました\",\n    \"memo-comment\": \"{{user}} があなたの {{memo}} にコメントしました。\",\n    \"no-archived\": \"アーカイブされた通知はありません\",\n    \"no-unread\": \"未読の通知はありません\",\n    \"unread\": \"未読\"\n  },\n  \"markdown\": {\n    \"checkbox\": \"チェックボックス\",\n    \"code-block\": \"コードブロック\",\n    \"content-syntax\": \"コンテンツ構文\"\n  },\n  \"memo\": {\n    \"archived-at\": \"アーカイブ:\",\n    \"click-to-hide-nsfw-content\": \"クリックしてセンシティブ内容を隠す\",\n    \"click-to-show-nsfw-content\": \"クリックしてセンシティブ内容を表示\",\n    \"code\": \"コード\",\n    \"comment\": {\n      \"self\": \"コメント\",\n      \"write-a-comment\": \"コメントを書く\"\n    },\n    \"copy-content\": \"コンテンツをコピー\",\n    \"copy-link\": \"リンクをコピー\",\n    \"count-memos-in-date\": \"{{count}}件の{{memos}}{{date}}\",\n    \"delete-confirm\": \"本当に削除しますか? この操作は元に戻せません。\",\n    \"delete-confirm-description\": \"この操作は元に戻せません。添付ファイル、リンク、参照も削除されます。\",\n    \"direction\": \"並び順\",\n    \"direction-asc\": \"昇順\",\n    \"direction-desc\": \"降順\",\n    \"display-time\": \"表示時間\",\n    \"filters\": {\n      \"has-code\": \"コードあり\",\n      \"has-link\": \"リンクあり\",\n      \"has-task-list\": \"タスクリストあり\"\n    },\n    \"links\": \"リンク\",\n    \"load-more\": \"さらに読み込む\",\n    \"no-archived-memos\": \"アーカイブされたメモはありません。\",\n    \"no-memos\": \"メモがありません。\",\n    \"order-by\": \"並び替え\",\n    \"search-placeholder\": \"メモを検索...\",\n    \"show-less\": \"少なく表示\",\n    \"show-more\": \"もっと見る\",\n    \"to-do\": \"タスク\",\n    \"view-detail\": \"詳細を見る\",\n    \"visibility\": {\n      \"disabled\": \"公開メモは無効化されています。\",\n      \"private\": \"非公開\",\n      \"protected\": \"メンバーに表示\",\n      \"public\": \"公開メモ\"\n    }\n  },\n  \"message\": {\n    \"archived-successfully\": \"アーカイブが完了しました\",\n    \"change-memo-created-time\": \"メモの作成時間を変更しました\",\n    \"copied\": \"コピーしました！\",\n    \"deleted-successfully\": \"削除されました\",\n    \"description-is-required\": \"説明は必須です\",\n    \"failed-to-embed-memo\": \"メモの埋め込みに失敗しました\",\n    \"fill-all\": \"すべての項目を入力してください。\",\n    \"fill-all-required-fields\": \"すべての必須項目を入力してください\",\n    \"maximum-upload-size-is\": \"ファイルの最大サイズは{{size}} MiBです。\",\n    \"memo-not-found\": \"メモは見つかりませんでした\",\n    \"new-password-not-match\": \"新しいパスワードが一致しません\",\n    \"no-data\": \"データが見つかりませんでした。\",\n    \"password-changed\": \"パスワードを変更しました\",\n    \"password-not-match\": \"パスワードが一致しません。\",\n    \"restored-successfully\": \"リストア成功\",\n    \"succeed-copy-content\": \"コンテンツのコピーに成功しました。\",\n    \"succeed-copy-link\": \"リンクのコピーに成功しました。\",\n    \"update-succeed\": \"変更は保存されました\",\n    \"user-not-found\": \"ユーザーが見つかりませんでした\"\n  },\n  \"reference\": {\n    \"add-references\": \"参照を追加\",\n    \"embedded-usage\": \"埋め込みコンテンツとして使用\",\n    \"no-memos-found\": \"メモが見つかりません\",\n    \"search-placeholder\": \"内容を検索\"\n  },\n  \"resource\": {\n    \"clear\": \"クリア\",\n    \"copy-link\": \"リンクをコピー\",\n    \"create-dialog\": {\n      \"external-link\": {\n        \"file-name\": \"ファイル名\",\n        \"file-name-placeholder\": \"ファイル名\",\n        \"link\": \"リンク\",\n        \"link-placeholder\": \"https://the.link.to/your/resource\",\n        \"option\": \"外部リンク\",\n        \"type\": \"タイプ\",\n        \"type-placeholder\": \"ファイルタイプ\"\n      },\n      \"local-file\": {\n        \"choose\": \"ファイルを選択する…\",\n        \"option\": \"ローカルファイル\"\n      },\n      \"title\": \"リソースを作成\",\n      \"upload-method\": \"アップロード方法\"\n    },\n    \"delete-all-unused\": \"未使用をすべて削除\",\n    \"delete-all-unused-confirm\": \"未使用のリソースをすべて削除してもよろしいですか？この操作は元に戻せません\",\n    \"delete-all-unused-error\": \"未使用リソースの削除に失敗しました\",\n    \"delete-all-unused-success\": \"リソースを削除しました\",\n    \"delete-resource\": \"リソースを削除\",\n    \"delete-selected-resources\": \"選択したリソースを削除\",\n    \"fetching-data\": \"データを取得中…\",\n    \"file-drag-drop-prompt\": \"アップロードするファイルをドラックアンドドロップする\",\n    \"linked-amount\": \"リンクされている数\",\n    \"no-files-selected\": \"ファイルが選択されていません\",\n    \"no-resources\": \"リソースはありません。\",\n    \"no-unused-resources\": \"未使用のリソースはありません\",\n    \"reset-link\": \"リンクをリセット\",\n    \"reset-link-prompt\": \"本当にリンクをリセットしますか？実行すると既存のリンクは無効化されます。この操作は元に戻せません。\",\n    \"reset-resource-link\": \"リソースリンクをリセット\",\n    \"unused-resources\": \"未使用リソース\"\n  },\n  \"router\": {\n    \"back-to-top\": \"トップに戻る\",\n    \"go-to-home\": \"ホームへ戻る\"\n  },\n  \"setting\": {\n    \"member\": {\n      \"admin\": \"管理者\",\n      \"archive-member\": \"メンバーをアーカイブ\",\n      \"archive-success\": \"{{username}} をアーカイブしました\",\n      \"archive-warning\": \"{{username}} をアーカイブしますか？\",\n      \"archive-warning-description\": \"アーカイブするとアカウントが無効になります。あとで復元または削除できます。\",\n      \"create-a-member\": \"メンバーを追加する\",\n      \"delete-member\": \"メンバーを削除\",\n      \"delete-success\": \"{{username}} を削除しました\",\n      \"delete-warning\": \"{{username}} を削除しますか？この操作は元に戻せません。\",\n      \"delete-warning-description\": \"この操作は元に戻せません\",\n      \"restore-success\": \"{{username}} を復元しました\",\n      \"user\": \"ユーザー\",\n      \"label\": \"メンバー\",\n      \"list-title\": \"メンバーリスト\"\n    },\n    \"my-account\": {\n      \"label\": \"アカウント設定\"\n    },\n    \"preference\": {\n      \"default-memo-sort-option\": \"メモの表示時間\",\n      \"default-memo-visibility\": \"公開範囲の初期設定\",\n      \"theme\": \"テーマ\",\n      \"label\": \"設定\"\n    },\n    \"shortcut\": {\n      \"delete-confirm\": \"ショートカット `{{title}}` を削除してもよろしいですか？\",\n      \"delete-success\": \"ショートカット `{{title}}` を削除しました\"\n    },\n    \"sso\": {\n      \"authorization-endpoint\": \"認証エンドポイント\",\n      \"client-id\": \"クライアントID\",\n      \"client-secret\": \"クライアントシークレット\",\n      \"confirm-delete\": \"\\\"{{name}}\\\" のSSO設定を削除しますか？この操作は元に戻せません。\",\n      \"create-sso\": \"SSOを作成する\",\n      \"custom\": \"カスタム\",\n      \"delete-sso\": \"削除を確定\",\n      \"disabled-password-login-warning\": \"パスワードによるログインが無効になっています。IDプロバイダーを削除する際はご注意ください\",\n      \"display-name\": \"表示名\",\n      \"identifier\": \"識別子\",\n      \"identifier-filter\": \"識別子フィルター\",\n      \"no-sso-found\": \"SSOが見つかりません。\",\n      \"redirect-url\": \"リダイレクトURL\",\n      \"scopes\": \"スコープ\",\n      \"single-sign-on\": \"認証用Single Sign-On(SSO)の設定\",\n      \"sso-created\": \"SSO {{name}}を作成しました\",\n      \"sso-list\": \"SSOリスト\",\n      \"sso-updated\": \"SSO {{name}}を更新しました\",\n      \"template\": \"テンプレート\",\n      \"token-endpoint\": \"トークンエンドポイント\",\n      \"update-sso\": \"SSOを更新する\",\n      \"user-endpoint\": \"ユーザーエンドポイント\",\n      \"label\": \"SSO\"\n    },\n    \"storage\": {\n      \"accesskey\": \"アクセスキー\",\n      \"accesskey-placeholder\": \"アクセスキー / アクセスID\",\n      \"bucket\": \"バケット\",\n      \"bucket-placeholder\": \"バケット名\",\n      \"create-a-service\": \"サービスを作成\",\n      \"create-storage\": \"ストレージを作成\",\n      \"current-storage\": \"現在のストレージ\",\n      \"delete-storage\": \"ストレージを削除\",\n      \"endpoint\": \"エンドポイント\",\n      \"filepath-template\": \"ファイルパステンプレート\",\n      \"local-storage-path\": \"ローカルストレージパス\",\n      \"path\": \"ストレージパス\",\n      \"path-description\": \"ローカルストレージと同じ動的変数（例: {filename}）が使えます\",\n      \"path-placeholder\": \"カスタム/パス\",\n      \"presign-placeholder\": \"署名付きURL（オプション）\",\n      \"region\": \"リージョン\",\n      \"region-placeholder\": \"リージョン名\",\n      \"s3-compatible-url\": \"S3互換URL\",\n      \"secretkey\": \"シークレットキー\",\n      \"secretkey-placeholder\": \"シークレットキー / アクセスキー\",\n      \"storage-services\": \"ストレージサービス\",\n      \"type-database\": \"データベース\",\n      \"type-local\": \"ローカルファイルシステム\",\n      \"update-a-service\": \"サービスを更新\",\n      \"update-local-path\": \"ローカルストレージパスを更新\",\n      \"update-local-path-description\": \"ローカルストレージパスはデータベースファイルへの相対パスです\",\n      \"update-storage\": \"ストレージを更新\",\n      \"url-prefix\": \"URLプレフィックス\",\n      \"url-prefix-placeholder\": \"カスタムURLプレフィックス（オプション）\",\n      \"url-suffix\": \"URLサフィックス\",\n      \"url-suffix-placeholder\": \"カスタムURLサフィックス（オプション）\",\n      \"warning-text\": \"ストレージサービス \\\"{{name}}\\\" を削除しますか？この操作は元に戻せません。\",\n      \"label\": \"ストレージ\"\n    },\n    \"system\": {\n      \"additional-script\": \"追加スクリプト\",\n      \"additional-script-placeholder\": \"追加JavaScriptコード\",\n      \"additional-style\": \"追加CSS\",\n      \"additional-style-placeholder\": \"追加CSSコード\",\n      \"allow-user-signup\": \"ユーザー登録を許可\",\n      \"customize-server\": {\n        \"description\": \"説明\",\n        \"icon-url\": \"アイコンURL\",\n        \"locale\": \"サーバーのロケール\",\n        \"title\": \"サーバーをカスタマイズ\"\n      },\n      \"disable-password-login\": \"パスワードログインを無効化\",\n      \"disable-password-login-final-warning\": \"「CONFIRM」と入力してください（自己責任）\",\n      \"disable-password-login-warning\": \"これにより全ユーザーのパスワードログインが無効になります。IDプロバイダーが失敗した場合、DBでこの設定を戻さないとログインできません。IDプロバイダー削除時はご注意ください。\",\n      \"display-with-updated-time\": \"更新日時で表示\",\n      \"enable-auto-compact\": \"自動折りたたみを有効化\",\n      \"enable-double-click-to-edit\": \"ダブルクリック編集を有効化\",\n      \"enable-password-login\": \"パスワードログインを有効化\",\n      \"enable-password-login-warning\": \"これにより全ユーザーのパスワードログインが有効になります。SSOとパスワード両方でログインさせたい場合のみ続行してください\",\n      \"max-upload-size\": \"最大アップロードサイズ(MiB)\",\n      \"max-upload-size-hint\": \"推奨値は32 MiBです。\",\n      \"removed-completed-task-list-items\": \"完了タスクの削除を有効化\",\n      \"server-name\": \"サーバー名\",\n      \"title\": \"全般\",\n      \"label\": \"システム\"\n    },\n    \"version\": \"バージョン\",\n    \"access-token\": {\n      \"access-token-copied-to-clipboard\": \"アクセストークンをクリップボードにコピーしました\",\n      \"access-token-deleted\": \"アクセストークン `{{description}}` を削除しました\",\n      \"access-token-deletion\": \"アクセストークン `{{description}}` を削除しますか？この操作は元に戻せません。\",\n      \"access-token-deletion-description\": \"この操作は元に戻せません。このトークンを使用サービスは新しいトークンを使用するように更新する必要があります。\",\n      \"create-dialog\": {\n        \"access-token-created\": \"アクセストークン `{{description}}` を作成しました\",\n        \"create-access-token\": \"アクセストークンを作成\",\n        \"created-at\": \"作成日時\",\n        \"description\": \"説明\",\n        \"duration-1m\": \"1ヶ月\",\n        \"duration-8h\": \"8時間\",\n        \"duration-never\": \"無期限\",\n        \"expiration\": \"有効期限\",\n        \"expires-at\": \"有効期限\",\n        \"some-description\": \"説明...\"\n      },\n      \"description\": \"あなたのアカウントのすべてのアクセストークン一覧です。\",\n      \"title\": \"アクセストークン\",\n      \"token\": \"トークン\"\n    },\n    \"account\": {\n      \"change-password\": \"パスワードを変更\",\n      \"email-note\": \"オプション\",\n      \"export-memos\": \"メモをエクスポート\",\n      \"nickname-note\": \"バナーに表示されます\",\n      \"openapi-reset\": \"OpenAPI Keyをリセットする\",\n      \"openapi-sample-post\": \"{{url}}より、こんにちは！#memos\",\n      \"openapi-title\": \"OpenAPI\",\n      \"reset-api\": \"APIをリセットする\",\n      \"title\": \"アカウント情報\",\n      \"update-information\": \"プロフィールを編集\",\n      \"username-note\": \"ログインに使用します\"\n    },\n    \"instance\": {\n      \"disallow-change-nickname\": \"ニックネーム変更を禁止\",\n      \"disallow-change-username\": \"ユーザー名変更を禁止\",\n      \"disallow-password-auth\": \"パスワード認証を禁止\",\n      \"disallow-user-registration\": \"ユーザー登録を禁止\",\n      \"monday\": \"月曜日\",\n      \"saturday\": \"土曜日\",\n      \"sunday\": \"日曜日\",\n      \"week-start-day\": \"週の開始日\"\n    },\n    \"memo\": {\n      \"content-length-limit\": \"内容の最大長（バイト）\",\n      \"enable-blur-nsfw-content\": \"センシティブ(NSFW)内容のぼかしを有効化\",\n      \"enable-memo-comments\": \"メモコメントを有効化\",\n      \"enable-memo-location\": \"メモの位置情報を有効化\",\n      \"reactions\": \"リアクション\",\n      \"title\": \"メモ関連設定\",\n      \"label\": \"メモ\"\n    },\n    \"webhook\": {\n      \"create-dialog\": {\n        \"an-easy-to-remember-name\": \"覚えやすい名前\",\n        \"create-webhook\": \"Webhookを作成\",\n        \"create-webhook-success\": \"Webhook `{{name}}` を作成しました\",\n        \"edit-webhook\": \"Webhookを編集\",\n        \"payload-url\": \"ペイロードURL\",\n        \"title\": \"タイトル\",\n        \"url-example-post-receive\": \"https://example.com/postreceive\"\n      },\n      \"delete-dialog\": {\n        \"delete-webhook-description\": \"この操作は元に戻せません。\",\n        \"delete-webhook-success\": \"Webhook `{{name}}` を削除しました\",\n        \"delete-webhook-title\": \"Webhook `{{name}}` を削除してもよろしいですか？\"\n      },\n      \"no-webhooks-found\": \"Webhookが見つかりません。\",\n      \"title\": \"Webhook\",\n      \"url\": \"URL\"\n    }\n  },\n  \"tag\": {\n    \"all-tags\": \"すべてのタグ\",\n    \"create-tag\": \"タグを作成する\",\n    \"create-tags-guide\": \"`#タグ名`と入力することでタグを作成できます。\",\n    \"delete-confirm\": \"このタグを削除しますか? 関連するすべてのメモがアーカイブされます。\",\n    \"delete-success\": \"タグを削除しました\",\n    \"delete-tag\": \"タグを削除\",\n    \"new-name\": \"新しい名前\",\n    \"no-tag-found\": \"タグが見つかりませんでした\",\n    \"old-name\": \"以前の名前\",\n    \"rename-error-empty\": \"タグ名は空やスペースを含めることはできません\",\n    \"rename-error-repeat\": \"新しい名前は以前の名前と同じにはできません\",\n    \"rename-success\": \"タグ名を変更しました\",\n    \"rename-tag\": \"タグ名を変更\",\n    \"rename-tip\": \"このタグが付いたすべてのメモが更新されます。\"\n  },\n  \"tooltip\": {\n    \"link-memo\": \"メモをリンク\",\n    \"markdown-menu\": \"Markdown\",\n    \"select-location\": \"場所\",\n    \"select-visibility\": \"公開範囲\",\n    \"tags\": \"タグ\",\n    \"upload-attachment\": \"添付ファイルをアップロード\"\n  }\n}\n"
  },
  {
    "path": "web/src/locales/ka-GE.json",
    "content": "{\n  \"about\": {\n    \"blogs\": \"ბლოგები\",\n    \"description\": \"კონფიდენციალურობაზე ორიენტირებული, მსუბუქი ჩანაწერების სერვისი. მარტივად დააფიქსირეთ და გააზიარეთ თქვენი საუკეთესო იდეები.\",\n    \"documents\": \"დოკუმენტები\",\n    \"github-repository\": \"GitHub რეპოზიტორია\",\n    \"official-website\": \"ოფიციალური ვებგვერდი\"\n  },\n  \"auth\": {\n    \"create-your-account\": \"შექმენით თქვენი ანგარიში\",\n    \"host-tip\": \"თქვენ რეგისტრირდებით როგორც საიტის მასპინძელი.\",\n    \"new-password\": \"ახალი პაროლი\",\n    \"repeat-new-password\": \"გაიმეორეთ ახალი პაროლი\",\n    \"sign-in-tip\": \"დაქვემდებარებული გაქვთ ანგარიში?\",\n    \"sign-up-tip\": \"ჯერ არ გაქვთ ანგარიში?\"\n  },\n  \"common\": {\n    \"about\": \"შესახებ\",\n    \"add\": \"დამატება\",\n    \"admin\": \"ადმინი\",\n    \"all\": \"ყველა\",\n    \"archive\": \"არქივი\",\n    \"archived\": \"დაარქივებული\",\n    \"attachments\": \"დანართები\",\n    \"auto-expand\": \"ავტომატური გაფართოება\",\n    \"avatar\": \"ავატარი\",\n    \"basic\": \"ძირითადი\",\n    \"beta\": \"ბეტა\",\n    \"calendar\": \"კალენდარი\",\n    \"cancel\": \"გაუქმება\",\n    \"change\": \"ცვლილება\",\n    \"clear\": \"გასუფთავება\",\n    \"close\": \"დახურვა\",\n    \"collapse\": \"შეკუმშვა\",\n    \"confirm\": \"დადასტურება\",\n    \"copy\": \"კოპირება\",\n    \"create\": \"შექმნა\",\n    \"created-at\": \"შექმნილია\",\n    \"database\": \"მონაცემთა ბაზა\",\n    \"day\": \"დღე\",\n    \"days\": {\n      \"fri\": \"პარ\",\n      \"mon\": \"ორშ\",\n      \"sat\": \"შაბ\",\n      \"sun\": \"კვი\",\n      \"thu\": \"ხუთ\",\n      \"tue\": \"სამ\",\n      \"wed\": \"ოთხ\"\n    },\n    \"delete\": \"წაშლა\",\n    \"description\": \"აღწერა\",\n    \"edit\": \"რედაქტირება\",\n    \"email\": \"ელ.ფოსტა\",\n    \"expand\": \"გაფართოება\",\n    \"explore\": \"კვლევა\",\n    \"file\": \"ფაილი\",\n    \"filter\": \"ფილტრი\",\n    \"home\": \"მთავარი\",\n    \"image\": \"სურათი\",\n    \"in\": \"ში\",\n    \"inbox\": \"ინბოქსი\",\n    \"input\": \"შეყვანა\",\n    \"language\": \"ენა\",\n    \"last-updated-at\": \"ბოლო განახლება\",\n    \"learn-more\": \"გაიგეთ მეტი\",\n    \"link\": \"ლინკი\",\n    \"map\": \"რუკა\",\n    \"mark\": \"მონიშვნა\",\n    \"memo\": \"მემო\",\n    \"memos\": \"მემოები\",\n    \"more\": \"მეტი\",\n    \"name\": \"სახელი\",\n    \"new\": \"ახალი\",\n    \"nickname\": \"მეტსახელი\",\n    \"null\": \"NULL\",\n    \"or\": \"ან\",\n    \"password\": \"პაროლი\",\n    \"pin\": \"პინი\",\n    \"pinned\": \"პინული\",\n    \"preview\": \"წინასწარი ნახვა\",\n    \"profile\": \"პროფილი\",\n    \"properties\": \"თვისებები\",\n    \"referenced-by\": \"მიუთითებს\",\n    \"referencing\": \"მითითება\",\n    \"relations\": \"კავშირები\",\n    \"remember-me\": \"დამიმახსოვრე\",\n    \"rename\": \"გადარქმევა\",\n    \"reset\": \"გადატვირთვა\",\n    \"resources\": \"რესურსები\",\n    \"restore\": \"აღდგენა\",\n    \"role\": \"როლი\",\n    \"save\": \"შენახვა\",\n    \"search\": \"ძიება\",\n    \"select\": \"არჩევა\",\n    \"settings\": \"პარამეტრები\",\n    \"share\": \"გაზიარება\",\n    \"shortcut-filter\": \"მოკლე გზების ფილტრი\",\n    \"shortcuts\": \"მოკლე გზები\",\n    \"sign-in\": \"შესვლა\",\n    \"sign-in-with\": \"შედით {{provider}}-ით\",\n    \"sign-out\": \"გამოსვლა\",\n    \"sign-up\": \"რეგისტრაცია\",\n    \"statistics\": \"სტატისტიკა\",\n    \"tags\": \"თეგები\",\n    \"title\": \"სათაური\",\n    \"today\": \"დღეს\",\n    \"tree-mode\": \"ხის რეჟიმი\",\n    \"type\": \"ტიპი\",\n    \"unpin\": \"პინიდან ამოღება\",\n    \"update\": \"განახლება\",\n    \"upload\": \"ატვირთვა\",\n    \"user\": \"მომხმარებელი\",\n    \"username\": \"მომხმარებლის სახელი\",\n    \"version\": \"ვერსია\",\n    \"visibility\": \"ხილვადობა\",\n    \"yourself\": \"თქვენ\"\n  },\n  \"editor\": {\n    \"add-your-comment-here\": \"დაამატეთ თქვენი კომენტარი აქ...\",\n    \"any-thoughts\": \"რამე იდეები...\",\n    \"exit-focus-mode\": \"ფოკუსის რეჟიმიდან გასვლა\",\n    \"focus-mode\": \"ფოკუსის რეჟიმი\",\n    \"no-changes-detected\": \"ცვლილებები არ აღმოჩენილა\",\n    \"save\": \"შენახვა\",\n    \"saving\": \"შენახვა...\",\n    \"slash-commands\": \"შეიყვანეთ `/` ბრძანებებისთვის\"\n  },\n  \"inbox\": {\n    \"failed-to-load\": \"ინბოქსის ელემენტის ჩატვირთვა ვერ მოხერხდა\",\n    \"memo-comment\": \"{{user}}-მა კომენტარი გაუკეთა თქვენს {{memo}}-ს.\",\n    \"no-archived\": \"არ არის დაარქივებული შეტყობინებები\",\n    \"no-unread\": \"არ არის წაუკითხავი შეტყობინებები\",\n    \"unread\": \"წაუკითხავი\"\n  },\n  \"markdown\": {\n    \"checkbox\": \"ჩასანიშნი ველი\",\n    \"code-block\": \"კოდის ბლოკი\",\n    \"content-syntax\": \"კონტენტის სინტაქსი\"\n  },\n  \"memo\": {\n    \"archived-at\": \"დაარქივებულია\",\n    \"click-to-hide-nsfw-content\": \"დააწკაპუნეთ NSFW კონტენტის დასამალად\",\n    \"click-to-show-nsfw-content\": \"დააწკაპუნეთ NSFW კონტენტის საჩვენებლად\",\n    \"code\": \"კოდი\",\n    \"comment\": {\n      \"self\": \"კომენტარები\",\n      \"write-a-comment\": \"კომენტარის დაწერა\"\n    },\n    \"copy-content\": \"კონტენტის კოპირება\",\n    \"copy-link\": \"ლინკის კოპირება\",\n    \"count-memos-in-date\": \"{{count}} {{memos}} {{date}}-ში\",\n    \"delete-confirm\": \"დარწმუნებული ხართ, რომ გინდათ ამ მემოს წაშლა? ეს ქმედება შეუქცევადია\",\n    \"delete-confirm-description\": \"ეს ქმედება შეუქცევადია. დანართები, ლინკები და რეფერენციებიც წაიშლება.\",\n    \"direction\": \"მიმართულება\",\n    \"direction-asc\": \"ზრდადობით\",\n    \"direction-desc\": \"კლებადობით\",\n    \"display-time\": \"ჩვენების დრო\",\n    \"filters\": {\n      \"has-code\": \"კოდიარის\",\n      \"has-link\": \"ლინკიარის\",\n      \"has-task-list\": \"სიისამოცანებია\"\n    },\n    \"links\": \"ლინკები\",\n    \"load-more\": \"მეტის ჩატვირთვა\",\n    \"no-archived-memos\": \"დაარქივებული მემოები არ არის.\",\n    \"no-memos\": \"მემოები არ არის.\",\n    \"order-by\": \"დალაგება\",\n    \"search-placeholder\": \"მემოების ძიება...\",\n    \"show-less\": \"ნაკლების ჩვენება\",\n    \"show-more\": \"მეტის ჩვენება\",\n    \"to-do\": \"სამოქმედო\",\n    \"view-detail\": \"დეტალების ნახვა\",\n    \"visibility\": {\n      \"disabled\": \"საჯარო მემოები გამორთულია\",\n      \"private\": \"პირადი\",\n      \"protected\": \"სამუშაო სივრცე\",\n      \"public\": \"საჯარო\"\n    }\n  },\n  \"message\": {\n    \"archived-successfully\": \"წარმატებით დაარქივდა\",\n    \"change-memo-created-time\": \"მემოს შექმნის დროის შეცვლა\",\n    \"copied\": \"კოპირებული\",\n    \"deleted-successfully\": \"წარმატებით წაიშალა\",\n    \"description-is-required\": \"აღწერა აუცილებელია\",\n    \"failed-to-embed-memo\": \"მემოს ჩასმა ვერ მოხერხდა\",\n    \"fill-all\": \"გთხოვთ შეავსოთ ყველა ველი.\",\n    \"fill-all-required-fields\": \"გთხოვთ შეავსოთ ყველა აუცილებელი ველი\",\n    \"maximum-upload-size-is\": \"მაქსიმალური ატვირთვის ზომა არის {{size}} MiB\",\n    \"memo-not-found\": \"მემო ვერ მოიძებნა.\",\n    \"new-password-not-match\": \"ახალი პაროლები არ ემთხვევა.\",\n    \"no-data\": \"მონაცემები არ მოიძებნა.\",\n    \"password-changed\": \"პაროლი შეიცვალა\",\n    \"password-not-match\": \"პაროლები არ ემთხვევა.\",\n    \"restored-successfully\": \"წარმატებით აღდგა\",\n    \"succeed-copy-content\": \"კონტენტი წარმატებით კოპირდა.\",\n    \"succeed-copy-link\": \"ლინკი წარმატებით კოპირდა.\",\n    \"update-succeed\": \"განახლება წარმატებით დასრულდა\",\n    \"user-not-found\": \"მომხმარებელი ვერ მოიძებნა\"\n  },\n  \"reference\": {\n    \"add-references\": \"რეფერენციის დამატება\",\n    \"embedded-usage\": \"გამოყენება ჩასმულ კონტენტად\",\n    \"no-memos-found\": \"მემოები ვერ მოიძებნა\",\n    \"search-placeholder\": \"კონტენტის ძიება\"\n  },\n  \"resource\": {\n    \"clear\": \"გასუფთავება\",\n    \"copy-link\": \"ლინკის კოპირება\",\n    \"create-dialog\": {\n      \"external-link\": {\n        \"file-name\": \"ფაილის სახელი\",\n        \"file-name-placeholder\": \"ფაილის სახელი\",\n        \"link\": \"ლინკი\",\n        \"link-placeholder\": \"https://the.link.to/your/resource\",\n        \"option\": \"გარე ლინკი\",\n        \"type\": \"ტიპი\",\n        \"type-placeholder\": \"ფაილის ტიპი\"\n      },\n      \"local-file\": {\n        \"choose\": \"აირჩიეთ ფაილი...\",\n        \"option\": \"ლოკალური ფაილი\"\n      },\n      \"title\": \"რესურსის შექმნა\",\n      \"upload-method\": \"ატვირთვის მეთოდი\"\n    },\n    \"delete-all-unused\": \"ყველა გამოუყენებელის წაშლა\",\n    \"delete-all-unused-confirm\": \"დარწმუნებული ხართ, რომ გინდათ ყველა გამოუყენებელი რესურსის წაშლა? ეს ქმედება შეუქცევადია\",\n    \"delete-all-unused-error\": \"გამოუყენებელი რესურსების წაშლა ვერ მოხერხდა\",\n    \"delete-all-unused-success\": \"რესურსები წარმატებით წაიშალა\",\n    \"delete-resource\": \"რესურსის წაშლა\",\n    \"delete-selected-resources\": \"არჩეული რესურსების წაშლა\",\n    \"fetching-data\": \"მონაცემების ჩატვირთვა...\",\n    \"file-drag-drop-prompt\": \"გადმოაგდეთ ფაილი აქ, რომ ატვირთოთ\",\n    \"linked-amount\": \"დაკავშირებული რაოდენობა\",\n    \"no-files-selected\": \"არცერთი ფაილი არჩეული არ არის\",\n    \"no-resources\": \"რესურსები არ არის.\",\n    \"no-unused-resources\": \"გამოუყენებელი რესურსები არ არის\",\n    \"reset-link\": \"ლინკის გადატვირთვა\",\n    \"reset-link-prompt\": \"დარწმუნებული ხართ, რომ გსურთ ლინკის გადატვირთვა? ეს გააუქმებს ყველა მიმდინარე ლინკის გამოყენებას. ეს ქმედება შეუქცევადია\",\n    \"reset-resource-link\": \"რესურსის ლინკის გადატვირთვა\",\n    \"unused-resources\": \"გამოუყენებელი რესურსები\"\n  },\n  \"router\": {\n    \"back-to-top\": \"ზემოთ დაბრუნება\",\n    \"go-to-home\": \"მთავარზე გადასვლა\"\n  },\n  \"setting\": {\n    \"member\": {\n      \"admin\": \"ადმინი\",\n      \"archive-member\": \"წევრის არქივირება\",\n      \"archive-success\": \"{{username}} წარმატებით დაარქივდა\",\n      \"archive-warning\": \"დარწმუნებული ხართ, რომ გინდათ {{username}}-ის არქივირება?\",\n      \"archive-warning-description\": \"არქივირება დეაქტივირებს ანგარიშს. თქვენ შეგიძლიათ აღადგინოთ ან წაშალოთ მოგვიანებით.\",\n      \"create-a-member\": \"წევრის შექმნა\",\n      \"delete-member\": \"წევრის წაშლა\",\n      \"delete-success\": \"{{username}} წარმატებით წაიშალა\",\n      \"delete-warning\": \"დარწმუნებული ხართ, რომ გინდათ {{username}}-ის წაშლა? ეს ქმედება შეუქცევადია\",\n      \"delete-warning-description\": \"ეს ქმედება შეუქცევადია\",\n      \"restore-success\": \"{{username}} წარმატებით აღდგა\",\n      \"user\": \"მომხმარებელი\",\n      \"label\": \"წევრი\",\n      \"list-title\": \"წევრების სია\"\n    },\n    \"my-account\": {\n      \"label\": \"ჩემი ანგარიში\"\n    },\n    \"preference\": {\n      \"default-memo-sort-option\": \"მემოს ჩვენების დრო\",\n      \"default-memo-visibility\": \"მემოს ნაგულისხმევი ხილვადობა\",\n      \"theme\": \"თემა\",\n      \"label\": \"პარამეტრები\"\n    },\n    \"shortcut\": {\n      \"delete-confirm\": \"დარწმუნებული ხართ, რომ გინდათ მოკლე გზის `{{title}}` წაშლა?\",\n      \"delete-success\": \"მოკლე გზა `{{title}}` წარმატებით წაიშალა\"\n    },\n    \"sso\": {\n      \"authorization-endpoint\": \"ავტორიზაციის წერტილი\",\n      \"client-id\": \"კლიენტის ID\",\n      \"client-secret\": \"კლიენტის საიდუმლო\",\n      \"confirm-delete\": \"დარწმუნებული ხართ, რომ გინდათ \\\"{{name}}\\\" SSO კონფიგურაციის წაშლა? ეს ქმედება შეუქცევადია\",\n      \"create-sso\": \"SSO-ს შექმნა\",\n      \"custom\": \"მორგებული\",\n      \"delete-sso\": \"წაშლის დადასტურება\",\n      \"disabled-password-login-warning\": \"პაროლის-შესვლის ფუნქცია გამორთულია, ყურადღებით იყავით იდენტობის პროვაიდერების წაშლისას\",\n      \"display-name\": \"გამოჩენილი სახელი\",\n      \"identifier\": \"იდენტიფიკატორი\",\n      \"identifier-filter\": \"იდენტიფიკატორის ფილტრი\",\n      \"no-sso-found\": \"SSO ვერ მოიძებნა.\",\n      \"redirect-url\": \"გადამისამართების URL\",\n      \"scopes\": \"სკოპები\",\n      \"single-sign-on\": \"SSO-ს კონფიგურაცია ავთენტიკაციისთვის\",\n      \"sso-created\": \"SSO {{name}} შეიქმნა\",\n      \"sso-list\": \"SSO სია\",\n      \"sso-updated\": \"SSO {{name}} განახლდა\",\n      \"template\": \"შაბლონი\",\n      \"token-endpoint\": \"ტოკენის წერტილი\",\n      \"update-sso\": \"SSO-ს განახლება\",\n      \"user-endpoint\": \"მომხმარებლის წერტილი\",\n      \"label\": \"SSO\"\n    },\n    \"storage\": {\n      \"accesskey\": \"წვდომის გასაღები\",\n      \"accesskey-placeholder\": \"წვდომის გასაღები / წვდომის ID\",\n      \"bucket\": \"ბაკეტი\",\n      \"bucket-placeholder\": \"ბაკეტის სახელი\",\n      \"create-a-service\": \"სერვისის შექმნა\",\n      \"create-storage\": \"შენახვის შექმნა\",\n      \"current-storage\": \"მიმდინარე ობიექტის შენახვა\",\n      \"delete-storage\": \"შენახვის წაშლა\",\n      \"endpoint\": \"წერტილი\",\n      \"filepath-template\": \"ფაილის გზის შაბლონი\",\n      \"local-storage-path\": \"ლოკალური შენახვის გზა\",\n      \"path\": \"შენახვის გზა\",\n      \"path-description\": \"შეგიძლიათ გამოიყენოთ იგივე დინამიური ცვლადები, როგორც ლოკალური შენახვისთვის, როგორიცაა {filename}\",\n      \"path-placeholder\": \"მორგებული/გზა\",\n      \"presign-placeholder\": \"წინასწარი ხელმოწერის URL, არასავალდებულო\",\n      \"region\": \"რეგიონი\",\n      \"region-placeholder\": \"რეგიონის სახელი\",\n      \"s3-compatible-url\": \"S3 თავსებადი URL\",\n      \"secretkey\": \"საიდუმლო გასაღები\",\n      \"secretkey-placeholder\": \"საიდუმლო გასაღები / წვდომის გასაღები\",\n      \"storage-services\": \"შენახვის სერვისები\",\n      \"type-database\": \"მონაცემთა ბაზა\",\n      \"type-local\": \"ლოკალური ფაილური სისტემა\",\n      \"update-a-service\": \"სერვისის განახლება\",\n      \"update-local-path\": \"ლოკალური შენახვის გზის განახლება\",\n      \"update-local-path-description\": \"ლოკალური შენახვის გზა არის თქვენი მონაცემთა ბაზის ფაილის მიმართებითი გზა\",\n      \"update-storage\": \"შენახვის განახლება\",\n      \"url-prefix\": \"URL პრეფიქსი\",\n      \"url-prefix-placeholder\": \"მორგებული URL პრეფიქსი, არასავალდებულო\",\n      \"url-suffix\": \"URL სუფიქსი\",\n      \"url-suffix-placeholder\": \"მორგებული URL სუფიქსი, არასავალდებულო\",\n      \"warning-text\": \"დარწმუნებული ხართ, რომ გინდათ „{{name}}“ შენახვის სერვისის წაშლა? ეს ქმედება შეუქცევადია\",\n      \"label\": \"შენახვა\"\n    },\n    \"system\": {\n      \"additional-script\": \"დამატებითი სკრიპტი\",\n      \"additional-script-placeholder\": \"დამატებითი JavaScript კოდი\",\n      \"additional-style\": \"დამატებითი სტილი\",\n      \"additional-style-placeholder\": \"დამატებითი CSS კოდი\",\n      \"allow-user-signup\": \"მომხმარებლის რეგისტრაციის ნებართვა\",\n      \"customize-server\": {\n        \"description\": \"აღწერა\",\n        \"icon-url\": \"ხატულას URL\",\n        \"locale\": \"სერვერის ლოკალიზაცია\",\n        \"title\": \"სერვერის მორგება\"\n      },\n      \"disable-password-login\": \"პაროლის-შესვლის ფუნქციის გამორთვა\",\n      \"disable-password-login-final-warning\": \"გთხოვთ აკრიფოთ „CONFIRM“, თუ იცით, რას აკეთებთ.\",\n      \"disable-password-login-warning\": \"ეს გამორთავს პაროლის შესვლას ყველა მომხმარებლისთვის. შეუძლებელია შესვლა ამ პარამეტრის მონაცემთა ბაზაში დაბრუნების გარეშე, თუ თქვენი კონფიგურირებული იდენტობის პროვაიდერები ვერ იმუშავებენ. ასევე უნდა იყოთ ყურადღებით, როდესაც იდენტობის პროვაიდერს წაშლით\",\n      \"display-with-updated-time\": \"განახლებული დროით ჩვენება\",\n      \"enable-auto-compact\": \"ავტომატური კომპაქტირების ჩართვა\",\n      \"enable-double-click-to-edit\": \"ორმაგი დაწკაპუნებით რედაქტირების ჩართვა\",\n      \"enable-password-login\": \"პაროლით შესვლის ჩართვა\",\n      \"enable-password-login-warning\": \"ეს ჩართავს პაროლით შესვლას ყველა მომხმარებლისთვის. გააგრძელეთ მხოლოდ იმ შემთხვევაში, თუ გინდათ, რომ მომხმარებლებს შეეძლოთ შესვლა როგორც SSO-ს, ასევე პაროლით\",\n      \"max-upload-size\": \"მაქსიმალური ატვირთვის ზომა (MiB)\",\n      \"max-upload-size-hint\": \"რეკომენდირებული ზომა არის 32 MiB.\",\n      \"removed-completed-task-list-items\": \"დასრულებული ამოცანების წაშლის ჩართვა\",\n      \"server-name\": \"სერვერის სახელი\",\n      \"title\": \"ზოგადი\",\n      \"label\": \"სისტემა\"\n    },\n    \"version\": \"ვერსია\",\n    \"access-token\": {\n      \"access-token-copied-to-clipboard\": \"წვდომის ტოკენი დაკოპირდა\",\n      \"access-token-deleted\": \"წვდომის ტოკენი `{{description}}` წაიშალა\",\n      \"access-token-deletion\": \"დარწმუნებული ხართ, რომ გსურთ წაშალოთ წვდომის ტოკენი {{description}}? ეს ქმედება შეუქცევადია.\",\n      \"access-token-deletion-description\": \"ეს ქმედება შეუქცევადია. თქვენ დაგჭირდებათ განაახლოთ ყველა სერვისი, რომელიც იყენებს ამ ტოკენს, რომ გამოიყენოს ახალი ტოკენი.\",\n      \"create-dialog\": {\n        \"access-token-created\": \"წვდომის ტოკენი `{{description}}` შეიქმნა\",\n        \"create-access-token\": \"წვდომის ტოკენის შექმნა\",\n        \"created-at\": \"შექმნილია\",\n        \"description\": \"აღწერა\",\n        \"duration-1m\": \"1 თვე\",\n        \"duration-8h\": \"8 საათი\",\n        \"duration-never\": \"არასდროს\",\n        \"expiration\": \"ვადა\",\n        \"expires-at\": \"ვადა იწურება\",\n        \"some-description\": \"აღწერა...\"\n      },\n      \"description\": \"თქვენი ანგარიშის ყველა წვდომის ტოკენის სია.\",\n      \"title\": \"წვდომის ტოკენები\",\n      \"token\": \"ტოკენი\"\n    },\n    \"account\": {\n      \"change-password\": \"პაროლის შეცვლა\",\n      \"email-note\": \"არასავალდებულო\",\n      \"export-memos\": \"მემოების ექსპორტი\",\n      \"nickname-note\": \"გამოჩენილი ბანერზე\",\n      \"openapi-reset\": \"OpenAPI გასაღების გადატვირთვა\",\n      \"openapi-sample-post\": \"გამარჯობა #მემოები {{url}}-დან\",\n      \"openapi-title\": \"OpenAPI\",\n      \"reset-api\": \"API-ს გადატვირთვა\",\n      \"title\": \"ანგარიშის ინფორმაცია\",\n      \"update-information\": \"ინფორმაციის განახლება\",\n      \"username-note\": \"გამოიყენება შესასვლელად\"\n    },\n    \"instance\": {\n      \"disallow-change-nickname\": \"მეტსახელის შეცვლის აკრძალვა\",\n      \"disallow-change-username\": \"მომხმარებლის სახელის შეცვლის აკრძალვა\",\n      \"disallow-password-auth\": \"პაროლით ავთენტიკაციის აკრძალვა\",\n      \"disallow-user-registration\": \"მომხმარებლის რეგისტრაციის აკრძალვა\",\n      \"monday\": \"ორშაბათი\",\n      \"saturday\": \"შაბათი\",\n      \"sunday\": \"კვირა\",\n      \"week-start-day\": \"კვირის დაწყების დღე\"\n    },\n    \"memo\": {\n      \"content-length-limit\": \"კონტენტის სიგრძის ლიმიტი (ბაიტი)\",\n      \"enable-blur-nsfw-content\": \"NSFW კონტენტის დაბუნდოვანების ჩართვა\",\n      \"enable-memo-comments\": \"მემოზე კომენტარების ჩართვა\",\n      \"enable-memo-location\": \"მემოს მდებარეობის ჩართვა\",\n      \"reactions\": \"რეაქციები\",\n      \"title\": \"მემოს პარამეტრები\",\n      \"label\": \"მემო\"\n    },\n    \"webhook\": {\n      \"create-dialog\": {\n        \"an-easy-to-remember-name\": \"მარტივად დასამახსოვრებელი სახელი\",\n        \"create-webhook\": \"Webhook-ის შექმნა\",\n        \"create-webhook-success\": \"Webhook `{{name}}` შეიქმნა\",\n        \"edit-webhook\": \"Webhook-ის რედაქტირება\",\n        \"payload-url\": \"Payload URL\",\n        \"title\": \"სათაური\",\n        \"url-example-post-receive\": \"https://example.com/postreceive\"\n      },\n      \"delete-dialog\": {\n        \"delete-webhook-description\": \"ეს ქმედება შეუქცევადია.\",\n        \"delete-webhook-success\": \"Webhook `{{name}}` წარმატებით წაიშალა\",\n        \"delete-webhook-title\": \"დარწმუნებული ხართ, რომ გინდათ webhook `{{name}}-ის` წაშლა?\"\n      },\n      \"no-webhooks-found\": \"Webhook-ები ვერ მოიძებნა.\",\n      \"title\": \"Webhook-ები\",\n      \"url\": \"URL\"\n    }\n  },\n  \"tag\": {\n    \"all-tags\": \"ყველა თეგი\",\n    \"create-tag\": \"თეგის შექმნა\",\n    \"create-tags-guide\": \"თეგების შექმნა შეგიძლიათ `#tag`-ის შეყვანით.\",\n    \"delete-confirm\": \"დარწმუნებული ხართ, რომ გსურთ ამ თეგის წაშლა? ყველა დაკავშირებული მემო არქივირდება.\",\n    \"delete-success\": \"თეგი წარმატებით წაიშალა\",\n    \"delete-tag\": \"თეგის წაშლა\",\n    \"new-name\": \"ახალი სახელი\",\n    \"no-tag-found\": \"თეგი ვერ მოიძებნა\",\n    \"old-name\": \"ძველი სახელი\",\n    \"rename-error-empty\": \"თეგის სახელი არ შეიძლება იყოს ცარიელი ან შეიცავდეს გამოტოვებებს\",\n    \"rename-error-repeat\": \"ახალი სახელი არ შეიძლება ემთხვეოდეს ძველ სახელს\",\n    \"rename-success\": \"თეგი წარმატებით გადაერქვა\",\n    \"rename-tag\": \"თეგის გადარქმევა\",\n    \"rename-tip\": \"თქვენი ყველა მემო ამ თეგით განახლდება.\"\n  },\n  \"tooltip\": {\n    \"link-memo\": \"მემოს დაკავშირება\",\n    \"markdown-menu\": \"Markdown\",\n    \"select-location\": \"მდებარეობა\",\n    \"select-visibility\": \"ხილვადობა\",\n    \"tags\": \"თეგები\",\n    \"upload-attachment\": \"დანართ(ებ)ის ატვირთვა\"\n  }\n}\n"
  },
  {
    "path": "web/src/locales/ko.json",
    "content": "{\n  \"about\": {\n    \"blogs\": \"블로그\",\n    \"description\": \"개인정보 보호를 우선시하는 가벼운 노트 서비스입니다. 멋진 생각을 쉽게 기록하고 공유하세요.\",\n    \"documents\": \"문서\",\n    \"github-repository\": \"GitHub 저장소\",\n    \"official-website\": \"공식 웹사이트\"\n  },\n  \"auth\": {\n    \"create-your-account\": \"계정 만들기\",\n    \"host-tip\": \"사이트 주인으로서의 계정을 등록합니다.\",\n    \"new-password\": \"새 비밀번호\",\n    \"repeat-new-password\": \"새 비밀번호 재확인\",\n    \"sign-in-tip\": \"이미 계정이 있으신가요?\",\n    \"sign-up-tip\": \"아직 계정이 없으신가요?\"\n  },\n  \"common\": {\n    \"about\": \"정보\",\n    \"add\": \"추가\",\n    \"admin\": \"관리\",\n    \"all\": \"전체\",\n    \"archive\": \"보관 처리\",\n    \"archived\": \"보관 목록\",\n    \"attachments\": \"첨부파일\",\n    \"auto-expand\": \"자동 펼치기\",\n    \"avatar\": \"아바타\",\n    \"basic\": \"기본\",\n    \"beta\": \"베타\",\n    \"calendar\": \"달력\",\n    \"cancel\": \"취소\",\n    \"change\": \"변경\",\n    \"clear\": \"정리하기\",\n    \"close\": \"닫기\",\n    \"collapse\": \"접기\",\n    \"confirm\": \"확인\",\n    \"copy\": \"복사\",\n    \"create\": \"생성\",\n    \"created-at\": \"생성일\",\n    \"database\": \"데이터베이스\",\n    \"day\": \"일\",\n    \"days\": {\n      \"fri\": \"금\",\n      \"mon\": \"월\",\n      \"sat\": \"토\",\n      \"sun\": \"일\",\n      \"thu\": \"목\",\n      \"tue\": \"화\",\n      \"wed\": \"수\"\n    },\n    \"delete\": \"삭제\",\n    \"description\": \"설명\",\n    \"edit\": \"편집\",\n    \"email\": \"이메일\",\n    \"expand\": \"펼치기\",\n    \"explore\": \"탐색\",\n    \"file\": \"파일\",\n    \"filter\": \"필터\",\n    \"home\": \"홈\",\n    \"image\": \"이미지\",\n    \"in\": \"내\",\n    \"inbox\": \"알림함\",\n    \"input\": \"입력\",\n    \"language\": \"언어\",\n    \"last-updated-at\": \"마지막 수정일\",\n    \"learn-more\": \"더 보기\",\n    \"link\": \"링크\",\n    \"map\": \"지도\",\n    \"mark\": \"연결\",\n    \"memo\": \"메모\",\n    \"memos\": \"메모\",\n    \"more\": \"더보기\",\n    \"name\": \"이름\",\n    \"new\": \"신규\",\n    \"nickname\": \"닉네임\",\n    \"null\": \"비어 있음\",\n    \"or\": \"또는\",\n    \"password\": \"비밀번호\",\n    \"pin\": \"고정\",\n    \"pinned\": \"고정됨\",\n    \"preview\": \"미리 보기\",\n    \"profile\": \"프로필\",\n    \"properties\": \"속성\",\n    \"referenced-by\": \"참조됨\",\n    \"referencing\": \"참조중\",\n    \"relations\": \"관계\",\n    \"remember-me\": \"로그인 정보 기억하기\",\n    \"rename\": \"이름 바꾸기\",\n    \"reset\": \"재설정\",\n    \"resources\": \"리소스\",\n    \"restore\": \"복원\",\n    \"role\": \"역할\",\n    \"save\": \"저장\",\n    \"search\": \"검색\",\n    \"select\": \"선택\",\n    \"settings\": \"설정\",\n    \"share\": \"공유\",\n    \"shortcut-filter\": \"단축키 필터\",\n    \"shortcuts\": \"단축키\",\n    \"sign-in\": \"로그인\",\n    \"sign-in-with\": \"{{provider}}(으)로 로그인\",\n    \"sign-out\": \"로그아웃\",\n    \"sign-up\": \"회원등록\",\n    \"statistics\": \"통계\",\n    \"tags\": \"태그\",\n    \"title\": \"제목\",\n    \"today\": \"오늘\",\n    \"tree-mode\": \"트리 모드\",\n    \"type\": \"타입\",\n    \"unpin\": \"고정 해제\",\n    \"update\": \"수정\",\n    \"upload\": \"업로드\",\n    \"user\": \"사용자\",\n    \"username\": \"유저네임\",\n    \"version\": \"버전\",\n    \"visibility\": \"공개 범위\",\n    \"yourself\": \"자기 자신\"\n  },\n  \"editor\": {\n    \"add-your-comment-here\": \"여기에 댓글을 추가하세요...\",\n    \"any-thoughts\": \"떠오르는 게 있나요...\",\n    \"exit-focus-mode\": \"집중 모드 종료\",\n    \"focus-mode\": \"집중 모드\",\n    \"no-changes-detected\": \"변경사항이 없습니다\",\n    \"save\": \"저장\",\n    \"saving\": \"저장 중...\",\n    \"slash-commands\": \"명령어를 보시려면 /를 입력하세요.\"\n  },\n  \"inbox\": {\n    \"failed-to-load\": \"알림함 항목을 불러오지 못했습니다\",\n    \"memo-comment\": \"{{user}}님이 {{memo}}에 댓글을 남겼습니다.\",\n    \"no-archived\": \"보관된 알림이 없습니다\",\n    \"no-unread\": \"읽지 않은 알림이 없습니다\",\n    \"unread\": \"읽지 않음\"\n  },\n  \"markdown\": {\n    \"checkbox\": \"체크박스\",\n    \"code-block\": \"코드 블록\",\n    \"content-syntax\": \"내용 문법\"\n  },\n  \"memo\": {\n    \"archived-at\": \"보관된 날짜\",\n    \"click-to-hide-nsfw-content\": \"민감한(성인) 콘텐츠 숨기기\",\n    \"click-to-show-nsfw-content\": \"민감한(성인) 콘텐츠 보기\",\n    \"code\": \"코드\",\n    \"comment\": {\n      \"self\": \"댓글\",\n      \"write-a-comment\": \"댓글 작성\"\n    },\n    \"copy-content\": \"콘텐츠 복사\",\n    \"copy-link\": \"링크 복사\",\n    \"count-memos-in-date\": \"{{date}}에 {{count}}개의 {{memos}}\",\n    \"delete-confirm\": \"이 메모를 삭제하시겠습니까?\",\n    \"delete-confirm-description\": \"이 행동은 되돌릴 수 없으며, 첨부파일, 링크 및 참조가 삭제됩니다.\",\n    \"direction\": \"정렬\",\n    \"direction-asc\": \"오름차순\",\n    \"direction-desc\": \"내림차순\",\n    \"display-time\": \"표시 시간\",\n    \"filters\": {\n      \"has-code\": \"코드있음\",\n      \"has-link\": \"링크있음\",\n      \"has-task-list\": \"할일목록있음\"\n    },\n    \"links\": \"링크\",\n    \"load-more\": \"더보기\",\n    \"no-archived-memos\": \"보관처리된 메모가 없습니다.\",\n    \"no-memos\": \"메모가 없습니다.\",\n    \"order-by\": \"정렬 기준\",\n    \"search-placeholder\": \"메모 검색하기...\",\n    \"show-less\": \"간략히 보기\",\n    \"show-more\": \"더보기\",\n    \"to-do\": \"할 일\",\n    \"view-detail\": \"자세히 보기\",\n    \"visibility\": {\n      \"disabled\": \"공개 메모는 비활성화됨\",\n      \"private\": \"나만 볼 수 있음\",\n      \"protected\": \"멤버 전용\",\n      \"public\": \"공개\"\n    }\n  },\n  \"message\": {\n    \"archived-successfully\": \"성공적으로 보관되었습니다\",\n    \"change-memo-created-time\": \"메모 생성 시각 변경\",\n    \"copied\": \"복사했습니다\",\n    \"deleted-successfully\": \"성공적으로 삭제되었습니다\",\n    \"description-is-required\": \"설명이 필요합니다\",\n    \"failed-to-embed-memo\": \"메모 임베드에 실패했습니다\",\n    \"fill-all\": \"모든 칸을 채워 주세요.\",\n    \"fill-all-required-fields\": \"필수 항목을 모두 입력하세요\",\n    \"maximum-upload-size-is\": \"업로드 최대 크기는 {{size}} MiB입니다\",\n    \"memo-not-found\": \"메모를 찾을 수 없습니다.\",\n    \"new-password-not-match\": \"새 비밀번호가 서로 맞지 않습니다.\",\n    \"no-data\": \"데이터가 없습니다.\",\n    \"password-changed\": \"비밀번호를 변경했습니다\",\n    \"password-not-match\": \"비밀번호가 맞지 않습니다.\",\n    \"restored-successfully\": \"성공적으로 복구했습니다\",\n    \"succeed-copy-content\": \"콘텐츠를 클립보드에 복사했습니다.\",\n    \"succeed-copy-link\": \"링크를 클립보드에 복사했습니다.\",\n    \"update-succeed\": \"업데이트 성공\",\n    \"user-not-found\": \"회원을 찾을 수 없습니다\"\n  },\n  \"reference\": {\n    \"add-references\": \"참조 추가\",\n    \"embedded-usage\": \"임베드 콘텐츠로 사용\",\n    \"no-memos-found\": \"메모를 찾을 수 없습니다\",\n    \"search-placeholder\": \"내용 검색\"\n  },\n  \"resource\": {\n    \"clear\": \"클리어\",\n    \"copy-link\": \"링크 복사\",\n    \"create-dialog\": {\n      \"external-link\": {\n        \"file-name\": \"파일 이름\",\n        \"file-name-placeholder\": \"파일 이름\",\n        \"link\": \"링크\",\n        \"link-placeholder\": \"https://the.link.to/your/resource\",\n        \"option\": \"외부 링크\",\n        \"type\": \"종류\",\n        \"type-placeholder\": \"파일 종류\"\n      },\n      \"local-file\": {\n        \"choose\": \"파일을 선택하세요…\",\n        \"option\": \"내 컴퓨터에서\"\n      },\n      \"title\": \"리소스 생성\",\n      \"upload-method\": \"업로드 방법\"\n    },\n    \"delete-all-unused\": \"사용하지 않는 항목 모두 삭제\",\n    \"delete-all-unused-confirm\": \"사용하지 않는 모든 리소스를 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.\",\n    \"delete-all-unused-error\": \"사용하지 않는 리소스 삭제에 실패했습니다\",\n    \"delete-all-unused-success\": \"리소스가 성공적으로 삭제되었습니다\",\n    \"delete-resource\": \"리소스 삭제\",\n    \"delete-selected-resources\": \"선택된 리소스 삭제\",\n    \"fetching-data\": \"불러오는 중…\",\n    \"file-drag-drop-prompt\": \"업로드할 파일을 여기에 드래그하세요\",\n    \"linked-amount\": \"연결된 수\",\n    \"no-files-selected\": \"파일을 선택하지 않았습니다\",\n    \"no-resources\": \"리소스 없음.\",\n    \"no-unused-resources\": \"사용되지 않는 리소스 없음\",\n    \"reset-link\": \"링크 초기화\",\n    \"reset-link-prompt\": \"이 리소스의 링크를 초기화하시겠습니까? 모든 현재 링크 사용이 끊어집니다. 이 행동은 되돌릴 수 없습니다\",\n    \"reset-resource-link\": \"리소스 링크 초기화\",\n    \"unused-resources\": \"사용되지 않는 리소스\"\n  },\n  \"router\": {\n    \"back-to-top\": \"맨 위로 돌아가기\",\n    \"go-to-home\": \"홈으로\"\n  },\n  \"setting\": {\n    \"member\": {\n      \"admin\": \"관리자\",\n      \"archive-member\": \"멤버 보관처리\",\n      \"archive-success\": \"{{username}}님이 성공적으로 보관처리되었습니다\",\n      \"archive-warning\": \"멤버 {{username}}의 계정을 보관처리하시겠습니까?\",\n      \"archive-warning-description\": \"보관처리하면 해당 계정이 비활성화됩니다. 나중에 복구하거나 삭제할 수 있습니다.\",\n      \"create-a-member\": \"새 멤버 등록\",\n      \"delete-member\": \"멤버 계정 삭제\",\n      \"delete-success\": \"{{username}}님이 성공적으로 삭제되었습니다\",\n      \"delete-warning\": \"멤버 {{username}}의 계정을 완전히 삭제하시겠습니까? 이 행동은 되돌릴 수 없습니다.\",\n      \"delete-warning-description\": \"이 작업은 되돌릴 수 없습니다.\",\n      \"restore-success\": \"{{username}}님이 성공적으로 복구되었습니다\",\n      \"user\": \"사용자\",\n      \"label\": \"멤버\",\n      \"list-title\": \"멤버 목록\"\n    },\n    \"my-account\": {\n      \"label\": \"내 계정\"\n    },\n    \"preference\": {\n      \"default-memo-sort-option\": \"메모에 표시할 시각\",\n      \"default-memo-visibility\": \"메모 공개 범위 기본값\",\n      \"theme\": \"테마\",\n      \"label\": \"개인 설정\"\n    },\n    \"shortcut\": {\n      \"delete-confirm\": \"바로가기 `{{title}}`를 정말로 삭제하시겠습니까?\",\n      \"delete-success\": \"바로가기 `{{title}}`가 성공적으로 삭제되었습니다\"\n    },\n    \"sso\": {\n      \"authorization-endpoint\": \"인증 엔드포인트\",\n      \"client-id\": \"클라이언트 ID\",\n      \"client-secret\": \"클라이언트 비밀키\",\n      \"confirm-delete\": \"SSO 설정 \\\"{{name}}\\\"을 완전히 삭제하시겠습니까? 이 행동은 되돌릴 수 없습니다\",\n      \"create-sso\": \"SSO 생성\",\n      \"custom\": \"사용자 지정\",\n      \"delete-sso\": \"삭제 확인\",\n      \"disabled-password-login-warning\": \"비밀번호 로그인이 비활성화되어 있습니다. SSO를 지울 때에는 특히 조심해주세요\",\n      \"display-name\": \"보여줄 이름\",\n      \"identifier\": \"식별자\",\n      \"identifier-filter\": \"식별자 필터\",\n      \"no-sso-found\": \"SSO가 없습니다.\",\n      \"redirect-url\": \"리다이렉트 URL\",\n      \"scopes\": \"스코프\",\n      \"single-sign-on\": \"인증을 위한 Single Sign-On(SSO) 구성\",\n      \"sso-created\": \"SSO {{name}}이 생성되었습니다\",\n      \"sso-list\": \"SSO 목록\",\n      \"sso-updated\": \"SSO {{name}}이 수정되었습니다\",\n      \"template\": \"템플릿\",\n      \"token-endpoint\": \"토큰 엔드포인트\",\n      \"update-sso\": \"SSO 수정\",\n      \"user-endpoint\": \"유저 엔드포인트\",\n      \"label\": \"SSO\"\n    },\n    \"storage\": {\n      \"accesskey\": \"액세스 키\",\n      \"accesskey-placeholder\": \"액세스 키 / 액세스 ID\",\n      \"bucket\": \"버킷\",\n      \"bucket-placeholder\": \"버킷 이름\",\n      \"create-a-service\": \"서비스 생성\",\n      \"create-storage\": \"저장소 생성\",\n      \"current-storage\": \"사용 중인 저장소\",\n      \"delete-storage\": \"저장소 삭제\",\n      \"endpoint\": \"엔드포인트\",\n      \"filepath-template\": \"파일 경로 템플릿\",\n      \"local-storage-path\": \"로컬 저장소 경로\",\n      \"path\": \"저장소 경로\",\n      \"path-description\": \"{filename} 등 로컬 저장소에서와 같은 변수를 사용할 수 있습니다\",\n      \"path-placeholder\": \"custom/path\",\n      \"presign-placeholder\": \"미리 서명된 URL, 선택 사항\",\n      \"region\": \"리전\",\n      \"region-placeholder\": \"리전 이름\",\n      \"s3-compatible-url\": \"S3과 호환되는 서비스 URL\",\n      \"secretkey\": \"비밀 액세스 키\",\n      \"secretkey-placeholder\": \"비밀 키 / 비밀 액세스 키\",\n      \"storage-services\": \"저장소 서비스 목록\",\n      \"type-database\": \"데이터베이스\",\n      \"type-local\": \"로컬 파일시스템\",\n      \"update-a-service\": \"서비스 편집\",\n      \"update-local-path\": \"로컬 저장소 경로 수정\",\n      \"update-local-path-description\": \"로컬 저장소 경로는 DB 파일로부터의 상대 경로입니다\",\n      \"update-storage\": \"저장소 수정\",\n      \"url-prefix\": \"URL 접두사\",\n      \"url-prefix-placeholder\": \"URL 앞에 붙을 커스텀 접두사, 선택 사항\",\n      \"url-suffix\": \"URL 접미사\",\n      \"url-suffix-placeholder\": \"URL 뒤에 붙을 커스텀 접미사, 선택 사항\",\n      \"warning-text\": \"저장소 서비스 \\\"{{name}}\\\"을(를) 삭제하시겠습니까? 이 행동은 되돌릴 수 없습니다\",\n      \"label\": \"저장소\"\n    },\n    \"system\": {\n      \"additional-script\": \"추가 스크립트\",\n      \"additional-script-placeholder\": \"추가 JavaScript 코드\",\n      \"additional-style\": \"추가 스타일\",\n      \"additional-style-placeholder\": \"추가 CSS 코드\",\n      \"allow-user-signup\": \"회원가입 허용\",\n      \"customize-server\": {\n        \"description\": \"설명\",\n        \"icon-url\": \"아이콘 URL\",\n        \"locale\": \"서버 언어\",\n        \"title\": \"서버 커스터마이징\"\n      },\n      \"disable-password-login\": \"비밀번호 로그인 금지\",\n      \"disable-password-login-final-warning\": \"이로부터 발생할 수 있는 모든 상황을 이해했다면, 아래에 \\\"CONFIRM\\\"을 입력해 주세요.\",\n      \"disable-password-login-warning\": \"모든 사용자가 비밀번호를 사용해서 로그인하지 못하게 합니다. 설정된 SSO에 문제가 생겼을 경우, 데이터베이스를 직접 조작해서 이 설정을 끄지 않는 한 절대로 로그인하지 못하게 됩니다. SSO 설정을 수정/제거할 때에는 특히 조심해 주세요\",\n      \"display-with-updated-time\": \"수정된 시각을 표시\",\n      \"enable-auto-compact\": \"자동 압축 활성화\",\n      \"enable-double-click-to-edit\": \"더블 클릭으로 편집 활성화\",\n      \"enable-password-login\": \"비밀번호 로그인 허용\",\n      \"enable-password-login-warning\": \"모든 사용자가 비밀번호를 사용해서 로그인할 수 있게 합니다. 사용자들이 SSO와 비밀번호 둘 다 사용할 수 있게 하고 싶은 경우에만 켜 주세요\",\n      \"max-upload-size\": \"최대 업로드 크기 (MiB)\",\n      \"max-upload-size-hint\": \"권장값은 32 MiB입니다.\",\n      \"removed-completed-task-list-items\": \"완료된 할 일 삭제 활성화\",\n      \"server-name\": \"서버 이름\",\n      \"title\": \"일반\",\n      \"label\": \"시스템\"\n    },\n    \"version\": \"버전\",\n    \"access-token\": {\n      \"access-token-copied-to-clipboard\": \"액세스 토큰이 복사되었습니다\",\n      \"access-token-deleted\": \"액세스 토큰 `{{description}}`이 삭제되었습니다\",\n      \"access-token-deletion\": \"액세스 토큰 {{description}}을 삭제하시겠습니까? 이 행동은 되돌릴 수 없습니다.\",\n      \"access-token-deletion-description\": \"이 작업은 되돌릴 수 없습니다. 이 토큰을 사용하는 서비스들은 새 토큰을 사용하도록 업데이트해야 합니다.\",\n      \"create-dialog\": {\n        \"access-token-created\": \"액세스 토큰 `{{description}}`이 생성되었습니다\",\n        \"create-access-token\": \"액세스 토큰 생성\",\n        \"created-at\": \"생성일\",\n        \"description\": \"설명\",\n        \"duration-1m\": \"1개월\",\n        \"duration-8h\": \"8시간\",\n        \"duration-never\": \"영구\",\n        \"expiration\": \"만료\",\n        \"expires-at\": \"만료일\",\n        \"some-description\": \"설명...\"\n      },\n      \"description\": \"계정의 모든 액세스 토큰 목록입니다.\",\n      \"title\": \"액세스 토큰\",\n      \"token\": \"토큰\"\n    },\n    \"account\": {\n      \"change-password\": \"비밀번호 변경\",\n      \"email-note\": \"선택 사항\",\n      \"export-memos\": \"메모 내보내기\",\n      \"nickname-note\": \"배너에 표시됨\",\n      \"openapi-reset\": \"OpenAPI 키 초기화\",\n      \"openapi-sample-post\": \"안녕, {{url}} ! #memo\",\n      \"openapi-title\": \"OpenAPI\",\n      \"reset-api\": \"API 초기화\",\n      \"title\": \"계정 정보\",\n      \"update-information\": \"정보 변경\",\n      \"username-note\": \"로그인하는 데 사용됨\"\n    },\n    \"instance\": {\n      \"disallow-change-nickname\": \"닉네임 변경 금지\",\n      \"disallow-change-username\": \"유저네임 변경 금지\",\n      \"disallow-password-auth\": \"비밀번호 인증 금지\",\n      \"disallow-user-registration\": \"회원가입 금지\",\n      \"monday\": \"월요일\",\n      \"saturday\": \"토요일\",\n      \"sunday\": \"일요일\",\n      \"week-start-day\": \"주 시작 요일\"\n    },\n    \"memo\": {\n      \"content-length-limit\": \"내용 길이 제한 (바이트)\",\n      \"enable-blur-nsfw-content\": \"민감한(성인) 콘텐츠 블러 처리 활성화\",\n      \"enable-memo-comments\": \"메모 댓글 활성화\",\n      \"enable-memo-location\": \"메모 위치 활성화\",\n      \"reactions\": \"반응\",\n      \"title\": \"메모 관련 설정\",\n      \"label\": \"메모\"\n    },\n    \"webhook\": {\n      \"create-dialog\": {\n        \"an-easy-to-remember-name\": \"기억하기 쉬운 이름\",\n        \"create-webhook\": \"Webhook 생성\",\n        \"create-webhook-success\": \"Webhook `{{name}}`이 생성되었습니다\",\n        \"edit-webhook\": \"Webhook 편집\",\n        \"payload-url\": \"페이로드 URL\",\n        \"title\": \"제목\",\n        \"url-example-post-receive\": \"https://example.com/postreceive\"\n      },\n      \"delete-dialog\": {\n        \"delete-webhook-description\": \"이 작업은 되돌릴 수 없습니다.\",\n        \"delete-webhook-success\": \"Webhook `{{name}}`이 성공적으로 삭제되었습니다\",\n        \"delete-webhook-title\": \"Webhook `{{name}}`을 정말로 삭제하시겠습니까?\"\n      },\n      \"no-webhooks-found\": \"Webhook이 없습니다.\",\n      \"title\": \"Webhook\",\n      \"url\": \"URL\"\n    }\n  },\n  \"tag\": {\n    \"all-tags\": \"모든 태그\",\n    \"create-tag\": \"태그 생성\",\n    \"create-tags-guide\": \"`#태그`를 입력하여 태그를 만들 수 있습니다.\",\n    \"delete-confirm\": \"이 태그를 삭제하시겠습니까? 관련된 모든 메모가 보관됩니다.\",\n    \"delete-success\": \"태그가 성공적으로 삭제되었습니다\",\n    \"delete-tag\": \"태그 삭제\",\n    \"new-name\": \"새 이름\",\n    \"no-tag-found\": \"태그를 찾을 수 없습니다\",\n    \"old-name\": \"이전 이름\",\n    \"rename-error-empty\": \"태그 이름은 비어 있거나 공백을 포함할 수 없습니다\",\n    \"rename-error-repeat\": \"새 이름은 이전 이름과 같을 수 없습니다\",\n    \"rename-success\": \"태그 이름이 성공적으로 변경되었습니다\",\n    \"rename-tag\": \"태그 이름 변경\",\n    \"rename-tip\": \"이 태그가 포함된 모든 메모가 업데이트됩니다.\"\n  },\n  \"tooltip\": {\n    \"link-memo\": \"메모 링크\",\n    \"markdown-menu\": \"Markdown\",\n    \"select-location\": \"위치\",\n    \"select-visibility\": \"공개 범위\",\n    \"tags\": \"태그\",\n    \"upload-attachment\": \"첨부파일 업로드\"\n  }\n}\n"
  },
  {
    "path": "web/src/locales/mr.json",
    "content": "{\n  \"about\": {\n    \"blogs\": \"ब्लॉग्ज\",\n    \"description\": \"गोपनीयता-प्रथम, हलकी नोट घेण्याची सेवा. तुमचे उत्तम विचार सहजपणे टिपा आणि शेअर करा.\",\n    \"documents\": \"दस्तऐवज\",\n    \"github-repository\": \"GitHub रेपॉजिटरी\",\n    \"official-website\": \"अधिकृत वेबसाइट\"\n  },\n  \"auth\": {\n    \"create-your-account\": \"तुमचे खाते तयार करा\",\n    \"host-tip\": \"तुम्ही साइट होस्ट म्हणून नोंदणी करत आहात.\",\n    \"new-password\": \"नवीन पासवर्ड\",\n    \"repeat-new-password\": \"नवीन पासवर्डची पुनरावृत्ती करा\",\n    \"sign-in-tip\": \"आधीपासूनच एक खाते आहे?\",\n    \"sign-up-tip\": \"अजून खाते नाही?\"\n  },\n  \"common\": {\n    \"about\": \"बद्दल\",\n    \"add\": \"जोडा\",\n    \"admin\": \"ॲडमिन\",\n    \"all\": \"सर्व\",\n    \"archive\": \"संग्रहण\",\n    \"archived\": \"संग्रहित\",\n    \"attachments\": \"संलग्नक\",\n    \"auto-expand\": \"स्वयं विस्तार\",\n    \"avatar\": \"अवतार\",\n    \"basic\": \"मूलभूत\",\n    \"beta\": \"बीटा\",\n    \"calendar\": \"दिनदर्शिका\",\n    \"cancel\": \"रद्द करा\",\n    \"change\": \"बदला\",\n    \"clear\": \"साफ\",\n    \"close\": \"बंद\",\n    \"collapse\": \"संकुचित करा\",\n    \"confirm\": \"पुष्टी\",\n    \"copy\": \"प्रत बनवा\",\n    \"create\": \"तयार करा\",\n    \"created-at\": \"तयार केले\",\n    \"database\": \"डेटाबेस\",\n    \"day\": \"दिवस\",\n    \"days\": {\n      \"fri\": \"शुक्र\",\n      \"mon\": \"सोम\",\n      \"sat\": \"शनि\",\n      \"sun\": \"रवि\",\n      \"thu\": \"गुरु\",\n      \"tue\": \"मंगळ\",\n      \"wed\": \"बुध\"\n    },\n    \"delete\": \"हटवा\",\n    \"description\": \"वर्णन\",\n    \"edit\": \"संपादन\",\n    \"email\": \"ईमेल\",\n    \"expand\": \"विस्तृत करा\",\n    \"explore\": \"अन्वेषण\",\n    \"file\": \"फाईल\",\n    \"filter\": \"गाळा\",\n    \"home\": \"घर\",\n    \"image\": \"प्रतिमा\",\n    \"in\": \"मध्ये\",\n    \"inbox\": \"इनबॉक्स\",\n    \"input\": \"इनपुट\",\n    \"language\": \"भाषा\",\n    \"last-updated-at\": \"शेवटचे अद्यतन\",\n    \"learn-more\": \"अधिक जाणून घ्या\",\n    \"link\": \"लिंक\",\n    \"map\": \"नकाशा\",\n    \"mark\": \"खूण\",\n    \"memo\": \"मेमो\",\n    \"memos\": \"मेमो\",\n    \"more\": \"अधिक\",\n    \"name\": \"नाव\",\n    \"new\": \"नविन\",\n    \"nickname\": \"टोपणनाव\",\n    \"null\": \"निरर्थक\",\n    \"or\": \"किंवा\",\n    \"password\": \"गुप्त शब्द\",\n    \"pin\": \"पिन\",\n    \"pinned\": \"पिन केलेला\",\n    \"preview\": \"पूर्वावलोकन\",\n    \"profile\": \"प्रोफाइल\",\n    \"properties\": \"गुणधर्म\",\n    \"referenced-by\": \"याने संदर्भित\",\n    \"referencing\": \"संदर्भ देत आहे\",\n    \"relations\": \"संबंध\",\n    \"remember-me\": \"माझी आठवण ठेवा\",\n    \"rename\": \"नाव बदला\",\n    \"reset\": \"रीसेट करा\",\n    \"resources\": \"संसाधने\",\n    \"restore\": \"पुनर्संचयित करा\",\n    \"role\": \"भूमिका\",\n    \"save\": \"जतन करा\",\n    \"search\": \"शोधा\",\n    \"select\": \"निवडा\",\n    \"settings\": \"सेटिंग्ज\",\n    \"share\": \"शेअर करा\",\n    \"shortcut-filter\": \"शॉर्टकट फिल्टर\",\n    \"shortcuts\": \"शॉर्टकट्स\",\n    \"sign-in\": \"साइन इन करा\",\n    \"sign-in-with\": \"{{provider}} सह साइन इन करा\",\n    \"sign-out\": \"साइन आउट करा\",\n    \"sign-up\": \"साइन अप करा\",\n    \"statistics\": \"आकडेवारी\",\n    \"tags\": \"टॅग्ज\",\n    \"title\": \"शीर्षक\",\n    \"today\": \"आज\",\n    \"tree-mode\": \"ट्री मोड\",\n    \"type\": \"प्रकार\",\n    \"unpin\": \"अनपिन\",\n    \"update\": \"अपडेट करा\",\n    \"upload\": \"अपलोड करा\",\n    \"user\": \"वापरकर्ता\",\n    \"username\": \"वापरकर्तानाव\",\n    \"version\": \"आवृत्ती\",\n    \"visibility\": \"दृश्यमानता\",\n    \"yourself\": \"स्वतः\"\n  },\n  \"editor\": {\n    \"add-your-comment-here\": \"तुमची टिप्पणी येथे जो़डा\",\n    \"any-thoughts\": \"आपले विचार...\",\n    \"exit-focus-mode\": \"फोकस मोड सोडा\",\n    \"focus-mode\": \"फोकस मोड\",\n    \"no-changes-detected\": \"कोणतेही बदल आढळले नाहीत\",\n    \"save\": \"जतन\",\n    \"saving\": \"जतन होत आहे...\",\n    \"slash-commands\": \"कमांडसाठी `/` टाइप करा\"\n  },\n  \"inbox\": {\n    \"failed-to-load\": \"इनबॉक्स आयटम लोड करण्यात अयशस्वी\",\n    \"memo-comment\": \"{{user}} ची तुमच्या {{memo}} वर टिप्पणी आहे.\",\n    \"no-archived\": \"संग्रहित सूचना नाहीत\",\n    \"no-unread\": \"न वाचलेल्या सूचना नाहीत\",\n    \"unread\": \"न वाचलेले\"\n  },\n  \"markdown\": {\n    \"checkbox\": \"चेकबॉक्स\",\n    \"code-block\": \"कोड ब्लॉक\",\n    \"content-syntax\": \"सामग्री सिंटॅक्स\"\n  },\n  \"memo\": {\n    \"archived-at\": \"येथे संग्रहित\",\n    \"click-to-hide-nsfw-content\": \"संवेदनशील सामग्री लपवण्यासाठी क्लिक करा\",\n    \"click-to-show-nsfw-content\": \"संवेदनशील सामग्री दाखवण्यासाठी क्लिक करा\",\n    \"code\": \"कोड\",\n    \"comment\": {\n      \"self\": \"टिप्पण्या\",\n      \"write-a-comment\": \"टिप्पणी लिहा\"\n    },\n    \"copy-content\": \"सामग्रीची प्रत बनवा\",\n    \"copy-link\": \"लिंकची प्रत बनवा\",\n    \"count-memos-in-date\": \"{{count}} {{memos}} {{date}} ला\",\n    \"delete-confirm\": \"हा मेमो हटवण्याची तुम्हाला खात्री आहे का? हि क्रिया अपरिवर्तनीय आहे\",\n    \"delete-confirm-description\": \"ही क्रिया अपरिवर्तनीय आहे. संलग्नक, लिंक आणि संदर्भ देखील काढून टाकले जातील.\",\n    \"direction\": \"दिशा\",\n    \"direction-asc\": \"आरोही\",\n    \"direction-desc\": \"अवरोही\",\n    \"display-time\": \"प्रदर्शन वेळ\",\n    \"filters\": {\n      \"has-code\": \"कोड आहे\",\n      \"has-link\": \"लिंक आहे\",\n      \"has-task-list\": \"कार्यसूची आहे\"\n    },\n    \"links\": \"लिंक्स\",\n    \"load-more\": \"अधिक लोड करा\",\n    \"no-archived-memos\": \"कोणतेही संग्रहित मेमो नाहीत.\",\n    \"no-memos\": \"कोणतेही मेमो नाहीत.\",\n    \"order-by\": \"क्रमवारी\",\n    \"search-placeholder\": \"मेमोज शोधा...\",\n    \"show-less\": \"कमी दाखवा\",\n    \"show-more\": \"अधिक दाखवा\",\n    \"to-do\": \"कार्य\",\n    \"view-detail\": \"तपशील दाखवा\",\n    \"visibility\": {\n      \"disabled\": \"सार्वजनिक मेमो अक्षम केले आहेत\",\n      \"private\": \"खाजगी\",\n      \"protected\": \"संरक्षित\",\n      \"public\": \"सार्वजनिक\"\n    }\n  },\n  \"message\": {\n    \"archived-successfully\": \"यशस्वीरित्या संग्रहित केले\",\n    \"change-memo-created-time\": \"मेमो तयार केलेली वेळ बदला\",\n    \"copied\": \"प्रत बनवली\",\n    \"deleted-successfully\": \"यशस्वीरित्या हटवले\",\n    \"description-is-required\": \"वर्णन आवश्यक आहे\",\n    \"failed-to-embed-memo\": \"मेमो एम्बेड करण्यात अयशस्वी\",\n    \"fill-all\": \"कृपया सर्व फील्ड भरा.\",\n    \"fill-all-required-fields\": \"कृपया सर्व आवश्यक फील्ड भरा\",\n    \"maximum-upload-size-is\": \"कमाल अनुमत अपलोड आकार {{size}} MiB आहे\",\n    \"memo-not-found\": \"मेमो सापडला नाही.\",\n    \"new-password-not-match\": \"नवीन पासवर्ड जुळत नाहीत.\",\n    \"no-data\": \"माहिती आढळली नाही.\",\n    \"password-changed\": \"पासवर्ड बदलला\",\n    \"password-not-match\": \"पासवर्ड जुळत नाही.\",\n    \"restored-successfully\": \"यशस्वीरित्या पुनर्संचयित केले\",\n    \"succeed-copy-content\": \"सामग्री यशस्वीरित्या कॉपी केली.\",\n    \"succeed-copy-link\": \"लिंक यशस्वीरित्या कॉपी केली.\",\n    \"update-succeed\": \"अपडेट यशस्वी झाले\",\n    \"user-not-found\": \"वापरकर्ता सापडला नाही\"\n  },\n  \"reference\": {\n    \"add-references\": \"संदर्भ जोडा\",\n    \"embedded-usage\": \"एम्बेडेड सामग्री म्हणून वापरा\",\n    \"no-memos-found\": \"कोणतेही मेमो आढळले नाहीत\",\n    \"search-placeholder\": \"सामग्री शोधा\"\n  },\n  \"resource\": {\n    \"clear\": \"साफ\",\n    \"copy-link\": \"लिंक कॉपी करा\",\n    \"create-dialog\": {\n      \"external-link\": {\n        \"file-name\": \"फाईलचे नाव\",\n        \"file-name-placeholder\": \"फाईलचे नाव\",\n        \"link\": \"लिंक\",\n        \"link-placeholder\": \"https://the.link.to/your/resource\",\n        \"option\": \"बाह्य लिंक\",\n        \"type\": \"प्रकार\",\n        \"type-placeholder\": \"फाईल प्रकार\"\n      },\n      \"local-file\": {\n        \"choose\": \"एक फाइल निवडा…\",\n        \"option\": \"स्थानिक फाइल\"\n      },\n      \"title\": \"संसाधन तयार करा\",\n      \"upload-method\": \"अपलोड पद्धत\"\n    },\n    \"delete-all-unused\": \"सर्व न वापरलेली हटवा\",\n    \"delete-all-unused-confirm\": \"तुम्हाला सर्व न वापरलेली संसाधने हटवायची खात्री आहे का? ही क्रिया अपरिवर्तनीय आहे\",\n    \"delete-all-unused-error\": \"न वापरलेली संसाधने हटवण्यात अयशस्वी\",\n    \"delete-all-unused-success\": \"संसाधने यशस्वीरित्या हटवली\",\n    \"delete-resource\": \"संसाधन हटवा\",\n    \"delete-selected-resources\": \"निवडलेली संसाधने हटवा\",\n    \"fetching-data\": \"डेटा आणत आहे…\",\n    \"file-drag-drop-prompt\": \"फाइल अपलोड करण्यासाठी तुमची फाइल येथे ड्रॅग आणि ड्रॉप करा\",\n    \"linked-amount\": \"लिंक केलेली रक्कम\",\n    \"no-files-selected\": \"कोणत्याही फाइल्स निवडल्या नाहीत\",\n    \"no-resources\": \"संसाधने नाहीत.\",\n    \"no-unused-resources\": \"कोणतीही न वापरलेली संसाधने नाहीत\",\n    \"reset-link\": \"लिंक रीसेट करा\",\n    \"reset-link-prompt\": \"तुम्हाला लिंक रीसेट करण्याची खात्री आहे का? हे सर्व वर्तमान लिंक वापर खंडित करेल. ही क्रिया अपरिवर्तनीय आहे\",\n    \"reset-resource-link\": \"संसाधन लिंक रीसेट करा\",\n    \"unused-resources\": \"न वापरलेली संसाधने\"\n  },\n  \"router\": {\n    \"back-to-top\": \"परत वर जा\",\n    \"go-to-home\": \"घरी जा\"\n  },\n  \"setting\": {\n    \"member\": {\n      \"admin\": \"ॲडमिन\",\n      \"archive-member\": \"संग्रहण सदस्य\",\n      \"archive-success\": \"{{username}} यशस्वीरित्या संग्रहित केले\",\n      \"archive-warning\": \"तुम्हाला {{username}} संग्रहित करण्याची खात्री आहे?\",\n      \"archive-warning-description\": \"संग्रहण खाते अक्षम करते. तुम्ही नंतर पुनर्संचयित किंवा हटवू शकता.\",\n      \"create-a-member\": \"सदस्य तयार करा\",\n      \"delete-member\": \"सदस्य हटवा\",\n      \"delete-success\": \"{{username}} यशस्वीरित्या हटवले\",\n      \"delete-warning\": \"तुम्हाला नक्की {{username}} हटवायचे आहे का? ही क्रिया अपरिवर्तनीय आहे\",\n      \"delete-warning-description\": \"ही क्रिया अपरिवर्तनीय आहे.\",\n      \"restore-success\": \"{{username}} यशस्वीरित्या पुनर्संचयित केले\",\n      \"user\": \"वापरकर्ता\",\n      \"label\": \"सदस्य\",\n      \"list-title\": \"सदस्य यादी\"\n    },\n    \"my-account\": {\n      \"label\": \"माझे खाते\"\n    },\n    \"preference\": {\n      \"default-memo-sort-option\": \"मेमो प्रदर्शन वेळ\",\n      \"default-memo-visibility\": \"डीफॉल्ट मेमो दृश्यमानता\",\n      \"theme\": \"थीम\",\n      \"label\": \"प्राधान्ये\"\n    },\n    \"shortcut\": {\n      \"delete-confirm\": \"तुम्हाला शॉर्टकट `{{title}}` हटवायची खात्री आहे का?\",\n      \"delete-success\": \"शॉर्टकट `{{title}}` यशस्वीरित्या हटवले\"\n    },\n    \"sso\": {\n      \"authorization-endpoint\": \"अधिकृतता एन्डपाॅंइन्ट\",\n      \"client-id\": \"क्लायंट आयडी\",\n      \"client-secret\": \"क्लायंट गुपित\",\n      \"confirm-delete\": \"तुम्हाला \\\"{{name}}\\\" SSO कॉन्फिगरेशन हटवण्याची खात्री आहे का? ही क्रिया अपरिवर्तनीय आहे\",\n      \"create-sso\": \"SSO तयार करा\",\n      \"custom\": \"सानुकूल\",\n      \"delete-sso\": \"हटविण्याची पुष्टी करा\",\n      \"disabled-password-login-warning\": \"पासवर्ड-लॉगिन अक्षम केले आहे, ओळख प्रदाते काढताना अधिक काळजी घ्या\",\n      \"display-name\": \"प्रदर्शनासाठी नाव\",\n      \"identifier\": \"ओळखकर्ता\",\n      \"identifier-filter\": \"अभिज्ञापक फिल्टर\",\n      \"no-sso-found\": \"कोणतेही SSO आढळले नाही.\",\n      \"redirect-url\": \"URL पुनर्निर्देशित करा\",\n      \"scopes\": \"व्याप्ती\",\n      \"single-sign-on\": \"प्रमाणीकरणासाठी Single Sign-On (SSO) कॉन्फिगर करा\",\n      \"sso-created\": \"SSO {{name}} तयार केले\",\n      \"sso-list\": \"SSO सूची\",\n      \"sso-updated\": \"SSO {{name}} अपडेट केले\",\n      \"template\": \"साचा\",\n      \"token-endpoint\": \"टोकन एंडपॉइंट\",\n      \"update-sso\": \"SSO अपडेट करा\",\n      \"user-endpoint\": \"वापरकर्ता एंडपॉइंट\",\n      \"label\": \"SSO\"\n    },\n    \"storage\": {\n      \"accesskey\": \"प्रवेश की\",\n      \"accesskey-placeholder\": \"प्रवेश की / प्रवेश आयडी\",\n      \"bucket\": \"बादली\",\n      \"bucket-placeholder\": \"बादली नाव\",\n      \"create-a-service\": \"सेवा तयार करा\",\n      \"create-storage\": \"स्टोरेज तयार करा\",\n      \"current-storage\": \"वर्तमान ऑब्जेक्ट स्टोरेज\",\n      \"delete-storage\": \"स्टोरेज हटवा\",\n      \"endpoint\": \"एन्डपाॅंईन्ट\",\n      \"filepath-template\": \"फाईलपथ टेम्पलेट\",\n      \"local-storage-path\": \"स्थानिक स्टोरेज मार्ग\",\n      \"path\": \"स्टोरेज पथ\",\n      \"path-description\": \"तुम्ही स्थानिक स्टोरेजमधून समान डायनॅमिक व्हेरिएबल्स वापरू शकता, जसे की {filename}\",\n      \"path-placeholder\": \"सानुकूल/पथ\",\n      \"presign-placeholder\": \"पूर्व-स्वाक्षरी URL, पर्यायी\",\n      \"region\": \"प्रदेश\",\n      \"region-placeholder\": \"प्रदेशाचे नाव\",\n      \"s3-compatible-url\": \"S3 सुसंगत URL\",\n      \"secretkey\": \"गुप्त की\",\n      \"secretkey-placeholder\": \"गुप्त की / प्रवेश की\",\n      \"storage-services\": \"स्टोरेज सेवा\",\n      \"type-database\": \"डेटाबेस\",\n      \"type-local\": \"स्थानिक फाइल सिस्टम\",\n      \"update-a-service\": \"सेवा अपडेट करा\",\n      \"update-local-path\": \"स्थानिक स्टोरेज पथ अपडेट करा\",\n      \"update-local-path-description\": \"स्थानिक स्टोरेज पथ हा तुमच्या डेटाबेस फाइलचा सापेक्ष मार्ग आहे\",\n      \"update-storage\": \"स्टोरेज अपडेट करा\",\n      \"url-prefix\": \"URL उपसर्ग\",\n      \"url-prefix-placeholder\": \"सानुकूल URL उपसर्ग, पर्यायी\",\n      \"url-suffix\": \"URL प्रत्यय\",\n      \"url-suffix-placeholder\": \"सानुकूल URL प्रत्यय, पर्यायी\",\n      \"warning-text\": \"तुम्हाला स्टोरेज सेवा \\\"{{name}}\\\" हटवायची खात्री आहे का? ही क्रिया अपरिवर्तनीय आहे\",\n      \"label\": \"स्टोरेज\"\n    },\n    \"system\": {\n      \"additional-script\": \"अतिरिक्त स्क्रिप्ट\",\n      \"additional-script-placeholder\": \"अतिरिक्त JavaScript कोड\",\n      \"additional-style\": \"अतिरिक्त शैली\",\n      \"additional-style-placeholder\": \"अतिरिक्त CSS कोड\",\n      \"allow-user-signup\": \"वापरकर्ता साइनअपला अनुमती द्या\",\n      \"customize-server\": {\n        \"description\": \"वर्णन\",\n        \"icon-url\": \"चिन्ह URL\",\n        \"locale\": \"सर्व्हर लोकेल\",\n        \"title\": \"सर्व्हर सानुकूलित करा\"\n      },\n      \"disable-password-login\": \"पासवर्ड लॉगिन अक्षम करा\",\n      \"disable-password-login-final-warning\": \"तुम्ही काय करत आहात हे तुम्हाला माहीत असल्यास कृपया \\\"CONFIRM\\\" टाइप करा.\",\n      \"disable-password-login-warning\": \"हे सर्व वापरकर्त्यांसाठी पासवर्ड लॉगिन अक्षम करेल. तुमचे कॉन्फिगर केलेले ओळख प्रदाते अयशस्वी झाल्यास डेटाबेसमध्ये ही सेटिंग पूर्ववत केल्याशिवाय लॉग इन करणे शक्य नाही. ओळख प्रदाता काढून टाकताना तुम्हाला अधिक सावधगिरी बाळगावी लागेल\",\n      \"display-with-updated-time\": \"अद्यतनित वेळेसह प्रदर्शित करा\",\n      \"enable-auto-compact\": \"ऑटो-कॉम्पॅक्ट सक्षम करा\",\n      \"enable-double-click-to-edit\": \"डबल क्लिकने संपादन सक्षम करा\",\n      \"enable-password-login\": \"पासवर्ड लॉगिन सक्षम करा\",\n      \"enable-password-login-warning\": \"हे सर्व वापरकर्त्यांसाठी पासवर्ड लॉगिन सक्षम करेल. जर तुम्ही वापरकर्त्यांना SSO आणि पासवर्ड दोन्ही वापरून लॉग इन करू इच्छित असाल तरच सुरू ठेवा\",\n      \"max-upload-size\": \"कमाल अपलोड आकार (MiB)\",\n      \"max-upload-size-hint\": \"शिफारस केलेले मूल्य 32 MiB आहे.\",\n      \"removed-completed-task-list-items\": \"पूर्ण झालेल्या कार्यसूची आयटम हटवणे सक्षम करा\",\n      \"server-name\": \"सर्व्हरचे नाव\",\n      \"title\": \"सामान्य\",\n      \"label\": \"प्रणाली\"\n    },\n    \"version\": \"आवृत्ती\",\n    \"access-token\": {\n      \"access-token-copied-to-clipboard\": \"ऍक्सेस टोकन क्लिपबोर्डवर कॉपी केले\",\n      \"access-token-deleted\": \"ऍक्सेस टोकन `{{description}}` हटवले\",\n      \"access-token-deletion\": \"तुम्हाला ऍक्सेस टोकन `{{description}}` हटवायचे आहे का? ही क्रिया अपरिवर्तनीय आहे.\",\n      \"access-token-deletion-description\": \"ही क्रिया अपरिवर्तनीय आहे. या टोकनचा वापर करणाऱ्या कोणत्याही सेवांना नवीन टोकन वापरायला लावावे लागेल.\",\n      \"create-dialog\": {\n        \"access-token-created\": \"ऍक्सेस टोकन `{{description}}` तयार केले\",\n        \"create-access-token\": \"ऍक्सेस टोकन तयार करा\",\n        \"created-at\": \"तयार केले\",\n        \"description\": \"वर्णन\",\n        \"duration-1m\": \"1 महिना\",\n        \"duration-8h\": \"8 तास\",\n        \"duration-never\": \"कधीच नाही\",\n        \"expiration\": \"कालबाह्यता\",\n        \"expires-at\": \"कालबाह्यता तारीख\",\n        \"some-description\": \"काही वर्णन...\"\n      },\n      \"description\": \"तुमच्या खात्यातील सर्व ऍक्सेस टोकन्सची यादी.\",\n      \"title\": \"ऍक्सेस टोकन्स\",\n      \"token\": \"टोकन\"\n    },\n    \"account\": {\n      \"change-password\": \"पासवर्ड बदला\",\n      \"email-note\": \"ऐच्छिक\",\n      \"export-memos\": \"मेमो निर्यात करा\",\n      \"nickname-note\": \"बॅनर मध्ये प्रदर्शित\",\n      \"openapi-reset\": \"OpenAPI की रीसेट करा\",\n      \"openapi-sample-post\": \"{{url}} कडून नमस्कार #मेमो\",\n      \"openapi-title\": \"OpenAPI\",\n      \"reset-api\": \"API रीसेट करा\",\n      \"title\": \"खाते माहिती\",\n      \"update-information\": \"माहिती अपडेट करा\",\n      \"username-note\": \"साइन इन करण्यासाठी वापरले जाते\"\n    },\n    \"instance\": {\n      \"disallow-change-nickname\": \"टोपणनाव बदलण्यास मनाई करा\",\n      \"disallow-change-username\": \"वापरकर्तानाव बदलण्यास मनाई करा\",\n      \"disallow-password-auth\": \"पासवर्ड प्रमाणीकरणास मनाई करा\",\n      \"disallow-user-registration\": \"वापरकर्ता नोंदणीस मनाई करा\",\n      \"monday\": \"सोमवार\",\n      \"saturday\": \"शनिवार\",\n      \"sunday\": \"रविवार\",\n      \"week-start-day\": \"आठवड्याचा प्रारंभ दिवस\"\n    },\n    \"memo\": {\n      \"content-length-limit\": \"सामग्रीची कमाल लांबी (बाइट)\",\n      \"enable-blur-nsfw-content\": \"संवेदनशील (NSFW) सामग्री ब्लर करा सक्षम करा\",\n      \"enable-memo-comments\": \"मेमो टिप्पण्या सक्षम करा\",\n      \"enable-memo-location\": \"मेमो स्थान सक्षम करा\",\n      \"reactions\": \"प्रतिक्रिया\",\n      \"title\": \"मेमो संबंधित सेटिंग्ज\",\n      \"label\": \"मेमो\"\n    },\n    \"webhook\": {\n      \"create-dialog\": {\n        \"an-easy-to-remember-name\": \"स्मरणात राहणारे नाव\",\n        \"create-webhook\": \"वेबहुक तयार करा\",\n        \"create-webhook-success\": \"वेबहुक `{{name}}` तयार केले\",\n        \"edit-webhook\": \"वेबहुक संपादित करा\",\n        \"payload-url\": \"पेलोड URL\",\n        \"title\": \"शीर्षक\",\n        \"url-example-post-receive\": \"https://example.com/postreceive\"\n      },\n      \"delete-dialog\": {\n        \"delete-webhook-description\": \"ही क्रिया अपरिवर्तनीय आहे.\",\n        \"delete-webhook-success\": \"वेबहुक `{{name}}` यशस्वीरित्या हटवले\",\n        \"delete-webhook-title\": \"तुम्हाला webhook `{{name}}` हटवायची खात्री आहे का?\"\n      },\n      \"no-webhooks-found\": \"कोणतेही वेबहुक आढळले नाहीत.\",\n      \"title\": \"वेबहुक्स\",\n      \"url\": \"URL\"\n    }\n  },\n  \"tag\": {\n    \"all-tags\": \"सर्व टॅग\",\n    \"create-tag\": \"टॅग तयार करा\",\n    \"create-tags-guide\": \"तुम्ही `#tag` इनपुट करून टॅग तयार करू शकता.\",\n    \"delete-confirm\": \"तुम्हाला हा टॅग हटवण्याची खात्री आहे का? सर्व संबंधित मेमो संग्रहित केले जातील.\",\n    \"delete-success\": \"टॅग यशस्वीरित्या हटवला\",\n    \"delete-tag\": \"टॅग हटवा\",\n    \"new-name\": \"नवीन नाव\",\n    \"no-tag-found\": \"कोणताही टॅग आढळला नाही\",\n    \"old-name\": \"जुने नाव\",\n    \"rename-error-empty\": \"टॅगचे नाव रिकामे किंवा स्पेस असू शकत नाही\",\n    \"rename-error-repeat\": \"नवीन नाव जुना नावासारखे असू शकत नाही\",\n    \"rename-success\": \"टॅग यशस्वीरित्या बदलला\",\n    \"rename-tag\": \"टॅगचे नाव बदला\",\n    \"rename-tip\": \"या टॅगसह तुमचे सर्व मेमो अपडेट केले जातील.\"\n  },\n  \"tooltip\": {\n    \"link-memo\": \"मेमो लिंक करा\",\n    \"markdown-menu\": \"Markdown\",\n    \"select-location\": \"स्थान\",\n    \"select-visibility\": \"दृश्यमानता\",\n    \"tags\": \"टॅग्ज\",\n    \"upload-attachment\": \"संलग्नक अपलोड करा\"\n  }\n}\n"
  },
  {
    "path": "web/src/locales/nb.json",
    "content": "{\n  \"about\": {\n    \"blogs\": \"Blogger\",\n    \"description\": \"Et personvernsvennlig og lettvektig notatprogram. Lagre og del de gode tankene dine enkelt.\",\n    \"documents\": \"Dokumenter\",\n    \"github-repository\": \"GitHub Repo\",\n    \"official-website\": \"Offisiell nettside\"\n  },\n  \"auth\": {\n    \"create-your-account\": \"Opprett konto\",\n    \"host-tip\": \"Du registrerer deg som vert for nettstedet.\",\n    \"new-password\": \"Nytt passord\",\n    \"repeat-new-password\": \"Gjenta det nye passordet\",\n    \"sign-in-tip\": \"Har du allerede en konto?\",\n    \"sign-up-tip\": \"Har du ikke en konto enda?\"\n  },\n  \"common\": {\n    \"about\": \"Om\",\n    \"add\": \"Legg til\",\n    \"admin\": \"Administrasjon\",\n    \"all\": \"Alle\",\n    \"archive\": \"Arkiver\",\n    \"archived\": \"Arkivert\",\n    \"attachments\": \"Vedlegg\",\n    \"auto-expand\": \"Ekspander automatisk\",\n    \"avatar\": \"Avatar\",\n    \"basic\": \"Grunnleggende\",\n    \"beta\": \"Beta\",\n    \"calendar\": \"Kalender\",\n    \"cancel\": \"Avbryt\",\n    \"change\": \"Bytt\",\n    \"clear\": \"Tøm\",\n    \"close\": \"Lukk\",\n    \"collapse\": \"Kollaps\",\n    \"confirm\": \"Bekreft\",\n    \"copy\": \"Kopier\",\n    \"create\": \"Opprett\",\n    \"created-at\": \"Opprettet\",\n    \"database\": \"Database\",\n    \"day\": \"Dag\",\n    \"days\": {\n      \"fri\": \"fr.\",\n      \"mon\": \"ma.\",\n      \"sat\": \"lø.\",\n      \"sun\": \"sø.\",\n      \"thu\": \"to.\",\n      \"tue\": \"ti.\",\n      \"wed\": \"on.\"\n    },\n    \"delete\": \"Slett\",\n    \"description\": \"Beskrivelse\",\n    \"edit\": \"Rediger\",\n    \"email\": \"E-post\",\n    \"expand\": \"Ekspander\",\n    \"explore\": \"Utforsk\",\n    \"file\": \"Fil\",\n    \"filter\": \"Filter\",\n    \"home\": \"Hjem\",\n    \"image\": \"Bilde\",\n    \"in\": \"I\",\n    \"inbox\": \"Innboks\",\n    \"input\": \"Input\",\n    \"language\": \"Språk\",\n    \"last-updated-at\": \"Sist oppdatert\",\n    \"learn-more\": \"Lær mer\",\n    \"link\": \"Link\",\n    \"map\": \"Kart\",\n    \"mark\": \"Merk\",\n    \"memo\": \"Memo\",\n    \"memos\": \"Memoer\",\n    \"more\": \"Mer\",\n    \"name\": \"Navn\",\n    \"new\": \"Ny\",\n    \"nickname\": \"Kallenavn\",\n    \"null\": \"Null\",\n    \"or\": \"eller\",\n    \"password\": \"Passord\",\n    \"pin\": \"Fest\",\n    \"pinned\": \"Festet\",\n    \"preview\": \"Forhåndsvisning\",\n    \"profile\": \"Profil\",\n    \"properties\": \"Egenskaper\",\n    \"referenced-by\": \"Referert til av\",\n    \"referencing\": \"Refererer til\",\n    \"relations\": \"Relasjon\",\n    \"remember-me\": \"Husk meg\",\n    \"rename\": \"Gi nytt navn\",\n    \"reset\": \"Nullstill\",\n    \"resources\": \"Ressurser\",\n    \"restore\": \"Gjenopprett\",\n    \"role\": \"Rolle\",\n    \"save\": \"Lagre\",\n    \"search\": \"Søk\",\n    \"select\": \"Velg\",\n    \"settings\": \"Innstillinger\",\n    \"share\": \"Del\",\n    \"shortcut-filter\": \"Snarveifilter\",\n    \"shortcuts\": \"Snarveier\",\n    \"sign-in\": \"Logg inn\",\n    \"sign-in-with\": \"Logg inn med {{provider}}\",\n    \"sign-out\": \"Logg ut\",\n    \"sign-up\": \"Registrer deg\",\n    \"statistics\": \"Statistikk\",\n    \"tags\": \"Tagger\",\n    \"title\": \"Tittel\",\n    \"today\": \"I dag\",\n    \"tree-mode\": \"Tre-modus\",\n    \"type\": \"Type\",\n    \"unpin\": \"Fjern festet\",\n    \"update\": \"Oppdater\",\n    \"upload\": \"Last opp\",\n    \"user\": \"Bruker\",\n    \"username\": \"Brukernavn\",\n    \"version\": \"Versjon\",\n    \"visibility\": \"Synlighet\",\n    \"yourself\": \"Deg selv\"\n  },\n  \"editor\": {\n    \"add-your-comment-here\": \"Legg til din kommentar her...\",\n    \"any-thoughts\": \"Noen tanker...\",\n    \"exit-focus-mode\": \"Avslutt fokusmodus\",\n    \"focus-mode\": \"Fokusmodus\",\n    \"no-changes-detected\": \"Ingen endringer oppdaget\",\n    \"save\": \"Lagre\",\n    \"saving\": \"Lagrer...\",\n    \"slash-commands\": \"Skriv `/` for kommandoer\"\n  },\n  \"inbox\": {\n    \"failed-to-load\": \"Kunne ikke laste innbokselement\",\n    \"memo-comment\": \"{{user}} har en kommentar til din {{memo}}.\",\n    \"no-archived\": \"Ingen arkiverte varsler\",\n    \"no-unread\": \"Ingen uleste varsler\",\n    \"unread\": \"Ulest\"\n  },\n  \"markdown\": {\n    \"checkbox\": \"Avkrysningsboks\",\n    \"code-block\": \"Kodeblokk\",\n    \"content-syntax\": \"Innholdssyntaks\"\n  },\n  \"memo\": {\n    \"archived-at\": \"Arkivert\",\n    \"click-to-hide-nsfw-content\": \"Klikk for å skjule NSFW-innhold\",\n    \"click-to-show-nsfw-content\": \"Klikk for å vise NSFW-innhold\",\n    \"code\": \"Kode\",\n    \"comment\": {\n      \"self\": \"Kommentarer\",\n      \"write-a-comment\": \"Skriv en kommentar\"\n    },\n    \"copy-content\": \"Kopier innhold\",\n    \"copy-link\": \"Kopier link\",\n    \"count-memos-in-date\": \"{{count}} {{memos}} på {{date}}\",\n    \"delete-confirm\": \"Er du sikker på at du vil slette denne memoen? DENNE HANDLINGEN KAN IKKE ANGRES\",\n    \"delete-confirm-description\": \"Denne handlingen kan ikke angres. Vedlegg, lenker og referanser vil også bli fjernet.\",\n    \"direction\": \"Retning\",\n    \"direction-asc\": \"Stigende\",\n    \"direction-desc\": \"Synkende\",\n    \"display-time\": \"Vis tid\",\n    \"filters\": {\n      \"has-code\": \"harKode\",\n      \"has-link\": \"harLink\",\n      \"has-task-list\": \"harGjøremålsListe\"\n    },\n    \"links\": \"Linker\",\n    \"load-more\": \"Last inn mer\",\n    \"no-archived-memos\": \"Ingen arkiverte memoer.\",\n    \"no-memos\": \"Ingen memoer.\",\n    \"order-by\": \"Sorter etter\",\n    \"search-placeholder\": \"Søk etter memoer...\",\n    \"show-less\": \"Vis mindre\",\n    \"show-more\": \"Vis mer\",\n    \"to-do\": \"Gjøremål\",\n    \"view-detail\": \"Vis detalj\",\n    \"visibility\": {\n      \"disabled\": \"Offentlige memoer er deaktivert\",\n      \"private\": \"Privat\",\n      \"protected\": \"Beskyttet\",\n      \"public\": \"Offentlig\"\n    }\n  },\n  \"message\": {\n    \"archived-successfully\": \"Arkivering vellykket\",\n    \"change-memo-created-time\": \"Endre opprettelsestidspunkt for memo\",\n    \"copied\": \"Kopiert\",\n    \"deleted-successfully\": \"Sletting vellykket\",\n    \"description-is-required\": \"Beskrivelse kreves\",\n    \"failed-to-embed-memo\": \"Kunne ikke legge inn memoen\",\n    \"fill-all\": \"Vennligst fyll ut alle feltene.\",\n    \"fill-all-required-fields\": \"Vennligst fyll ut alle obligatoriske felter\",\n    \"maximum-upload-size-is\": \"Maksimal tillatt opplastingsstørrelse er {{size}} MiB\",\n    \"memo-not-found\": \"Memoen ble ikke funnet.\",\n    \"new-password-not-match\": \"Nye passord samsvarer ikke.\",\n    \"no-data\": \"Ingen data funnet.\",\n    \"password-changed\": \"Passord endret\",\n    \"password-not-match\": \"Passordene samsvarer ikke\",\n    \"restored-successfully\": \"Gjenoppretting vellykket\",\n    \"succeed-copy-content\": \"Innhold kopiert vellykket.\",\n    \"succeed-copy-link\": \"Link kopiert.\",\n    \"update-succeed\": \"Oppdatering vellykket\",\n    \"user-not-found\": \"Bruker ikke funnet\"\n  },\n  \"reference\": {\n    \"add-references\": \"Legg til referanser\",\n    \"embedded-usage\": \"Bruk som innbygd innhold\",\n    \"no-memos-found\": \"Ingen memoer funnet\",\n    \"search-placeholder\": \"Søk etter innhold\"\n  },\n  \"resource\": {\n    \"clear\": \"Tøm\",\n    \"copy-link\": \"Kopier link\",\n    \"create-dialog\": {\n      \"external-link\": {\n        \"file-name\": \"Filnavn\",\n        \"file-name-placeholder\": \"Filnavn\",\n        \"link\": \"Link\",\n        \"link-placeholder\": \"https://link.til/din/ressurs\",\n        \"option\": \"Ekstern link\",\n        \"type\": \"Type\",\n        \"type-placeholder\": \"Filtype\"\n      },\n      \"local-file\": {\n        \"choose\": \"Velg en fil…\",\n        \"option\": \"Lokal fil\"\n      },\n      \"title\": \"Opprett ressurs\",\n      \"upload-method\": \"Opplastingsmetode\"\n    },\n    \"delete-all-unused\": \"Slett alle ubrukte\",\n    \"delete-all-unused-confirm\": \"Er du sikker på at du vil slette alle ubrukte ressurser? DENNE HANDLINGEN KAN IKKE ANGRES\",\n    \"delete-all-unused-error\": \"Kunne ikke slette ubrukte ressurser\",\n    \"delete-all-unused-success\": \"Ressurser slettet vellykket\",\n    \"delete-resource\": \"Slett ressurs\",\n    \"delete-selected-resources\": \"Slett valgte ressurser\",\n    \"fetching-data\": \"Henter data…\",\n    \"file-drag-drop-prompt\": \"Dra og slipp en fil hit for å laste den opp\",\n    \"linked-amount\": \"Antall linket\",\n    \"no-files-selected\": \"Ingen filer valgt\",\n    \"no-resources\": \"Ingen ressurser.\",\n    \"no-unused-resources\": \"Ingen ubrukte ressurser\",\n    \"reset-link\": \"Nullstill link\",\n    \"reset-link-prompt\": \"Er du sikker på at du vil nullstille linken? Dette vil ødelegge alle forekomster av linken. DENNE HANDLINGEN KAN IKKE ANGRES\",\n    \"reset-resource-link\": \"Nullstill link for ressurs\",\n    \"unused-resources\": \"Ubrukte ressurser\"\n  },\n  \"router\": {\n    \"back-to-top\": \"Tilbake til toppen\",\n    \"go-to-home\": \"Gå til Hjem\"\n  },\n  \"setting\": {\n    \"member\": {\n      \"admin\": \"Administrator\",\n      \"archive-member\": \"Arkiver medlem\",\n      \"archive-success\": \"{{username}} arkivert vellykket\",\n      \"archive-warning\": \"Er du sikker på at du vil arkivere {{username}}?\",\n      \"archive-warning-description\": \"Arkivering deaktiverer kontoen. Du kan gjenopprette eller slette den senere.\",\n      \"create-a-member\": \"Opprett medlem\",\n      \"delete-member\": \"Slett medlem\",\n      \"delete-success\": \"{{username}} slettet vellykket\",\n      \"delete-warning\": \"Er du sikker på at du vil slette {{username}}? DENNE HANDLINGEN KAN IKKE ANGRES\",\n      \"delete-warning-description\": \"DENNE HANDLINGEN KAN IKKE ANGRES\",\n      \"restore-success\": \"{{username}} gjenopprettet vellykket\",\n      \"user\": \"Bruker\",\n      \"label\": \"Medlem\",\n      \"list-title\": \"Medlemsliste\"\n    },\n    \"my-account\": {\n      \"label\": \"Min konto\"\n    },\n    \"preference\": {\n      \"default-memo-sort-option\": \"Memo visningstid\",\n      \"default-memo-visibility\": \"Standard synlighet for memoer\",\n      \"theme\": \"Tema\",\n      \"label\": \"Innstillinger\"\n    },\n    \"shortcut\": {\n      \"delete-confirm\": \"Er du sikker på at du vil slette snarveien `{{title}}`?\",\n      \"delete-success\": \"Snarvei `{{title}}` slettet vellykket\"\n    },\n    \"sso\": {\n      \"authorization-endpoint\": \"Autorisasjon endepunkt\",\n      \"client-id\": \"Klient-ID\",\n      \"client-secret\": \"Klienthemmelighet\",\n      \"confirm-delete\": \"Er du sikker på at du vil slette SSO-konfigurasjonen \\\"{{name}}\\\"? DENNE HANDLINGEN KAN IKKE ANGRES\",\n      \"create-sso\": \"Opprett SSO\",\n      \"custom\": \"Egendefinert\",\n      \"delete-sso\": \"Bekreft sletting\",\n      \"disabled-password-login-warning\": \"Passordinnlogging er deaktivert, vær ekstra forsiktig når du fjerner identitetsleverandører\",\n      \"display-name\": \"Visningsnavn\",\n      \"identifier\": \"Identifikator\",\n      \"identifier-filter\": \"Identifikatorfilter\",\n      \"no-sso-found\": \"Ingen SSO funnet.\",\n      \"redirect-url\": \"Omdirigerings-URL\",\n      \"scopes\": \"Scopes\",\n      \"single-sign-on\": \"Konfigurerer Single Sign-On (SSO) for autentisering\",\n      \"sso-created\": \"SSO {{name}} opprettet\",\n      \"sso-list\": \"SSO-liste\",\n      \"sso-updated\": \"SSO {{name}} oppdatert\",\n      \"template\": \"Mal\",\n      \"token-endpoint\": \"Token endepunkt\",\n      \"update-sso\": \"Oppdater SSO\",\n      \"user-endpoint\": \"Brukerendepunkt\",\n      \"label\": \"SSO\"\n    },\n    \"storage\": {\n      \"accesskey\": \"Tilgangsnøkkel\",\n      \"accesskey-placeholder\": \"Tilgangsnøkkel / Access ID\",\n      \"bucket\": \"Bøtte\",\n      \"bucket-placeholder\": \"Navn på bøtte\",\n      \"create-a-service\": \"Opprett en tjeneste\",\n      \"create-storage\": \"Opprett lagring\",\n      \"current-storage\": \"Nåværende lagring av objekter\",\n      \"delete-storage\": \"Slett lagring\",\n      \"endpoint\": \"Endepunkt\",\n      \"filepath-template\": \"Filbanemal\",\n      \"local-storage-path\": \"Lokal lagringssti\",\n      \"path\": \"Lagringssti\",\n      \"path-description\": \"Du kan bruke de samme dynamiske variablene fra lokal lagring, som {filename}\",\n      \"path-placeholder\": \"egen/sti\",\n      \"presign-placeholder\": \"Pre-sign URL, valgfritt\",\n      \"region\": \"Region\",\n      \"region-placeholder\": \"Navn på region\",\n      \"s3-compatible-url\": \"S3-kompatibel URL\",\n      \"secretkey\": \"Hemmelig nøkkel\",\n      \"secretkey-placeholder\": \"Hemmelig nøkkel / Access Key\",\n      \"storage-services\": \"Lagringstjenester\",\n      \"type-database\": \"Database\",\n      \"type-local\": \"Lokalt filsystem\",\n      \"update-a-service\": \"Oppdater en tjeneste\",\n      \"update-local-path\": \"Oppdater lokal lagringssti\",\n      \"update-local-path-description\": \"Lokal lagringssti er en relativ sti til databasefilen din\",\n      \"update-storage\": \"Oppdater lagring\",\n      \"url-prefix\": \"URL-prefiks\",\n      \"url-prefix-placeholder\": \"Egendefinert URL-prefiks, valgfritt\",\n      \"url-suffix\": \"URL-suffiks\",\n      \"url-suffix-placeholder\": \"Egendefinert URL-suffiks, valgfritt\",\n      \"warning-text\": \"Er du sikker på at du vil slette lagringstjenesten \\\"{{name}}\\\"? DENNE HANDLINGEN KAN IKKE ANGRES\",\n      \"label\": \"Lagring\"\n    },\n    \"system\": {\n      \"additional-script\": \"Ekstra kode\",\n      \"additional-script-placeholder\": \"Ekstra Javascript-kode\",\n      \"additional-style\": \"Ekstra styling\",\n      \"additional-style-placeholder\": \"Ekstra CSS-kode\",\n      \"allow-user-signup\": \"Tillat registrering av brukere\",\n      \"customize-server\": {\n        \"description\": \"Beskrivelse\",\n        \"icon-url\": \"Ikon-URL\",\n        \"locale\": \"Serverlokalisering\",\n        \"title\": \"Tilpass server\"\n      },\n      \"disable-password-login\": \"Slå av passordinnlogging\",\n      \"disable-password-login-final-warning\": \"Skriv \\\"CONFIRM\\\" hvis du vet hva du gjør.\",\n      \"disable-password-login-warning\": \"Dette slår av passordinnlogging for alle brukere. Dersom de konfigurerte identitetsleverandørene mislykkes er det ikke mulig å logge inn uten å tilbakestille denne innstillingen i databasen. Du må også være ekstra forsiktig når du fjerner en identitetsleverandør.\",\n      \"display-with-updated-time\": \"Vis med oppdatert tid\",\n      \"enable-auto-compact\": \"Slå på automatisk kompakt visning\",\n      \"enable-double-click-to-edit\": \"Slå på dobbeltklikk for å redigere\",\n      \"enable-password-login\": \"Slå på passordinnlogging\",\n      \"enable-password-login-warning\": \"Dette slår på passordinnlogging for alle brukere. Fortsett kun dersom du vil at brukerne skal kunne logge inn med både SSO og passord\",\n      \"max-upload-size\": \"Maksimal opplastingsstørrelse (MiB)\",\n      \"max-upload-size-hint\": \"Anbefalt verdi er 32 MiB.\",\n      \"removed-completed-task-list-items\": \"Slå på fjerning av utførte gjøremål\",\n      \"server-name\": \"Servernavn\",\n      \"title\": \"Generelt\",\n      \"label\": \"System\"\n    },\n    \"version\": \"Versjon\",\n    \"access-token\": {\n      \"access-token-copied-to-clipboard\": \"Access token kopiert til utklippstavlen\",\n      \"access-token-deleted\": \"Access token `{{description}}` slettet\",\n      \"access-token-deletion\": \"Er du sikker på at du vil slette access token {{description}}? DENNE HANDLINGEN KAN IKKE ANGRES.\",\n      \"access-token-deletion-description\": \"Denne handlingen kan ikke angres. Du må oppdatere alle tjenester som bruker denne tokenen til å bruke en ny token.\",\n      \"create-dialog\": {\n        \"access-token-created\": \"Access token `{{description}}` opprettet\",\n        \"create-access-token\": \"Opprett access token\",\n        \"created-at\": \"Opprettet\",\n        \"description\": \"Beskrivelse\",\n        \"duration-1m\": \"1 måned\",\n        \"duration-8h\": \"8 timer\",\n        \"duration-never\": \"Aldri\",\n        \"expiration\": \"Utløpsdato\",\n        \"expires-at\": \"Utløper\",\n        \"some-description\": \"En beskrivelse...\"\n      },\n      \"description\": \"Liste over alle access tokens for din konto.\",\n      \"title\": \"Access tokens\",\n      \"token\": \"Token\"\n    },\n    \"account\": {\n      \"change-password\": \"Endre passord\",\n      \"email-note\": \"Valgfritt\",\n      \"export-memos\": \"Eksporter memoer\",\n      \"nickname-note\": \"Vises i banneret\",\n      \"openapi-reset\": \"Nullstill OpenAPI-nøkkel\",\n      \"openapi-sample-post\": \"Hei #memos fra {{url}}\",\n      \"openapi-title\": \"OpenAPI\",\n      \"reset-api\": \"Nullstill API\",\n      \"title\": \"Kontoinformasjon\",\n      \"update-information\": \"Oppdater informasjon\",\n      \"username-note\": \"Brukes til innlogging\"\n    },\n    \"instance\": {\n      \"disallow-change-nickname\": \"Ikke tillat endring av kallenavn\",\n      \"disallow-change-username\": \"Ikke tillat endring av brukernavn\",\n      \"disallow-password-auth\": \"Ikke tillat autentisering med passord\",\n      \"disallow-user-registration\": \"Ikke tillat registrering av brukere\",\n      \"monday\": \"Mandag\",\n      \"saturday\": \"Lørdag\",\n      \"sunday\": \"Søndag\",\n      \"week-start-day\": \"Ukestart\"\n    },\n    \"memo\": {\n      \"content-length-limit\": \"Maksimal innholdslengde (Byte)\",\n      \"enable-blur-nsfw-content\": \"Slå på sløring av NSFW-innhold (legg til NSFW-tagger nedenfor)\",\n      \"enable-memo-comments\": \"Slå på kommentarer for memoer\",\n      \"enable-memo-location\": \"Slå på lokasjon for memoer\",\n      \"reactions\": \"Reaksjoner\",\n      \"title\": \"Memo-relaterte innstillinger\",\n      \"label\": \"Memo\"\n    },\n    \"webhook\": {\n      \"create-dialog\": {\n        \"an-easy-to-remember-name\": \"Et lett å huske navn\",\n        \"create-webhook\": \"Opprett webhook\",\n        \"create-webhook-success\": \"Webhook `{{name}}` opprettet\",\n        \"edit-webhook\": \"Rediger webhook\",\n        \"payload-url\": \"Payload-URL\",\n        \"title\": \"Tittel\",\n        \"url-example-post-receive\": \"https://eksempel.no/postreceive\"\n      },\n      \"delete-dialog\": {\n        \"delete-webhook-description\": \"Denne handlingen kan ikke angres.\",\n        \"delete-webhook-success\": \"Webhook `{{name}}` slettet vellykket\",\n        \"delete-webhook-title\": \"Er du sikker på at du vil slette webhooken `{{name}}`?\"\n      },\n      \"no-webhooks-found\": \"Ingen webhooks funnet.\",\n      \"title\": \"Webhooks\",\n      \"url\": \"URL\"\n    }\n  },\n  \"tag\": {\n    \"all-tags\": \"Alle tagger\",\n    \"create-tag\": \"Opprett tagg\",\n    \"create-tags-guide\": \"Du kan opprette tagger ved å skrive `#tagg`.\",\n    \"delete-confirm\": \"Er du sikker på at du vil slette denne taggen? Alle relaterte memoer vil bli arkivert.\",\n    \"delete-success\": \"Tagg slettet vellykket\",\n    \"delete-tag\": \"Slett tagg\",\n    \"new-name\": \"Nytt navn\",\n    \"no-tag-found\": \"Ingen tagger funnet\",\n    \"old-name\": \"Gammelt navn\",\n    \"rename-error-empty\": \"Taggnavnet kan ikke være blankt eller ha mellomrom\",\n    \"rename-error-repeat\": \"Nytt navn kan ikke være det samme som gammelt navn\",\n    \"rename-success\": \"Taggnavn endret\",\n    \"rename-tag\": \"Gi nytt navn til tagg\",\n    \"rename-tip\": \"Alle dine memoer med denne taggen vil bli oppdatert.\"\n  },\n  \"tooltip\": {\n    \"link-memo\": \"Lenke til memo\",\n    \"markdown-menu\": \"Markdown\",\n    \"select-location\": \"Lokasjon\",\n    \"select-visibility\": \"Synlighet\",\n    \"tags\": \"Tagger\",\n    \"upload-attachment\": \"Last opp vedlegg\"\n  }\n}\n"
  },
  {
    "path": "web/src/locales/nl.json",
    "content": "{\n  \"about\": {\n    \"blogs\": \"Blogs\",\n    \"description\": \"Een privacy-eerst, lichtgewicht notitie-app. Leg je gedachten eenvoudig vast en deel ze.\",\n    \"documents\": \"Documenten\",\n    \"github-repository\": \"GitHub Repo\",\n    \"official-website\": \"Officiële website\"\n  },\n  \"auth\": {\n    \"create-your-account\": \"Maak je account\",\n    \"host-tip\": \"Je registreert je als Site Host.\",\n    \"new-password\": \"Nieuw wachtwoord\",\n    \"repeat-new-password\": \"Nieuw wachtwoord herhalen\",\n    \"sign-in-tip\": \"Al een account?\",\n    \"sign-up-tip\": \"Nog geen account?\"\n  },\n  \"common\": {\n    \"about\": \"Over\",\n    \"add\": \"Toevoegen\",\n    \"admin\": \"Beheerder\",\n    \"all\": \"Alle\",\n    \"archive\": \"Archiveren\",\n    \"archived\": \"Gearchiveerd\",\n    \"attachments\": \"Bijlagen\",\n    \"auto-expand\": \"Auto uitklappen\",\n    \"avatar\": \"Avatar\",\n    \"basic\": \"Simpel\",\n    \"beta\": \"Beta\",\n    \"calendar\": \"Kalender\",\n    \"cancel\": \"Annuleren\",\n    \"change\": \"Wijzigen\",\n    \"clear\": \"Leegmaken\",\n    \"close\": \"Sluiten\",\n    \"collapse\": \"Samenvouwen\",\n    \"confirm\": \"Bevestigen\",\n    \"copy\": \"Kopiëren\",\n    \"create\": \"Aanmaken\",\n    \"created-at\": \"Aangemaakt op\",\n    \"database\": \"Database\",\n    \"day\": \"Dag\",\n    \"days\": {\n      \"fri\": \"vr\",\n      \"mon\": \"ma\",\n      \"sat\": \"za\",\n      \"sun\": \"zo\",\n      \"thu\": \"do\",\n      \"tue\": \"di\",\n      \"wed\": \"wo\"\n    },\n    \"delete\": \"Verwijderen\",\n    \"description\": \"Beschrijving\",\n    \"edit\": \"Bewerken\",\n    \"email\": \"E-mailadres\",\n    \"expand\": \"Uitvouwen\",\n    \"explore\": \"Verkennen\",\n    \"file\": \"Bestand\",\n    \"filter\": \"Filter\",\n    \"home\": \"Thuis\",\n    \"image\": \"Afbeelding\",\n    \"in\": \"In\",\n    \"inbox\": \"Postvak\",\n    \"input\": \"Invoer\",\n    \"language\": \"Taal\",\n    \"last-updated-at\": \"Laatst bijgewerkt op\",\n    \"learn-more\": \"Meer informatie\",\n    \"link\": \"Link\",\n    \"map\": \"Kaart\",\n    \"mark\": \"Markeren\",\n    \"memo\": \"Memo\",\n    \"memos\": \"Memos\",\n    \"more\": \"Meer\",\n    \"name\": \"Naam\",\n    \"new\": \"Nieuw\",\n    \"nickname\": \"Bijnaam\",\n    \"null\": \"Null\",\n    \"or\": \"of\",\n    \"password\": \"Wachtwoord\",\n    \"pin\": \"Vastzetten\",\n    \"pinned\": \"Vastgezet\",\n    \"preview\": \"Voorbeeld\",\n    \"profile\": \"Profiel\",\n    \"properties\": \"Eigenschappen\",\n    \"referenced-by\": \"Gerefereerd door\",\n    \"referencing\": \"Verwijst naar\",\n    \"relations\": \"Relaties\",\n    \"remember-me\": \"Onthoud mij\",\n    \"rename\": \"Hernoemen\",\n    \"reset\": \"Herstel\",\n    \"resources\": \"Bronnen\",\n    \"restore\": \"Terugzetten\",\n    \"role\": \"Rol\",\n    \"save\": \"Opslaan\",\n    \"search\": \"Zoeken\",\n    \"select\": \"Selecteren\",\n    \"settings\": \"Instellingen\",\n    \"share\": \"Delen\",\n    \"shortcut-filter\": \"Sneltoetsfilter\",\n    \"shortcuts\": \"Sneltoetsen\",\n    \"sign-in\": \"Inloggen\",\n    \"sign-in-with\": \"Inloggen met {{provider}}\",\n    \"sign-out\": \"Uitloggen\",\n    \"sign-up\": \"Registreren\",\n    \"statistics\": \"Statistieken\",\n    \"tags\": \"Labels\",\n    \"title\": \"Titel\",\n    \"today\": \"Vandaag\",\n    \"tree-mode\": \"Boomstructuur\",\n    \"type\": \"Type\",\n    \"unpin\": \"Losmaken\",\n    \"update\": \"Bijwerken\",\n    \"upload\": \"Uploaden\",\n    \"user\": \"Gebruiker\",\n    \"username\": \"Gebruikersnaam\",\n    \"version\": \"Versie\",\n    \"visibility\": \"Zichtbaarheid\",\n    \"yourself\": \"Jijzelf\"\n  },\n  \"editor\": {\n    \"add-your-comment-here\": \"Voeg hier je opmerking toe…\",\n    \"any-thoughts\": \"Enkele gedachten…\",\n    \"exit-focus-mode\": \"Focusmodus afsluiten\",\n    \"focus-mode\": \"Focusmodus\",\n    \"no-changes-detected\": \"Geen wijzigingen gedetecteerd\",\n    \"save\": \"Opslaan\",\n    \"saving\": \"Opslaan...\",\n    \"slash-commands\": \"Typ `/` voor opdrachten\"\n  },\n  \"inbox\": {\n    \"failed-to-load\": \"Postvak-item laden mislukt\",\n    \"memo-comment\": \"{{user}} heeft een opmerking bij jouw {{memo}}.\",\n    \"no-archived\": \"Geen gearchiveerde meldingen\",\n    \"no-unread\": \"Geen ongelezen meldingen\",\n    \"unread\": \"Ongelezen\"\n  },\n  \"markdown\": {\n    \"checkbox\": \"Selectievakje\",\n    \"code-block\": \"Codeblok\",\n    \"content-syntax\": \"Inhoudssyntaxis\"\n  },\n  \"memo\": {\n    \"archived-at\": \"Gearchiveerd op\",\n    \"click-to-hide-nsfw-content\": \"Klik om NSFW-inhoud te verbergen\",\n    \"click-to-show-nsfw-content\": \"Klik om NSFW-inhoud te tonen\",\n    \"code\": \"Code\",\n    \"comment\": {\n      \"self\": \"Opmerkingen\",\n      \"write-a-comment\": \"Schrijf een opmerking\"\n    },\n    \"copy-content\": \"Inhoud kopiëren\",\n    \"copy-link\": \"Kopieer link\",\n    \"count-memos-in-date\": \"{{count}} {{memos}} op {{date}}\",\n    \"delete-confirm\": \"Weet je zeker dat je deze memo wilt verwijderen? DEZE ACTIE IS NIET TERUG TE DRAAIEN\",\n    \"delete-confirm-description\": \"Deze actie is onomkeerelijk. Bijlagen, links en referenties worden ook verwijderd.\",\n    \"direction\": \"Richting\",\n    \"direction-asc\": \"Oplopend\",\n    \"direction-desc\": \"Aflopend\",\n    \"display-time\": \"Tijd weergeven\",\n    \"filters\": {\n      \"has-code\": \"heeftCode\",\n      \"has-link\": \"heeftLink\",\n      \"has-task-list\": \"heeftTakenlijst\"\n    },\n    \"links\": \"Links\",\n    \"load-more\": \"Meer laden\",\n    \"no-archived-memos\": \"Geen gearchiveerde memos.\",\n    \"no-memos\": \"Geen memos.\",\n    \"order-by\": \"Sorteren op\",\n    \"search-placeholder\": \"Memos zoeken…\",\n    \"show-less\": \"Minder tonen\",\n    \"show-more\": \"Meer tonen\",\n    \"to-do\": \"Takenlijst\",\n    \"view-detail\": \"Details bekijken\",\n    \"visibility\": {\n      \"disabled\": \"Openbare memos zijn uitgezet\",\n      \"private\": \"Privé\",\n      \"protected\": \"Werkruimte\",\n      \"public\": \"Openbaar\"\n    }\n  },\n  \"message\": {\n    \"archived-successfully\": \"Succesvol gearchiveerd\",\n    \"change-memo-created-time\": \"Tijd van aanmaken wijzigen\",\n    \"copied\": \"Gekopieerd\",\n    \"deleted-successfully\": \"Succesvol verwijderd\",\n    \"description-is-required\": \"Beschrijving is verplicht\",\n    \"failed-to-embed-memo\": \"Memo insluiten is mislukt\",\n    \"fill-all\": \"Vul alsjeblieft alle velden in.\",\n    \"fill-all-required-fields\": \"Vul alle verplichte velden in\",\n    \"maximum-upload-size-is\": \"Maximale uploadgrootte is {{size}} MiB\",\n    \"memo-not-found\": \"Memo niet gevonden.\",\n    \"new-password-not-match\": \"Nieuwe wachtwoorden komen niet overeen.\",\n    \"no-data\": \"Geen gegevens gevonden.\",\n    \"password-changed\": \"Wachtwoord gewijzigd.\",\n    \"password-not-match\": \"Wachtwoorden komen niet overeen.\",\n    \"restored-successfully\": \"Succesvol teruggezet\",\n    \"succeed-copy-content\": \"Inhoud succesvol gekopieerd.\",\n    \"succeed-copy-link\": \"Link gekopieerd naar klembord.\",\n    \"update-succeed\": \"Update voltooid\",\n    \"user-not-found\": \"Gebruiker niet gevonden\"\n  },\n  \"reference\": {\n    \"add-references\": \"Referenties toevoegen\",\n    \"embedded-usage\": \"Gebruiken als ingesloten inhoud\",\n    \"no-memos-found\": \"Geen memos gevonden\",\n    \"search-placeholder\": \"Inhoud zoeken\"\n  },\n  \"resource\": {\n    \"clear\": \"Opruimen\",\n    \"copy-link\": \"Link kopiëren\",\n    \"create-dialog\": {\n      \"external-link\": {\n        \"file-name\": \"Bestandsnaam\",\n        \"file-name-placeholder\": \"Bestandsnaam\",\n        \"link\": \"Link\",\n        \"link-placeholder\": \"https://de.link.naar/je/bron\",\n        \"option\": \"Externe link\",\n        \"type\": \"Type\",\n        \"type-placeholder\": \"Bestandstype\"\n      },\n      \"local-file\": {\n        \"choose\": \"Kies een bestand…\",\n        \"option\": \"Lokaal bestand\"\n      },\n      \"title\": \"Bron aanmaken\",\n      \"upload-method\": \"Uploadmethode\"\n    },\n    \"delete-all-unused\": \"Alle ongebruikte verwijderen\",\n    \"delete-all-unused-confirm\": \"Weet je zeker dat je alle ongebruikte bronnen wilt verwijderen? DEZE ACTIE IS NIET TERUG TE DRAAIEN\",\n    \"delete-all-unused-error\": \"Ongebruikte bronnen verwijderen mislukt\",\n    \"delete-all-unused-success\": \"Bronnen succesvol verwijderd\",\n    \"delete-resource\": \"Bron verwijderen\",\n    \"delete-selected-resources\": \"Geselecteerde bronnen verwijderen\",\n    \"fetching-data\": \"Gegevens ophalen…\",\n    \"file-drag-drop-prompt\": \"Sleep je bestand hierheen om te uploaden\",\n    \"linked-amount\": \"Aantal gelinkte memo's\",\n    \"no-files-selected\": \"Geen bestanden geselecteerd\",\n    \"no-resources\": \"Geen bronnen.\",\n    \"no-unused-resources\": \"Geen ongebruikte bronnen\",\n    \"reset-link\": \"Link resetten\",\n    \"reset-link-prompt\": \"Weet je zeker dat je de link wilt resetten? Dit verbreekt alle huidige linkgebruik. DEZE ACTIE IS NIET TERUG TE DRAAIEN\",\n    \"reset-resource-link\": \"Bronlink resetten\",\n    \"unused-resources\": \"Ongebruikte bronnen\"\n  },\n  \"router\": {\n    \"back-to-top\": \"Terug naar boven\",\n    \"go-to-home\": \"Ga naar homepagina\"\n  },\n  \"setting\": {\n    \"member\": {\n      \"admin\": \"Beheerder\",\n      \"archive-member\": \"Gebruiker archiveren\",\n      \"archive-success\": \"{{username}} succesvol gearchiveerd\",\n      \"archive-warning\": \"Weet je zeker dat je {{username}} wilt archiveren?\",\n      \"archive-warning-description\": \"Archiveren deactiveert de account. Je kunt deze later terugzetten of verwijderen.\",\n      \"create-a-member\": \"Lid toevoegen\",\n      \"delete-member\": \"Gebruiker verwijderen\",\n      \"delete-success\": \"{{username}} succesvol verwijderd\",\n      \"delete-warning\": \"Weet je zeker dat je {{username}} wilt verwijderen? DEZE ACTIE IS NIET TERUG TE DRAAIEN\",\n      \"delete-warning-description\": \"DEZE ACTIE IS NIET TERUG TE DRAAIEN\",\n      \"restore-success\": \"{{username}} succesvol teruggezet\",\n      \"user\": \"Gebruiker\",\n      \"label\": \"Gebruiker\",\n      \"list-title\": \"Gebruikers\"\n    },\n    \"my-account\": {\n      \"label\": \"Mijn account\"\n    },\n    \"preference\": {\n      \"default-memo-sort-option\": \"Memo weergavetijd\",\n      \"default-memo-visibility\": \"Standaard memo zichtbaarheid\",\n      \"theme\": \"Thema\",\n      \"label\": \"Voorkeuren\"\n    },\n    \"shortcut\": {\n      \"delete-confirm\": \"Weet je zeker dat je sneltoets `{{title}}` wilt verwijderen?\",\n      \"delete-success\": \"Sneltoets `{{title}}` succesvol verwijderd\"\n    },\n    \"sso\": {\n      \"authorization-endpoint\": \"Autorisatie-eindpunt\",\n      \"client-id\": \"Client-ID\",\n      \"client-secret\": \"Client-geheim\",\n      \"confirm-delete\": \"Weet je zeker dat je de \\\"{{name}}\\\" SSO configuratie wilt verwijderen? DEZE ACTIE IS NIET TERUG TE DRAAIEN\",\n      \"create-sso\": \"SSO instellen\",\n      \"custom\": \"Aangepast\",\n      \"delete-sso\": \"Verwijdering bevestigen\",\n      \"disabled-password-login-warning\": \"Wachtwoordlogin is uitgeschakeld. Kijk uit met het verwijderen van inlogproviders\",\n      \"display-name\": \"Weergavenaam\",\n      \"identifier\": \"Identifier\",\n      \"identifier-filter\": \"Identifier filter\",\n      \"no-sso-found\": \"Geen SSO gevonden.\",\n      \"redirect-url\": \"Redirect URL\",\n      \"scopes\": \"Scopes\",\n      \"single-sign-on\": \"Single Sign-On (SSO) configureren voor authenticatie\",\n      \"sso-created\": \"SSO {{name}} ingesteld\",\n      \"sso-list\": \"SSO lijst\",\n      \"sso-updated\": \"SSO {{name}} bijgewerkt\",\n      \"template\": \"Sjabloon\",\n      \"token-endpoint\": \"Token-eindpunt\",\n      \"update-sso\": \"SSO bijwerken\",\n      \"user-endpoint\": \"Gebruikers-eindpunt\",\n      \"label\": \"SSO\"\n    },\n    \"storage\": {\n      \"accesskey\": \"Access key\",\n      \"accesskey-placeholder\": \"Access key / Access ID\",\n      \"bucket\": \"Bucket\",\n      \"bucket-placeholder\": \"Bucket naam\",\n      \"create-a-service\": \"Opslagdienst instellen\",\n      \"create-storage\": \"Opslagdienst aanmaken\",\n      \"current-storage\": \"Huidige objectopslag\",\n      \"delete-storage\": \"Opslagdienst verwijderen\",\n      \"endpoint\": \"Endpoint\",\n      \"filepath-template\": \"Bestandspad sjabloon\",\n      \"local-storage-path\": \"Lokaal opslagpad\",\n      \"path\": \"Opslagpad\",\n      \"path-description\": \"Je kan dezelfde dynamische variabelen gebruiken, zoals {filename}\",\n      \"path-placeholder\": \"eigen/pad\",\n      \"presign-placeholder\": \"Pre-sign URL, optioneel\",\n      \"region\": \"Regio\",\n      \"region-placeholder\": \"Regionaam\",\n      \"s3-compatible-url\": \"S3 compatibele URL\",\n      \"secretkey\": \"Secret key\",\n      \"secretkey-placeholder\": \"Secret key / Access Key\",\n      \"storage-services\": \"Opslagdiensten\",\n      \"type-database\": \"Database\",\n      \"type-local\": \"Lokaal bestandssysteem\",\n      \"update-a-service\": \"Opslagdienst bijwerken\",\n      \"update-local-path\": \"Lokaal opslagpad bijwerken\",\n      \"update-local-path-description\": \"Lokaal opslagpad is relatief aan je databasebestand.\",\n      \"update-storage\": \"Opslagdienst bijwerken\",\n      \"url-prefix\": \"URL-prefix\",\n      \"url-prefix-placeholder\": \"Aangepaste URL-prefix, optioneel\",\n      \"url-suffix\": \"URL-suffix\",\n      \"url-suffix-placeholder\": \"Aangepaste URL-suffix, optioneel\",\n      \"warning-text\": \"Weet je zeker dat je opslagdienst \\\"{{name}}\\\" wilt verwijderen? DEZE ACTIE IS NIET TERUG TE DRAAIEN!\",\n      \"label\": \"Opslag\"\n    },\n    \"system\": {\n      \"additional-script\": \"Optionele scripts\",\n      \"additional-script-placeholder\": \"Optionele JavaScript code\",\n      \"additional-style\": \"Optionele stijl\",\n      \"additional-style-placeholder\": \"Optionele CSS code\",\n      \"allow-user-signup\": \"Registratie toestaan\",\n      \"customize-server\": {\n        \"description\": \"Beschrijving\",\n        \"icon-url\": \"Pictogram-URL\",\n        \"locale\": \"Servertaal\",\n        \"title\": \"Server aanpassen\"\n      },\n      \"disable-password-login\": \"Wachtwoordlogin uitzetten\",\n      \"disable-password-login-final-warning\": \"Typ \\\"CONFIRM\\\" als je zeker weet wat je doet.\",\n      \"disable-password-login-warning\": \"Als je dit uitzet, kan je niet meer inloggen met een wachtwoord, zonder de database aan te passen. Als je inlogprovider niet werkt, kan je memos niet in.\",\n      \"display-with-updated-time\": \"Weergeven met bewerkte tijd\",\n      \"enable-auto-compact\": \"Automatisch compact inschakelen\",\n      \"enable-double-click-to-edit\": \"Dubbelklik om te bewerken inschakelen\",\n      \"enable-password-login\": \"Wachtwoordlogin aanzetten\",\n      \"enable-password-login-warning\": \"Als je dit aanzet kan er met zowel SSO als een wachtwoord ingelogd worden\",\n      \"max-upload-size\": \"Maximum uploadgrootte (MiB)\",\n      \"max-upload-size-hint\": \"32 MiB wordt aangeraden.\",\n      \"removed-completed-task-list-items\": \"Verwijder voltooid inschakelen\",\n      \"server-name\": \"Servernaam\",\n      \"title\": \"Algemeen\",\n      \"label\": \"Systeem\"\n    },\n    \"version\": \"Versie\",\n    \"access-token\": {\n      \"access-token-copied-to-clipboard\": \"Accesstoken gekopieerd naar klembord\",\n      \"access-token-deleted\": \"Accesstoken `{{description}}` verwijderd\",\n      \"access-token-deletion\": \"Weet je zeker dat je accesstoken `{{description}}` wilt verwijderen? DEZE ACTIE IS NIET TERUG TE DRAAIEN.\",\n      \"access-token-deletion-description\": \"Deze actie is onomkeerelijk. Je moet alle services die deze token gebruiken bijwerken om een nieuwe token te gebruiken.\",\n      \"create-dialog\": {\n        \"access-token-created\": \"Accesstoken `{{description}}` aangemaakt\",\n        \"create-access-token\": \"Accesstoken aanmaken\",\n        \"created-at\": \"Aangemaakt op\",\n        \"description\": \"Beschrijving\",\n        \"duration-1m\": \"1 maand\",\n        \"duration-8h\": \"8 uur\",\n        \"duration-never\": \"Nooit\",\n        \"expiration\": \"Vervaldatum\",\n        \"expires-at\": \"Verloopt op\",\n        \"some-description\": \"Een beschrijving…\"\n      },\n      \"description\": \"Lijst van alle accesstokens voor je account.\",\n      \"title\": \"Accesstokens\",\n      \"token\": \"Token\"\n    },\n    \"account\": {\n      \"change-password\": \"Wachtwoord wijzigen\",\n      \"email-note\": \"Optioneel\",\n      \"export-memos\": \"Memo's exporteren\",\n      \"nickname-note\": \"Wordt getoond in de banner\",\n      \"openapi-reset\": \"OpenAPI-sleutel resetten\",\n      \"openapi-sample-post\": \"Hallo #memos vanaf {{url}}\",\n      \"openapi-title\": \"OpenAPI-sleutel\",\n      \"reset-api\": \"API resetten\",\n      \"title\": \"Accountinformatie\",\n      \"update-information\": \"Informatie wijzigen\",\n      \"username-note\": \"Gebruikt om in te loggen\"\n    },\n    \"instance\": {\n      \"disallow-change-nickname\": \"Wijzigen van bijnaam niet toestaan\",\n      \"disallow-change-username\": \"Wijzigen van gebruikersnaam niet toestaan\",\n      \"disallow-password-auth\": \"Wachtwoordauthenticatie niet toestaan\",\n      \"disallow-user-registration\": \"Registratie van gebruikers niet toestaan\",\n      \"monday\": \"Maandag\",\n      \"saturday\": \"Zaterdag\",\n      \"sunday\": \"Zondag\",\n      \"week-start-day\": \"Eerste weekdag\"\n    },\n    \"memo\": {\n      \"content-length-limit\": \"Maximale inhoudslengte (Byte)\",\n      \"enable-blur-nsfw-content\": \"NSFW-inhoud vervagen inschakelen\",\n      \"enable-memo-comments\": \"Memo-opmerkingen inschakelen\",\n      \"enable-memo-location\": \"Memo-locatie inschakelen\",\n      \"reactions\": \"Reacties\",\n      \"title\": \"Memo-gerelateerde instellingen\",\n      \"label\": \"Memo\"\n    },\n    \"webhook\": {\n      \"create-dialog\": {\n        \"an-easy-to-remember-name\": \"Een makkelijk te onthouden naam\",\n        \"create-webhook\": \"Webhook aanmaken\",\n        \"create-webhook-success\": \"Webhook `{{name}}` aangemaakt\",\n        \"edit-webhook\": \"Webhook bewerken\",\n        \"payload-url\": \"Payload URL\",\n        \"title\": \"Titel\",\n        \"url-example-post-receive\": \"https://voorbeeld.com/postreceive\"\n      },\n      \"delete-dialog\": {\n        \"delete-webhook-description\": \"Deze actie is onomkeerelijk.\",\n        \"delete-webhook-success\": \"Webhook `{{name}}` succesvol verwijderd\",\n        \"delete-webhook-title\": \"Weet je zeker dat je webhook `{{name}}` wilt verwijderen?\"\n      },\n      \"no-webhooks-found\": \"Geen webhooks gevonden.\",\n      \"title\": \"Webhooks\",\n      \"url\": \"URL\"\n    }\n  },\n  \"tag\": {\n    \"all-tags\": \"Alle labels\",\n    \"create-tag\": \"Label aanmaken\",\n    \"create-tags-guide\": \"Je kunt labels aanmaken door `#label` in te voeren.\",\n    \"delete-confirm\": \"Weet je zeker dat je dit label wilt verwijderen? Alle gerelateerde memos worden gearchiveerd.\",\n    \"delete-success\": \"Label succesvol verwijderd\",\n    \"delete-tag\": \"Label verwijderen\",\n    \"new-name\": \"Nieuwe naam\",\n    \"no-tag-found\": \"Geen labels gevonden\",\n    \"old-name\": \"Oude naam\",\n    \"rename-error-empty\": \"Labelnaam mag niet leeg zijn of spaties bevatten\",\n    \"rename-error-repeat\": \"Nieuwe naam mag niet hetzelfde zijn als de oude naam\",\n    \"rename-success\": \"Label succesvol hernoemd\",\n    \"rename-tag\": \"Label hernoemen\",\n    \"rename-tip\": \"Al je memos met dit label worden automatisch bijgewerkt.\"\n  },\n  \"tooltip\": {\n    \"link-memo\": \"Memo linken\",\n    \"markdown-menu\": \"Markdown\",\n    \"select-location\": \"Locatie\",\n    \"select-visibility\": \"Zichtbaarheid\",\n    \"tags\": \"Labels\",\n    \"upload-attachment\": \"Bijlage(s) uploaden\"\n  }\n}\n"
  },
  {
    "path": "web/src/locales/pl.json",
    "content": "{\n  \"about\": {\n    \"blogs\": \"Blogi\",\n    \"description\": \"Usługa notatek z naciskiem na prywatność i lekkość. Łatwo zapisuj i udostępniaj swoje myśli.\",\n    \"documents\": \"Dokumenty\",\n    \"github-repository\": \"Repozytorium GitHub\",\n    \"official-website\": \"Oficjalna strona\"\n  },\n  \"auth\": {\n    \"create-your-account\": \"Utwórz swoje konto\",\n    \"host-tip\": \"Rejestrujesz się jako Gospodarz Strony.\",\n    \"new-password\": \"Nowe hasło\",\n    \"repeat-new-password\": \"Powtórz nowe hasło\",\n    \"sign-in-tip\": \"Masz już konto?\",\n    \"sign-up-tip\": \"Nie masz jeszcze konta?\"\n  },\n  \"common\": {\n    \"about\": \"O nas\",\n    \"add\": \"Dodaj\",\n    \"admin\": \"Administrator\",\n    \"all\": \"Wszystkie\",\n    \"archive\": \"Archiwum\",\n    \"archived\": \"Zarchiwizowane\",\n    \"attachments\": \"Załączniki\",\n    \"auto-expand\": \"Rozwiń automatycznie\",\n    \"avatar\": \"Awatar\",\n    \"basic\": \"Podstawowy\",\n    \"beta\": \"Beta\",\n    \"calendar\": \"Kalendarz\",\n    \"cancel\": \"Anuluj\",\n    \"change\": \"Zmień\",\n    \"clear\": \"Wyczyść\",\n    \"close\": \"Zamknij\",\n    \"collapse\": \"Zwiń\",\n    \"confirm\": \"Potwierdź\",\n    \"copy\": \"Kopiuj\",\n    \"create\": \"Utwórz\",\n    \"created-at\": \"Utworzono\",\n    \"database\": \"Baza danych\",\n    \"day\": \"Dzień\",\n    \"days\": {\n      \"fri\": \"Pt\",\n      \"mon\": \"Pn\",\n      \"sat\": \"Sb\",\n      \"sun\": \"Nd\",\n      \"thu\": \"Czw\",\n      \"tue\": \"Wt\",\n      \"wed\": \"Śr\"\n    },\n    \"delete\": \"Usuń\",\n    \"description\": \"Opis\",\n    \"edit\": \"Edytuj\",\n    \"email\": \"Email\",\n    \"expand\": \"Rozwiń\",\n    \"explore\": \"Odkryj\",\n    \"file\": \"Plik\",\n    \"filter\": \"Filtr\",\n    \"home\": \"Strona główna\",\n    \"image\": \"Obraz\",\n    \"in\": \"W\",\n    \"inbox\": \"Skrzynka odbiorcza\",\n    \"input\": \"Wejście\",\n    \"language\": \"Język\",\n    \"last-updated-at\": \"Ostatnia aktualizacja\",\n    \"learn-more\": \"Dowiedz się więcej\",\n    \"link\": \"Link\",\n    \"map\": \"Mapa\",\n    \"mark\": \"Oznacz\",\n    \"memo\": \"Notatka\",\n    \"memos\": \"Notatki\",\n    \"more\": \"Więcej\",\n    \"name\": \"Nazwa\",\n    \"new\": \"Nowy\",\n    \"nickname\": \"Pseudonim\",\n    \"null\": \"Pusty\",\n    \"or\": \"lub\",\n    \"password\": \"Hasło\",\n    \"pin\": \"Przypnij\",\n    \"pinned\": \"Przypięte\",\n    \"preview\": \"Podgląd\",\n    \"profile\": \"Profil\",\n    \"properties\": \"Właściwości\",\n    \"referenced-by\": \"Wspomniane przez\",\n    \"referencing\": \"Odwołuje się do\",\n    \"relations\": \"Relacje\",\n    \"remember-me\": \"Zapamiętaj mnie\",\n    \"rename\": \"Zmień nazwę\",\n    \"reset\": \"Resetuj\",\n    \"resources\": \"Zasoby\",\n    \"restore\": \"Przywróć\",\n    \"role\": \"Rola\",\n    \"save\": \"Zapisz\",\n    \"search\": \"Szukaj\",\n    \"select\": \"Wybierz\",\n    \"settings\": \"Ustawienia\",\n    \"share\": \"Udostępnij\",\n    \"shortcut-filter\": \"Filtr skrótów\",\n    \"shortcuts\": \"Skróty\",\n    \"sign-in\": \"Zaloguj się\",\n    \"sign-in-with\": \"Zaloguj się za pomocą {{provider}}\",\n    \"sign-out\": \"Wyloguj się\",\n    \"sign-up\": \"Zarejestruj się\",\n    \"statistics\": \"Statystyki\",\n    \"tags\": \"Tagi\",\n    \"title\": \"Tytuł\",\n    \"today\": \"Dziś\",\n    \"tree-mode\": \"Tryb drzewa\",\n    \"type\": \"Typ\",\n    \"unpin\": \"Odepnij\",\n    \"update\": \"Aktualizuj\",\n    \"upload\": \"Prześlij\",\n    \"user\": \"Użytkownik\",\n    \"username\": \"Nazwa użytkownika\",\n    \"version\": \"Wersja\",\n    \"visibility\": \"Widoczność\",\n    \"yourself\": \"Ty sam\"\n  },\n  \"editor\": {\n    \"add-your-comment-here\": \"Dodaj swój komentarz tutaj...\",\n    \"any-thoughts\": \"Jakieś przemyślenia...\",\n    \"exit-focus-mode\": \"Wyjdź z Trybu Skupienia\",\n    \"focus-mode\": \"Tryb Skupienia\",\n    \"no-changes-detected\": \"Brak wykrytych zmian\",\n    \"save\": \"Zapisz\",\n    \"saving\": \"Zapisywanie...\",\n    \"slash-commands\": \"Wciśnij `/` aby wydawać komendy\"\n  },\n  \"inbox\": {\n    \"failed-to-load\": \"Nie udało się załadować elementu skrzynki odbiorczej\",\n    \"memo-comment\": \"{{user}} dodał komentarz do Twojego {{memo}}.\",\n    \"no-archived\": \"Brak zarchiwizowanych powiadomień\",\n    \"no-unread\": \"Brak nieprzeczytanych powiadomień\",\n    \"unread\": \"Nieprzeczytane\",\n    \"version-update\": \"Nowa wersja {{version}} jest już dostępna!\"\n  },\n  \"markdown\": {\n    \"checkbox\": \"Pole wyboru\",\n    \"code-block\": \"Blok kodu\",\n    \"content-syntax\": \"Składnia treści\"\n  },\n  \"memo\": {\n    \"archived-at\": \"Zarchiwizowano w dniu\",\n    \"click-to-hide-nsfw-content\": \"Kliknij, aby ukryć treści NSFW\",\n    \"click-to-show-nsfw-content\": \"Kliknij, aby pokazać treści NSFW\",\n    \"code\": \"Kod\",\n    \"comment\": {\n      \"self\": \"Komentarze\",\n      \"write-a-comment\": \"Napisz komentarz\"\n    },\n    \"copy-content\": \"Kopiuj treść\",\n    \"copy-link\": \"Kopiuj link\",\n    \"count-memos-in-date\": \"{{count}} {{memos}} w dniu {{date}}\",\n    \"delete-confirm\": \"Czy na pewno chcesz usunąć tę notatkę?\",\n    \"delete-confirm-description\": \"Czy na pewno chcesz usunąć tę notatkę? Załączniki, linki i odnośniki także będą usunięte. TA AKCJA JEST NIEODWRACALNA\",\n    \"direction\": \"Kierunek\",\n    \"direction-asc\": \"Rosnąco\",\n    \"direction-desc\": \"Malejąco\",\n    \"display-time\": \"Wyświetl czas\",\n    \"filters\": {\n      \"has-code\": \"ma kod\",\n      \"has-link\": \"ma link\",\n      \"has-task-list\": \"ma listę zadań\"\n    },\n    \"links\": \"Linki\",\n    \"load-more\": \"Załaduj więcej\",\n    \"no-archived-memos\": \"Brak zarchiwizowanych notatek.\",\n    \"no-memos\": \"Brak notatek.\",\n    \"order-by\": \"Sortuj według\",\n    \"search-placeholder\": \"Szukaj notatek\",\n    \"show-less\": \"Pokaż mniej\",\n    \"show-more\": \"Pokaż więcej\",\n    \"to-do\": \"Lista zadań\",\n    \"view-detail\": \"Zobacz szczegóły\",\n    \"visibility\": {\n      \"disabled\": \"Publiczne notatki są wyłączone\",\n      \"private\": \"Prywatne\",\n      \"protected\": \"Przestrzeń robocza\",\n      \"public\": \"Publiczne\"\n    }\n  },\n  \"message\": {\n    \"archived-successfully\": \"Zarchiwizowano pomyślnie\",\n    \"change-memo-created-time\": \"Zmień datę utworzenia notatki\",\n    \"copied\": \"Skopiowano\",\n    \"deleted-successfully\": \"Usunięto pomyślnie\",\n    \"description-is-required\": \"Opis jest wymagany\",\n    \"failed-to-embed-memo\": \"Nie udało się osadzić notatki\",\n    \"fill-all\": \"Proszę wypełnić wszystkie pola.\",\n    \"fill-all-required-fields\": \"Proszę wypełnić wszystkie wymagane pola\",\n    \"maximum-upload-size-is\": \"Maksymalny dozwolony rozmiar przesyłanego pliku to {{size}} MiB\",\n    \"memo-not-found\": \"Notatka nie została znaleziona.\",\n    \"new-password-not-match\": \"Nowe hasła nie pasują do siebie.\",\n    \"no-data\": \"Nie znaleziono danych.\",\n    \"password-changed\": \"Hasło zostało zmienione\",\n    \"password-not-match\": \"Hasła nie pasują do siebie.\",\n    \"restored-successfully\": \"Przywrócono pomyślnie\",\n    \"succeed-copy-content\": \"Treść została pomyślnie skopiowana.\",\n    \"succeed-copy-link\": \"Link skopiowany pomyślnie.\",\n    \"update-succeed\": \"Aktualizacja zakończona sukcesem\",\n    \"user-not-found\": \"Użytkownik nie został znaleziony\"\n  },\n  \"reference\": {\n    \"add-references\": \"Dodaj odniesienia\",\n    \"embedded-usage\": \"Użyj jako treść osadzoną\",\n    \"no-memos-found\": \"Nie znaleziono notatek\",\n    \"search-placeholder\": \"Szukaj treści\"\n  },\n  \"resource\": {\n    \"clear\": \"Wyczyść\",\n    \"copy-link\": \"Kopiuj link\",\n    \"create-dialog\": {\n      \"external-link\": {\n        \"file-name\": \"Nazwa pliku\",\n        \"file-name-placeholder\": \"Nazwa pliku\",\n        \"link\": \"Link\",\n        \"link-placeholder\": \"https://link.do/twojego/zasobu\",\n        \"option\": \"Zewnętrzny link\",\n        \"type\": \"Typ\",\n        \"type-placeholder\": \"Typ pliku\"\n      },\n      \"local-file\": {\n        \"choose\": \"Wybierz plik…\",\n        \"option\": \"Plik lokalny\"\n      },\n      \"title\": \"Utwórz Zasób\",\n      \"upload-method\": \"Metoda przesyłania\"\n    },\n    \"delete-all-unused\": \"Usuń wszystkie nieużywane\",\n    \"delete-all-unused-confirm\": \"Na pewno chcesz usunąć wszystkie nieużywane zasoby?? TA AKCJA JEST NIEODWRACALNA\",\n    \"delete-all-unused-error\": \"Nie udało się usunąć nieużywanych zasobów\",\n    \"delete-all-unused-success\": \"Zasoby zostały pomyślnie usunięte\",\n    \"delete-resource\": \"Usuń zasób\",\n    \"delete-selected-resources\": \"Usuń wybrane zasoby\",\n    \"fetching-data\": \"Pobieranie danych…\",\n    \"file-drag-drop-prompt\": \"Przeciągnij i upuść plik tutaj, aby go przesłać\",\n    \"linked-amount\": \"Ilość powiązań\",\n    \"no-files-selected\": \"Nie wybrano plików\",\n    \"no-resources\": \"Brak zasobów.\",\n    \"no-unused-resources\": \"Brak nieużywanych zasobów\",\n    \"reset-link\": \"Resetuj link\",\n    \"reset-link-prompt\": \"Czy na pewno chcesz zresetować link? Spowoduje to zerwanie wszystkich obecnych powiązań linku. TA AKCJA JEST NIEODWRACALNA\",\n    \"reset-resource-link\": \"Resetuj link zasobu\",\n    \"unused-resources\": \"Nieużywane zasoby\"\n  },\n  \"router\": {\n    \"back-to-top\": \"Powrót na górę\",\n    \"go-to-home\": \"Przejdź do strony głównej\"\n  },\n  \"setting\": {\n    \"member\": {\n      \"admin\": \"Administrator\",\n      \"archive-member\": \"Archiwizuj członka\",\n      \"archive-success\": \"{{username}} pomyślnie zarchiwizowany\",\n      \"archive-warning\": \"Czy na pewno chcesz zarchiwizować {{username}}?\",\n      \"archive-warning-description\": \"Archiwizacja zablokuje to konto. Możesz je odblokować lub usunąć później.\",\n      \"create-a-member\": \"Utwórz członka\",\n      \"delete-member\": \"Usuń członka\",\n      \"delete-success\": \"{{username}} pomyślnie usunięty\",\n      \"delete-warning\": \"Czy na pewno chcesz usunąć {{username}}?\",\n      \"delete-warning-description\": \"TA AKCJA JEST NIEODWRACALNA\",\n      \"restore-success\": \"{{username}} pomyślnie odtworzony\",\n      \"user\": \"Użytkownik\",\n      \"label\": \"Członek\",\n      \"list-title\": \"Lista członków\"\n    },\n    \"my-account\": {\n      \"label\": \"Moje konto\"\n    },\n    \"preference\": {\n      \"default-memo-sort-option\": \"Domyślny czas wyświetlania notatek\",\n      \"default-memo-visibility\": \"Domyślna widoczność notatek\",\n      \"theme\": \"Motyw\",\n      \"label\": \"Preferencje\"\n    },\n    \"shortcut\": {\n      \"delete-confirm\": \"Czy na pewno chcesz usunąć skrót `{{title}}`?\",\n      \"delete-success\": \"Skrót `{{title}}` usunięty pomyślnie\"\n    },\n    \"sso\": {\n      \"authorization-endpoint\": \"Punkt autoryzacji\",\n      \"client-id\": \"ID klienta\",\n      \"client-secret\": \"Tajny klucz klienta\",\n      \"confirm-delete\": \"Czy na pewno chcesz usunąć konfigurację SSO \\\"{{name}}\\\"? TA AKCJA JEST NIEODWRACALNA\",\n      \"create-sso\": \"Utwórz SSO\",\n      \"custom\": \"Niestandardowe\",\n      \"delete-sso\": \"Potwierdź usunięcie\",\n      \"disabled-password-login-warning\": \"Logowanie hasłem jest wyłączone, zachowaj szczególną ostrożność przy usuwaniu dostawców tożsamości\",\n      \"display-name\": \"Nazwa wyświetlana\",\n      \"identifier\": \"Identyfikator\",\n      \"identifier-filter\": \"Filtr identyfikatorów\",\n      \"no-sso-found\": \"Nie znaleziono SSO.\",\n      \"redirect-url\": \"URL przekierowania\",\n      \"scopes\": \"Zakresy\",\n      \"single-sign-on\": \"Konfiguracja Single Sign-On (SSO) do uwierzytelniania\",\n      \"sso-created\": \"SSO {{name}} utworzono\",\n      \"sso-list\": \"Lista SSO\",\n      \"sso-updated\": \"SSO {{name}} zaktualizowano\",\n      \"template\": \"Szablon\",\n      \"token-endpoint\": \"Punkt wydawania tokenów\",\n      \"update-sso\": \"Zaktualizuj SSO\",\n      \"user-endpoint\": \"Punkt użytkownika\",\n      \"label\": \"SSO\"\n    },\n    \"storage\": {\n      \"accesskey\": \"Klucz dostępu\",\n      \"accesskey-placeholder\": \"Klucz dostępu / ID dostępu\",\n      \"bucket\": \"Bucket\",\n      \"bucket-placeholder\": \"Nazwa bucket\",\n      \"create-a-service\": \"Utwórz usługę\",\n      \"create-storage\": \"Utwórz magazyn\",\n      \"current-storage\": \"Aktualny magazyn obiektów\",\n      \"delete-storage\": \"Usuń magazyn\",\n      \"endpoint\": \"Punkt końcowy\",\n      \"filepath-template\": \"Szablon ścieżki pliku\",\n      \"local-storage-path\": \"Ścieżka lokalnego magazynu\",\n      \"path\": \"Ścieżka przechowywania\",\n      \"path-description\": \"Możesz użyć tych samych dynamicznych zmiennych jak w lokalnym magazynie, takich jak {filename}\",\n      \"path-placeholder\": \"niestandardowa/ścieżka\",\n      \"presign-placeholder\": \"Wstępnie podpisany URL, opcjonalnie\",\n      \"region\": \"Region\",\n      \"region-placeholder\": \"Nazwa regionu\",\n      \"s3-compatible-url\": \"URL zgodny z S3\",\n      \"secretkey\": \"Tajny klucz\",\n      \"secretkey-placeholder\": \"Tajny klucz / Klucz dostępu\",\n      \"storage-services\": \"Usługi przechowywania\",\n      \"type-database\": \"Baza danych\",\n      \"type-local\": \"Lokalny system plików\",\n      \"update-a-service\": \"Zaktualizuj usługę\",\n      \"update-local-path\": \"Zaktualizuj ścieżkę lokalnego magazynu\",\n      \"update-local-path-description\": \"Ścieżka lokalnego magazynu to ścieżka względna do pliku bazy danych\",\n      \"update-storage\": \"Zaktualizuj magazyn\",\n      \"url-prefix\": \"Prefiks URL\",\n      \"url-prefix-placeholder\": \"Niestandardowy prefiks URL, opcjonalnie\",\n      \"url-suffix\": \"Sufiks URL\",\n      \"url-suffix-placeholder\": \"Niestandardowy sufiks URL, opcjonalnie\",\n      \"warning-text\": \"Czy na pewno chcesz usunąć usługę przechowywania \\\"{{name}}\\\"? TA AKCJA JEST NIEODWRACALNA\",\n      \"label\": \"Przechowywanie\"\n    },\n    \"system\": {\n      \"additional-script\": \"Dodatkowy skrypt\",\n      \"additional-script-placeholder\": \"Dodatkowy kod JavaScript\",\n      \"additional-style\": \"Dodatkowy styl\",\n      \"additional-style-placeholder\": \"Dodatkowy kod CSS\",\n      \"allow-user-signup\": \"Zezwól na rejestrację użytkowników\",\n      \"customize-server\": {\n        \"description\": \"Opis\",\n        \"icon-url\": \"URL ikony\",\n        \"locale\": \"Język serwera\",\n        \"title\": \"Dostosuj serwer\"\n      },\n      \"disable-password-login\": \"Wyłącz logowanie hasłem\",\n      \"disable-password-login-final-warning\": \"Proszę wpisać \\\"CONFIRM\\\", jeśli wiesz, co robisz.\",\n      \"disable-password-login-warning\": \"To wyłączy logowanie hasłem dla wszystkich użytkowników. Jeśli skonfigurowani dostawcy tożsamości zawiodą, nie będzie możliwe zalogowanie się bez przywrócenia tego ustawienia w bazie danych. Bądź także szczególnie ostrożny przy usuwaniu dostawcy tożsamości\",\n      \"display-with-updated-time\": \"Wyświetlaj z czasem aktualizacji\",\n      \"enable-auto-compact\": \"Włącz automatyczne kompresowanie\",\n      \"enable-double-click-to-edit\": \"Włącz edycję przez podwójne kliknięcie\",\n      \"enable-password-login\": \"Włącz logowanie hasłem\",\n      \"enable-password-login-warning\": \"To włączy logowanie hasłem dla wszystkich użytkowników. Kontynuuj tylko wtedy, gdy chcesz, aby użytkownicy mogli logować się za pomocą zarówno SSO, jak i hasła\",\n      \"max-upload-size\": \"Maksymalny rozmiar przesyłanego pliku (MiB)\",\n      \"max-upload-size-hint\": \"Zalecana wartość to 32 MiB.\",\n      \"removed-completed-task-list-items\": \"Włącz usuwanie zakończonych zadań\",\n      \"server-name\": \"Nazwa serwera\",\n      \"title\": \"Ogólne\",\n      \"label\": \"System\"\n    },\n    \"version\": \"Wersja\",\n    \"access-token\": {\n      \"access-token-copied-to-clipboard\": \"Token dostępu skopiowany do schowka\",\n      \"access-token-deleted\": \"Token dostępu `{{description}}` został usunięty\",\n      \"access-token-deletion\": \"Czy na pewno chcesz usunąć token dostępu `{{description}}`?\",\n      \"access-token-deletion-description\": \"TA AKCJA JEST NIEODWRACALNA. Będziesz musiał zaktualizować wszystkie serwisy korzystające z tego tokena aby używały nowego tokena.\",\n      \"create-dialog\": {\n        \"access-token-created\": \"Utworzono token dostępu `{{description}}`\",\n        \"create-access-token\": \"Utwórz token dostępu\",\n        \"created-at\": \"Utworzono\",\n        \"description\": \"Opis\",\n        \"duration-1m\": \"1 miesiąc\",\n        \"duration-8h\": \"8 godzin\",\n        \"duration-never\": \"Nigdy\",\n        \"expiration\": \"Wygaśnięcie\",\n        \"expires-at\": \"Wygasa\",\n        \"some-description\": \"Jakiś opis...\"\n      },\n      \"description\": \"Lista wszystkich tokenów dostępu do Twojego konta.\",\n      \"title\": \"Tokeny dostępu\",\n      \"token\": \"Token\"\n    },\n    \"account\": {\n      \"change-password\": \"Zmień hasło\",\n      \"email-note\": \"Opcjonalne\",\n      \"export-memos\": \"Eksportuj notatki\",\n      \"nickname-note\": \"Wyświetlane w banerze\",\n      \"openapi-reset\": \"Resetuj klucz OpenAPI\",\n      \"openapi-sample-post\": \"Witaj #memos z {{url}}\",\n      \"openapi-title\": \"OpenAPI\",\n      \"reset-api\": \"Resetuj API\",\n      \"title\": \"Informacje o koncie\",\n      \"update-information\": \"Zaktualizuj informacje\",\n      \"username-note\": \"Używane do logowania\"\n    },\n    \"instance\": {\n      \"disallow-change-nickname\": \"Zabroń zmiany pseudonimu\",\n      \"disallow-change-username\": \"Zabroń zmiany nazwy użytkownika\",\n      \"disallow-password-auth\": \"Zabroń uwierzytelniania hasłem\",\n      \"disallow-user-registration\": \"Zabroń rejestracji użytkowników\",\n      \"monday\": \"Poniedziałek\",\n      \"saturday\": \"Sobota\",\n      \"sunday\": \"Niedziela\",\n      \"week-start-day\": \"Dzień rozpoczęcia tygodnia\"\n    },\n    \"memo\": {\n      \"content-length-limit\": \"Limit długości treści (Bajty)\",\n      \"enable-blur-nsfw-content\": \"Włącz rozmycie treści NSFW\",\n      \"enable-memo-comments\": \"Włącz komentarze do notatek\",\n      \"enable-memo-location\": \"Włącz lokalizację notatek\",\n      \"reactions\": \"Reakcje\",\n      \"title\": \"Ustawienia notatek\",\n      \"label\": \"Notatki\"\n    },\n    \"webhook\": {\n      \"create-dialog\": {\n        \"an-easy-to-remember-name\": \"Łatwa do zapamiętania nazwa\",\n        \"create-webhook\": \"Utwórz webhook\",\n        \"create-webhook-success\": \"Webhook `{{name}}` utworzony\",\n        \"edit-webhook\": \"Edytuj webhook\",\n        \"payload-url\": \"URL payload\",\n        \"title\": \"Tytuł\",\n        \"url-example-post-receive\": \"https://przyklad.com/postreceive\"\n      },\n      \"delete-dialog\": {\n        \"delete-webhook-description\": \"Ta akcja jest nieodwracalna\",\n        \"delete-webhook-success\": \"Webhook `{{name}}` pomyślnie usunięty\",\n        \"delete-webhook-title\": \"Czy na pewno chcesz usunąć webhook `{{name}}`?\"\n      },\n      \"no-webhooks-found\": \"Nie znaleziono webhooków.\",\n      \"title\": \"Webhooki\",\n      \"url\": \"URL\"\n    }\n  },\n  \"tag\": {\n    \"all-tags\": \"Wszystkie tagi\",\n    \"create-tag\": \"Utwórz tag\",\n    \"create-tags-guide\": \"Możesz tworzyć tagi, wpisując `#tag`.\",\n    \"delete-confirm\": \"Czy na pewno chcesz usunąć ten tag? Wszystkie powiązane notatki zostaną zarchiwizowane.\",\n    \"delete-success\": \"Tag został pomyślnie usunięty\",\n    \"delete-tag\": \"Usuń tag\",\n    \"new-name\": \"Nowa nazwa\",\n    \"no-tag-found\": \"Nie znaleziono tagu\",\n    \"old-name\": \"Stara nazwa\",\n    \"rename-error-empty\": \"Nazwa tagu nie może być pusta ani zawierać spacji\",\n    \"rename-error-repeat\": \"Nowa nazwa nie może być taka sama jak stara nazwa\",\n    \"rename-success\": \"Tag został pomyślnie zmieniony\",\n    \"rename-tag\": \"Zmień nazwę tagu\",\n    \"rename-tip\": \"Wszystkie Twoje notatki z tym tagiem zostaną zaktualizowane.\"\n  },\n  \"tooltip\": {\n    \"link-memo\": \"Podlinkuj Notatkę\",\n    \"markdown-menu\": \"Markdown\",\n    \"select-location\": \"Lokalizacja\",\n    \"select-visibility\": \"Widoczność\",\n    \"tags\": \"Tagi\",\n    \"upload-attachment\": \"Prześlij Załącznik(i)\"\n  }\n}\n"
  },
  {
    "path": "web/src/locales/pt-BR.json",
    "content": "{\n  \"about\": {\n    \"blogs\": \"Blogs\",\n    \"description\": \"Um serviço de anotações leve que prioriza a privacidade. Capture e compartilhe facilmente suas grandes ideias.\",\n    \"documents\": \"Documentação\",\n    \"github-repository\": \"Repositório do GitHub\",\n    \"official-website\": \"Site Oficial\"\n  },\n  \"auth\": {\n    \"create-your-account\": \"Crie sua conta\",\n    \"host-tip\": \"Você está se registrando como Administrador desta instância.\",\n    \"new-password\": \"Nova senha\",\n    \"repeat-new-password\": \"Repita a nova senha\",\n    \"sign-in-tip\": \"Já tem uma conta?\",\n    \"sign-up-tip\": \"Ainda não tem uma conta?\"\n  },\n  \"common\": {\n    \"about\": \"Sobre\",\n    \"add\": \"Adicionar\",\n    \"admin\": \"Admin\",\n    \"all\": \"Todos\",\n    \"archive\": \"Arquivar\",\n    \"archived\": \"Arquivo\",\n    \"attachments\": \"Anexos\",\n    \"auto-expand\": \"Expandir automaticamente\",\n    \"avatar\": \"Avatar\",\n    \"basic\": \"Básico\",\n    \"beta\": \"Beta\",\n    \"calendar\": \"Calendário\",\n    \"cancel\": \"Cancelar\",\n    \"change\": \"Alterar\",\n    \"clear\": \"Limpar\",\n    \"close\": \"Fechar\",\n    \"collapse\": \"Recolher\",\n    \"confirm\": \"Confirmar\",\n    \"copy\": \"Copiar\",\n    \"create\": \"Criar\",\n    \"created-at\": \"Criado em\",\n    \"database\": \"Banco de dados\",\n    \"day\": \"Dia\",\n    \"days\": {\n      \"fri\": \"Sex\",\n      \"mon\": \"Seg\",\n      \"sat\": \"Sáb\",\n      \"sun\": \"Dom\",\n      \"thu\": \"Qui\",\n      \"tue\": \"Ter\",\n      \"wed\": \"Qua\"\n    },\n    \"delete\": \"Deletar\",\n    \"description\": \"Descrição\",\n    \"edit\": \"Editar\",\n    \"email\": \"Email\",\n    \"expand\": \"Expandir\",\n    \"explore\": \"Explorar\",\n    \"file\": \"Arquivo\",\n    \"filter\": \"Filtro\",\n    \"home\": \"Início\",\n    \"image\": \"Imagem\",\n    \"in\": \"Em\",\n    \"inbox\": \"Notificações\",\n    \"input\": \"Entrada\",\n    \"language\": \"Idioma\",\n    \"last-updated-at\": \"Atualizado em\",\n    \"learn-more\": \"Saiba mais\",\n    \"link\": \"Link\",\n    \"map\": \"Mapa\",\n    \"mark\": \"Vincular\",\n    \"memo\": \"Memo\",\n    \"memos\": \"Memos\",\n    \"more\": \"Mais\",\n    \"name\": \"Nome\",\n    \"new\": \"Novo\",\n    \"nickname\": \"Apelido\",\n    \"null\": \"Nulo\",\n    \"or\": \"ou\",\n    \"password\": \"Senha\",\n    \"pin\": \"Fixar\",\n    \"pinned\": \"Fixado\",\n    \"preview\": \"Pré-visualizar\",\n    \"profile\": \"Perfil\",\n    \"properties\": \"Propriedades\",\n    \"referenced-by\": \"Referenciado por\",\n    \"referencing\": \"Referenciando\",\n    \"relations\": \"Relações\",\n    \"remember-me\": \"Lembrar de mim\",\n    \"rename\": \"Renomear\",\n    \"reset\": \"Redefinir\",\n    \"resources\": \"Recursos\",\n    \"restore\": \"Restaurar\",\n    \"role\": \"Cargo\",\n    \"save\": \"Salvar\",\n    \"search\": \"Pesquisar\",\n    \"select\": \"Selecionar\",\n    \"settings\": \"Ajustes\",\n    \"share\": \"Compartilhar\",\n    \"shortcut-filter\": \"Filtro de atalhos\",\n    \"shortcuts\": \"Atalhos\",\n    \"sign-in\": \"Entrar\",\n    \"sign-in-with\": \"Entrar com {{provider}}\",\n    \"sign-out\": \"Sair\",\n    \"sign-up\": \"Registrar\",\n    \"statistics\": \"Estatísticas\",\n    \"tags\": \"Tags\",\n    \"title\": \"Título\",\n    \"today\": \"Hoje\",\n    \"tree-mode\": \"Modo árvore\",\n    \"type\": \"Tipo\",\n    \"unpin\": \"Desafixar\",\n    \"update\": \"Atualizar\",\n    \"upload\": \"Carregar\",\n    \"user\": \"Usuário\",\n    \"username\": \"Nome de usuário\",\n    \"version\": \"Versão\",\n    \"visibility\": \"Visibilidade\",\n    \"yourself\": \"Você mesmo\"\n  },\n  \"editor\": {\n    \"add-your-comment-here\": \"Adicione seu comentário aqui…\",\n    \"any-thoughts\": \"Alguma ideia…\",\n    \"exit-focus-mode\": \"Sair do Modo Foco\",\n    \"focus-mode\": \"Modo Foco\",\n    \"no-changes-detected\": \"Nenhuma alteração detectada\",\n    \"save\": \"Salvar\",\n    \"saving\": \"Salvando…\",\n    \"slash-commands\": \"Digite `/` para comandos\"\n  },\n  \"inbox\": {\n    \"failed-to-load\": \"Falha ao carregar item da caixa de entrada\",\n    \"memo-comment\": \"{{user}} tem um comentário sobre {{memo}}.\",\n    \"no-archived\": \"Nenhuma notificação arquivada\",\n    \"no-unread\": \"Nenhuma notificação não lida\",\n    \"unread\": \"Não lida\"\n  },\n  \"markdown\": {\n    \"checkbox\": \"Caixa de seleção\",\n    \"code-block\": \"Bloco de código\",\n    \"content-syntax\": \"Sintaxe do conteúdo\"\n  },\n  \"memo\": {\n    \"archived-at\": \"Arquivado em\",\n    \"click-to-hide-nsfw-content\": \"Ocultar conteúdo impróprio\",\n    \"click-to-show-nsfw-content\": \"Mostrar conteúdo impróprio\",\n    \"code\": \"Código\",\n    \"comment\": {\n      \"self\": \"Comentários\",\n      \"write-a-comment\": \"Escreva um comentário\"\n    },\n    \"copy-content\": \"Copiar conteúdo\",\n    \"copy-link\": \"Copiar link\",\n    \"count-memos-in-date\": \"{{count}} {{memos}} em {{date}}\",\n    \"delete-confirm\": \"Tem certeza de que deseja deletar este memo?\\n\\nESTA AÇÃO É IRREVERSÍVEL\",\n    \"delete-confirm-description\": \"Esta ação é irreversível. Anexos, links e referências também serão removidos.\",\n    \"direction\": \"Sentido\",\n    \"direction-asc\": \"Crescente\",\n    \"direction-desc\": \"Decrescente\",\n    \"display-time\": \"Horário\",\n    \"filters\": {\n      \"has-code\": \"temCódigo\",\n      \"has-link\": \"temLink\",\n      \"has-task-list\": \"temListaDeTarefas\"\n    },\n    \"links\": \"Links\",\n    \"load-more\": \"Carregar mais\",\n    \"no-archived-memos\": \"Nenhum memo arquivado.\",\n    \"no-memos\": \"Nenhum memo.\",\n    \"order-by\": \"Ordenar por\",\n    \"search-placeholder\": \"Pesquisar memos...\",\n    \"show-less\": \"Mostrar menos\",\n    \"show-more\": \"Mostrar mais\",\n    \"to-do\": \"Tarefas\",\n    \"view-detail\": \"Ver detalhes\",\n    \"visibility\": {\n      \"disabled\": \"Memos públicos estão desabilitados\",\n      \"private\": \"Privado\",\n      \"protected\": \"Protegido\",\n      \"public\": \"Público\"\n    }\n  },\n  \"message\": {\n    \"archived-successfully\": \"Arquivado com êxito\",\n    \"change-memo-created-time\": \"Alterar data de criação do memo\",\n    \"copied\": \"Copiado\",\n    \"deleted-successfully\": \"Êxito na exclusão\",\n    \"description-is-required\": \"A descrição é necessária\",\n    \"failed-to-embed-memo\": \"Falha ao incorporar o memo\",\n    \"fill-all\": \"Por favor, preencha todos os campos.\",\n    \"fill-all-required-fields\": \"Por favor, preencha todos os campos obrigatórios\",\n    \"maximum-upload-size-is\": \"O tamanho máximo permitido para upload é {{size}} MiB\",\n    \"memo-not-found\": \"Memo não encontrado.\",\n    \"new-password-not-match\": \"As novas senhas não coincidem.\",\n    \"no-data\": \"Nenhum dado encontrado.\",\n    \"password-changed\": \"Senha alterada\",\n    \"password-not-match\": \"As senhas não coincidem.\",\n    \"restored-successfully\": \"Restaurado com êxito\",\n    \"succeed-copy-content\": \"Conteúdo copiado com êxito.\",\n    \"succeed-copy-link\": \"Link copiado com êxito.\",\n    \"update-succeed\": \"Atualizado com êxito\",\n    \"user-not-found\": \"Usuário não encontrado\"\n  },\n  \"reference\": {\n    \"add-references\": \"Adicionar referências\",\n    \"embedded-usage\": \"Usar como Conteúdo Embutido\",\n    \"no-memos-found\": \"Nenhum memo encontrado\",\n    \"search-placeholder\": \"Pesquisar conteúdo\"\n  },\n  \"resource\": {\n    \"clear\": \"Limpar\",\n    \"copy-link\": \"Copiar link\",\n    \"create-dialog\": {\n      \"external-link\": {\n        \"file-name\": \"Nome do arquivo\",\n        \"file-name-placeholder\": \"Nome do arquivo\",\n        \"link\": \"Link\",\n        \"link-placeholder\": \"https://o.link.para/seu/recurso\",\n        \"option\": \"Link externo\",\n        \"type\": \"Tipo\",\n        \"type-placeholder\": \"Tipo do arquivo\"\n      },\n      \"local-file\": {\n        \"choose\": \"Escolher arquivo…\",\n        \"option\": \"Arquivo local\"\n      },\n      \"title\": \"Criar recurso\",\n      \"upload-method\": \"Método de carregamento\"\n    },\n    \"delete-all-unused\": \"Deletar todos os não utilizados\",\n    \"delete-all-unused-confirm\": \"Tem certeza de que deseja deletar todos os recursos não utilizados? ESTA AÇÃO É IRREVERSÍVEL\",\n    \"delete-all-unused-error\": \"Falha ao deletar recursos não utilizados\",\n    \"delete-all-unused-success\": \"Recursos deletados com êxito\",\n    \"delete-resource\": \"Deletar recurso\",\n    \"delete-selected-resources\": \"Deletar recursos selecionados\",\n    \"fetching-data\": \"Obtendo dados…\",\n    \"file-drag-drop-prompt\": \"Arraste e solte o arquivo aqui para carregá-lo\",\n    \"linked-amount\": \"Quantidade de vínculos\",\n    \"no-files-selected\": \"Nenhum arquivo selecionado!\",\n    \"no-resources\": \"Nenhum recurso.\",\n    \"no-unused-resources\": \"Nenhum recurso não utilizado\",\n    \"reset-link\": \"Redefinir link\",\n    \"reset-link-prompt\": \"Tem certeza de que deseja redefinir o link?\\nIsso quebrará todos os vínculos atuais.\\n\\nESTA AÇÃO É IRREVERSÍVEL\",\n    \"reset-resource-link\": \"Redefinir link do recurso\",\n    \"unused-resources\": \"Recursos não utilizados\"\n  },\n  \"router\": {\n    \"back-to-top\": \"Voltar ao Topo\",\n    \"go-to-home\": \"Ir ao início\"\n  },\n  \"setting\": {\n    \"member\": {\n      \"admin\": \"Admin\",\n      \"archive-member\": \"Arquivar membro\",\n      \"archive-success\": \"{{username}} arquivado com êxito\",\n      \"archive-warning\": \"Tem certeza de que deseja arquivar {{username}}?\",\n      \"archive-warning-description\": \"Arquivar desabilita a conta. Você pode restaurá-la ou deletá-la depois.\",\n      \"create-a-member\": \"Criar um membro\",\n      \"delete-member\": \"Deletar membro\",\n      \"delete-success\": \"{{username}} deletado com êxito\",\n      \"delete-warning\": \"Tem certeza de que deseja deletar {{username}}?\\n\\nESTA AÇÃO É IRREVERSÍVEL\",\n      \"delete-warning-description\": \"ESTA AÇÃO É IRREVERSÍVEL.\",\n      \"restore-success\": \"{{username}} restaurado com êxito\",\n      \"user\": \"Usuário\",\n      \"label\": \"Membro\",\n      \"list-title\": \"Lista de membros\"\n    },\n    \"my-account\": {\n      \"label\": \"Minha conta\"\n    },\n    \"preference\": {\n      \"default-memo-sort-option\": \"Exibição de tempo do memo\",\n      \"default-memo-visibility\": \"Visibilidade padrão do memo\",\n      \"theme\": \"Tema\",\n      \"label\": \"Preferências\"\n    },\n    \"shortcut\": {\n      \"delete-confirm\": \"Tem certeza de que deseja deletar o atalho `{{title}}`?\",\n      \"delete-success\": \"Atalho `{{title}}` deletado com êxito\"\n    },\n    \"sso\": {\n      \"authorization-endpoint\": \"Endpoint de autenticação\",\n      \"client-id\": \"ID do cliente\",\n      \"client-secret\": \"Segredo do cliente\",\n      \"confirm-delete\": \"Tem certeza de que deseja excluir a configuração SSO \\\"{{name}}\\\"?\\n\\nESTA AÇÃO É IRREVERSÍVEL\",\n      \"create-sso\": \"Criar SSO\",\n      \"custom\": \"Personalizado\",\n      \"delete-sso\": \"Confirmar exclusão\",\n      \"disabled-password-login-warning\": \"O login com senha está desabilitado. Tome cuidado ao remover provedores de identidade!\",\n      \"display-name\": \"Nome de exibição\",\n      \"identifier\": \"Identificador\",\n      \"identifier-filter\": \"Filtro identificador\",\n      \"no-sso-found\": \"Nenhum SSO encontrado.\",\n      \"redirect-url\": \"URL de redirecionamento\",\n      \"scopes\": \"Escopos\",\n      \"single-sign-on\": \"Configurando login único (SSO) para autenticação\",\n      \"sso-created\": \"SSO {{name}} criado\",\n      \"sso-list\": \"Lista de SSOs (Login Único)\",\n      \"sso-updated\": \"SSO {{name}} atualizado\",\n      \"template\": \"Modelo\",\n      \"token-endpoint\": \"Endpoint de token\",\n      \"update-sso\": \"Atualizar SSO\",\n      \"user-endpoint\": \"Endpoint do usuário\",\n      \"label\": \"SSO\"\n    },\n    \"storage\": {\n      \"accesskey\": \"Chave de acesso\",\n      \"accesskey-placeholder\": \"Chave de acesso / ID de acesso\",\n      \"bucket\": \"Bucket\",\n      \"bucket-placeholder\": \"Nome do bucket\",\n      \"create-a-service\": \"Criar um serviço\",\n      \"create-storage\": \"Criar armazenamento\",\n      \"current-storage\": \"Armazenamento de objetos atual\",\n      \"delete-storage\": \"Deletar armazenamento\",\n      \"endpoint\": \"Ponto de extremidade (endpoint)\",\n      \"filepath-template\": \"Formato do caminho de arquivo\",\n      \"local-storage-path\": \"Caminho do armazenamento local\",\n      \"path\": \"Caminho do armazenamento\",\n      \"path-description\": \"Você pode usar as mesmas variáveis dinâmicas do armazenamento local, como {filename}\",\n      \"path-placeholder\": \"caminho\",\n      \"presign-placeholder\": \"URL pré-assinado, opcional\",\n      \"region\": \"Região\",\n      \"region-placeholder\": \"Nome da região\",\n      \"s3-compatible-url\": \"URL compatível com S3\",\n      \"secretkey\": \"Chave secreta\",\n      \"secretkey-placeholder\": \"Chave secreta / Chave de acesso\",\n      \"storage-services\": \"Lista de serviços de armazenamento\",\n      \"type-database\": \"Banco de dados\",\n      \"type-local\": \"Arquivos locais\",\n      \"update-a-service\": \"Atualizar um serviço\",\n      \"update-local-path\": \"Atualizar caminho do armazenamento local\",\n      \"update-local-path-description\": \"O caminho de armazenamento local é relativo ao seu banco de dados\",\n      \"update-storage\": \"Atualizar armazenamento\",\n      \"url-prefix\": \"Prefixo da URL\",\n      \"url-prefix-placeholder\": \"Prefixo personalizado da URL, opcional\",\n      \"url-suffix\": \"Sufixo da URL\",\n      \"url-suffix-placeholder\": \"Sufixo personalizado da URL, opcional\",\n      \"warning-text\": \"Tem certeza de que deseja deletar o serviço de armazenamento \\\"{{name}}\\\"?\\n\\nESTA AÇÃO É IRREVERSÍVEL\",\n      \"label\": \"Armazenamento\"\n    },\n    \"system\": {\n      \"additional-script\": \"Script adicional\",\n      \"additional-script-placeholder\": \"Código JavaScript adicional\",\n      \"additional-style\": \"Estilo adicional\",\n      \"additional-style-placeholder\": \"Código CSS adicional\",\n      \"allow-user-signup\": \"Permitir registro de usuário\",\n      \"customize-server\": {\n        \"description\": \"Descrição\",\n        \"icon-url\": \"URL do ícone\",\n        \"locale\": \"Localização da instância\",\n        \"title\": \"Personalizar instância\"\n      },\n      \"disable-password-login\": \"Desabilitar login com senha\",\n      \"disable-password-login-final-warning\": \"Por favor, digite \\\"CONFIRM\\\" para prosseguir.\",\n      \"disable-password-login-warning\": \"Isso desabilitará o login com senha para todos os usuários. Caso seus provedores de identidade (SSO) configurados falhem, não será possível fazer login sem reverter esse ajuste manualmente no banco de dados. Você também deverá tomar precauções adicionais ao remover qualquer provedor de identidade!\",\n      \"display-with-updated-time\": \"Exibir hora de atualização nos memos\",\n      \"enable-auto-compact\": \"Ativar exibição compacta automaticamente\",\n      \"enable-double-click-to-edit\": \"Ativar clique duplo para editar\",\n      \"enable-password-login\": \"Habilitar login com senha\",\n      \"enable-password-login-warning\": \"Isso permitirá o login com senha para todos os usuários. Continue apenas se desejar que os usuários possam fazer login usando SSO e senha local.\",\n      \"max-upload-size\": \"Tamanho máximo de upload (MiB)\",\n      \"max-upload-size-hint\": \"O valor recomendado é 32 MiB.\",\n      \"removed-completed-task-list-items\": \"Remoção de itens concluídos da lista de tarefas\",\n      \"server-name\": \"Nome da instância\",\n      \"title\": \"Geral\",\n      \"label\": \"Sistema\"\n    },\n    \"version\": \"Versão\",\n    \"access-token\": {\n      \"access-token-copied-to-clipboard\": \"Token de acesso copiado para a área de transferência\",\n      \"access-token-deleted\": \"Token de acesso `{{description}}` deletado\",\n      \"access-token-deletion\": \"Tem certeza de que deseja excluir o token de acesso `{{description}}`? ESSA AÇÃO É IRREVERSÍVEL.\",\n      \"access-token-deletion-description\": \"Esta ação é irreversível. Você precisará atualizar quaisquer serviços que usam este token para usar um novo token.\",\n      \"create-dialog\": {\n        \"access-token-created\": \"Token de acesso `{{description}}` criado\",\n        \"create-access-token\": \"Criar token de acesso\",\n        \"created-at\": \"Criado em\",\n        \"description\": \"Descrição\",\n        \"duration-1m\": \"1 Mês\",\n        \"duration-8h\": \"8 Horas\",\n        \"duration-never\": \"Nunca\",\n        \"expiration\": \"Expiração\",\n        \"expires-at\": \"Expira em\",\n        \"some-description\": \"Alguma descrição…\"\n      },\n      \"description\": \"Uma lista de todos os tokens de acesso de sua conta.\",\n      \"title\": \"Tokens de acesso\",\n      \"token\": \"Token\"\n    },\n    \"account\": {\n      \"change-password\": \"Alterar senha\",\n      \"email-note\": \"Opcional\",\n      \"export-memos\": \"Exportar Memos\",\n      \"nickname-note\": \"Exibido no banner\",\n      \"openapi-reset\": \"Redefinir chave OpenAPI\",\n      \"openapi-sample-post\": \"Olá #memos em {{url}}\",\n      \"openapi-title\": \"OpenAPI\",\n      \"reset-api\": \"Redefinir API\",\n      \"title\": \"Informação da conta\",\n      \"update-information\": \"Atualizar informações\",\n      \"username-note\": \"Usado para entrar\"\n    },\n    \"instance\": {\n      \"disallow-change-nickname\": \"Impedir troca de apelido\",\n      \"disallow-change-username\": \"Impedir troca de nome de usuário\",\n      \"disallow-password-auth\": \"Impedir autenticação por senha\",\n      \"disallow-user-registration\": \"Impedir registro de usuário\",\n      \"monday\": \"Segunda-feira\",\n      \"saturday\": \"Sábado\",\n      \"sunday\": \"Domingo\",\n      \"week-start-day\": \"Dia de início da semana\"\n    },\n    \"memo\": {\n      \"content-length-limit\": \"Limite de tamanho do conteúdo (Bytes)\",\n      \"enable-blur-nsfw-content\": \"Desfocar conteúdo impróprio (adicione as tags abaixo)\",\n      \"enable-memo-comments\": \"Comentários nos memos\",\n      \"enable-memo-location\": \"Marcador de localização\",\n      \"reactions\": \"Reações\",\n      \"title\": \"Ajustes relacionados aos memos\",\n      \"label\": \"Memo\"\n    },\n    \"webhook\": {\n      \"create-dialog\": {\n        \"an-easy-to-remember-name\": \"Um nome fácil de lembrar\",\n        \"create-webhook\": \"Criar webhook\",\n        \"create-webhook-success\": \"Webhook `{{name}}` criado\",\n        \"edit-webhook\": \"Editar webhook\",\n        \"payload-url\": \"URL do payload\",\n        \"title\": \"Nome\",\n        \"url-example-post-receive\": \"https://exemplo.com.br/pós-recebimento\"\n      },\n      \"delete-dialog\": {\n        \"delete-webhook-description\": \"Esta ação é irreversível.\",\n        \"delete-webhook-success\": \"Webhook `{{name}}` deletado com êxito\",\n        \"delete-webhook-title\": \"Tem certeza de que deseja deletar o webhook `{{name}}`?\"\n      },\n      \"no-webhooks-found\": \"Nenhum webhook encontrado.\",\n      \"title\": \"Webhooks\",\n      \"url\": \"URL\"\n    }\n  },\n  \"tag\": {\n    \"all-tags\": \"Todas as Tags\",\n    \"create-tag\": \"Criar Tag\",\n    \"create-tags-guide\": \"Você pode criar tags inserindo `#tag`.\",\n    \"delete-confirm\": \"Tem certeza de que deseja excluir esta tag?\\nTodos os memos relacionados serão arquivados.\",\n    \"delete-success\": \"Tag deletada com êxito\",\n    \"delete-tag\": \"Excluir Tag\",\n    \"new-name\": \"Novo Nome\",\n    \"no-tag-found\": \"Nenhuma tag encontrada\",\n    \"old-name\": \"Nome Antigo\",\n    \"rename-error-empty\": \"O nome da tag não pode ser vazio ou conter espaços\",\n    \"rename-error-repeat\": \"O novo nome não pode ser o mesmo que o antigo\",\n    \"rename-success\": \"Tag renomeada com êxito\",\n    \"rename-tag\": \"Renomear tag\",\n    \"rename-tip\": \"Todos seus memos com essa tag serão atualizados.\"\n  },\n  \"tooltip\": {\n    \"link-memo\": \"Vincular Memo\",\n    \"markdown-menu\": \"Markdown\",\n    \"select-location\": \"Localização\",\n    \"select-visibility\": \"Visibilidade\",\n    \"tags\": \"Tags\",\n    \"upload-attachment\": \"Carregar Anexo(s)\"\n  }\n}\n"
  },
  {
    "path": "web/src/locales/pt-PT.json",
    "content": "{\n  \"about\": {\n    \"blogs\": \"Blogues\",\n    \"description\": \"Um serviço de notas leve e que prioriza a privacidade. Capture e partilhe facilmente as suas melhores ideias.\",\n    \"documents\": \"Documentos\",\n    \"github-repository\": \"Repositório GitHub\",\n    \"official-website\": \"Site Oficial\"\n  },\n  \"auth\": {\n    \"create-your-account\": \"Crie a sua conta\",\n    \"host-tip\": \"Está a registar-se como o anfitrião do site.\",\n    \"new-password\": \"Nova palavra-passe\",\n    \"repeat-new-password\": \"Repita a nova palavra-passe\",\n    \"sign-in-tip\": \"Já tem uma conta?\",\n    \"sign-up-tip\": \"Ainda não tem uma conta?\"\n  },\n  \"common\": {\n    \"about\": \"Sobre\",\n    \"add\": \"Adicionar\",\n    \"admin\": \"Administrador\",\n    \"all\": \"Todos\",\n    \"archive\": \"Arquivar\",\n    \"archived\": \"Arquivado\",\n    \"attachments\": \"Anexos\",\n    \"auto-expand\": \"Expandir automaticamente\",\n    \"avatar\": \"Avatar\",\n    \"basic\": \"Básico\",\n    \"beta\": \"Beta\",\n    \"calendar\": \"Calendário\",\n    \"cancel\": \"Cancelar\",\n    \"change\": \"Alterar\",\n    \"clear\": \"Limpar\",\n    \"close\": \"Fechar\",\n    \"collapse\": \"Recolher\",\n    \"confirm\": \"Confirmar\",\n    \"copy\": \"Copiar\",\n    \"create\": \"Criar\",\n    \"created-at\": \"Criado em\",\n    \"database\": \"Base de dados\",\n    \"day\": \"Dia\",\n    \"days\": {\n      \"fri\": \"Sex\",\n      \"mon\": \"Seg\",\n      \"sat\": \"Sáb\",\n      \"sun\": \"Dom\",\n      \"thu\": \"Qui\",\n      \"tue\": \"Ter\",\n      \"wed\": \"Qua\"\n    },\n    \"delete\": \"Eliminar\",\n    \"description\": \"Descrição\",\n    \"edit\": \"Editar\",\n    \"email\": \"Email\",\n    \"expand\": \"Expandir\",\n    \"explore\": \"Explorar\",\n    \"file\": \"Ficheiro\",\n    \"filter\": \"Filtrar\",\n    \"home\": \"Início\",\n    \"image\": \"Imagem\",\n    \"in\": \"Em\",\n    \"inbox\": \"Notificações\",\n    \"input\": \"Entrada\",\n    \"language\": \"Idioma\",\n    \"last-updated-at\": \"Última atualização em\",\n    \"learn-more\": \"Saiba mais\",\n    \"link\": \"Link\",\n    \"map\": \"Mapa\",\n    \"mark\": \"Marcar\",\n    \"memo\": \"Memo\",\n    \"memos\": \"Memos\",\n    \"more\": \"Mais\",\n    \"name\": \"Nome\",\n    \"new\": \"Novo\",\n    \"nickname\": \"Apelido\",\n    \"null\": \"Nulo\",\n    \"or\": \"ou\",\n    \"password\": \"Palavra-passe\",\n    \"pin\": \"Afixar\",\n    \"pinned\": \"Afixado\",\n    \"preview\": \"Pré-visualizar\",\n    \"profile\": \"Perfil\",\n    \"properties\": \"Propriedades\",\n    \"referenced-by\": \"Referenciado por\",\n    \"referencing\": \"A referenciar\",\n    \"relations\": \"Relações\",\n    \"remember-me\": \"Lembrar-me\",\n    \"rename\": \"Renomear\",\n    \"reset\": \"Reiniciar\",\n    \"resources\": \"Recursos\",\n    \"restore\": \"Restaurar\",\n    \"role\": \"Função\",\n    \"save\": \"Guardar\",\n    \"search\": \"Pesquisar\",\n    \"select\": \"Selecionar\",\n    \"settings\": \"Definições\",\n    \"share\": \"Partilhar\",\n    \"shortcut-filter\": \"Filtro de atalho\",\n    \"shortcuts\": \"Atalhos\",\n    \"sign-in\": \"Iniciar sessão\",\n    \"sign-in-with\": \"Iniciar sessão com {{provider}}\",\n    \"sign-out\": \"Terminar sessão\",\n    \"sign-up\": \"Registar-se\",\n    \"statistics\": \"Estatísticas\",\n    \"tags\": \"Etiquetas\",\n    \"title\": \"Título\",\n    \"today\": \"Hoje\",\n    \"tree-mode\": \"Modo árvore\",\n    \"type\": \"Tipo\",\n    \"unpin\": \"Desafixar\",\n    \"update\": \"Atualizar\",\n    \"upload\": \"Carregar\",\n    \"user\": \"Utilizador\",\n    \"username\": \"Nome de utilizador\",\n    \"version\": \"Versão\",\n    \"visibility\": \"Visibilidade\",\n    \"yourself\": \"Você próprio\"\n  },\n  \"editor\": {\n    \"add-your-comment-here\": \"Adicione o seu comentário aqui...\",\n    \"any-thoughts\": \"Alguma ideia…\",\n    \"exit-focus-mode\": \"Sair do Modo Foco\",\n    \"focus-mode\": \"Modo Foco\",\n    \"no-changes-detected\": \"Nenhuma alteração detetada\",\n    \"save\": \"Guardar\",\n    \"saving\": \"A guardar...\",\n    \"slash-commands\": \"Digite `/` para comandos\"\n  },\n  \"inbox\": {\n    \"failed-to-load\": \"Falha ao carregar item da caixa de entrada\",\n    \"memo-comment\": \"{{user}} deixou um comentário no seu {{memo}}.\",\n    \"no-archived\": \"Sem notificações arquivadas\",\n    \"no-unread\": \"Sem notificações não lidas\",\n    \"unread\": \"Não lido\"\n  },\n  \"markdown\": {\n    \"checkbox\": \"Caixa de seleção\",\n    \"code-block\": \"Bloco de código\",\n    \"content-syntax\": \"Sintaxe de conteúdo\"\n  },\n  \"memo\": {\n    \"archived-at\": \"Arquivado em\",\n    \"click-to-hide-nsfw-content\": \"Clique para ocultar conteúdo NSFW\",\n    \"click-to-show-nsfw-content\": \"Clique para mostrar conteúdo NSFW\",\n    \"code\": \"Código\",\n    \"comment\": {\n      \"self\": \"Comentários\",\n      \"write-a-comment\": \"Escreva um comentário\"\n    },\n    \"copy-content\": \"Copiar Conteúdo\",\n    \"copy-link\": \"Copiar link\",\n    \"count-memos-in-date\": \"{{count}} {{memos}} em {{date}}\",\n    \"delete-confirm\": \"Tem a certeza de que quer eliminar este memo?\",\n    \"delete-confirm-description\": \"Esta ação é irreversível. Os anexos, links e referências também serão removidos.\",\n    \"direction\": \"Direção\",\n    \"direction-asc\": \"Ascendente\",\n    \"direction-desc\": \"Descendente\",\n    \"display-time\": \"Hora de exibição\",\n    \"filters\": {\n      \"has-code\": \"temCódigo\",\n      \"has-link\": \"temLink\",\n      \"has-task-list\": \"temListaDeTarefas\"\n    },\n    \"links\": \"Links\",\n    \"load-more\": \"Carregar mais\",\n    \"no-archived-memos\": \"Não existem memos arquivados.\",\n    \"no-memos\": \"Não existem memos.\",\n    \"order-by\": \"Ordenar por\",\n    \"search-placeholder\": \"Pesquisar memos...\",\n    \"show-less\": \"Mostrar menos\",\n    \"show-more\": \"Mostrar mais\",\n    \"to-do\": \"Tarefas\",\n    \"view-detail\": \"Ver detalhes\",\n    \"visibility\": {\n      \"disabled\": \"Memos públicos estão desativados\",\n      \"private\": \"Privado\",\n      \"protected\": \"Protegido\",\n      \"public\": \"Público\"\n    }\n  },\n  \"message\": {\n    \"archived-successfully\": \"Arquivado com sucesso\",\n    \"change-memo-created-time\": \"Alterar a data de criação do memo\",\n    \"copied\": \"Copiado\",\n    \"deleted-successfully\": \"Eliminado com sucesso\",\n    \"description-is-required\": \"A descrição é obrigatória\",\n    \"failed-to-embed-memo\": \"Falha ao incorporar memo\",\n    \"fill-all\": \"Por favor, preencha todos os campos.\",\n    \"fill-all-required-fields\": \"Por favor, preencha todos os campos obrigatórios\",\n    \"maximum-upload-size-is\": \"O tamanho máximo permitido para carregamento é {{size}} MiB\",\n    \"memo-not-found\": \"Memo não encontrado.\",\n    \"new-password-not-match\": \"As novas palavras-passe não coincidem.\",\n    \"no-data\": \"Nenhum dado encontrado.\",\n    \"password-changed\": \"Palavra-passe alterada\",\n    \"password-not-match\": \"As palavras-passe não coincidem.\",\n    \"restored-successfully\": \"Restaurado com sucesso\",\n    \"succeed-copy-content\": \"Conteúdo copiado com sucesso.\",\n    \"succeed-copy-link\": \"Link copiado com sucesso.\",\n    \"update-succeed\": \"Atualização bem-sucedida\",\n    \"user-not-found\": \"Utilizador não encontrado\"\n  },\n  \"reference\": {\n    \"add-references\": \"Adicionar referências\",\n    \"embedded-usage\": \"Usar como conteúdo incorporado\",\n    \"no-memos-found\": \"Não foram encontrados memos\",\n    \"search-placeholder\": \"Pesquisar conteúdo\"\n  },\n  \"resource\": {\n    \"clear\": \"Limpar\",\n    \"copy-link\": \"Copiar link\",\n    \"create-dialog\": {\n      \"external-link\": {\n        \"file-name\": \"Nome do ficheiro\",\n        \"file-name-placeholder\": \"Nome do ficheiro\",\n        \"link\": \"Link\",\n        \"link-placeholder\": \"https://o.link.para/o/seu/recurso\",\n        \"option\": \"Link externo\",\n        \"type\": \"Tipo\",\n        \"type-placeholder\": \"Tipo de ficheiro\"\n      },\n      \"local-file\": {\n        \"choose\": \"Escolha um ficheiro…\",\n        \"option\": \"Ficheiro local\"\n      },\n      \"title\": \"Criar Recurso\",\n      \"upload-method\": \"Método de carregamento\"\n    },\n    \"delete-all-unused\": \"Eliminar todos os não utilizados\",\n    \"delete-all-unused-confirm\": \"Tem a certeza de que deseja eliminar todos os recursos não utilizados? ESTA AÇÃO É IRREVERSÍVEL\",\n    \"delete-all-unused-error\": \"Falha ao eliminar recursos não utilizados\",\n    \"delete-all-unused-success\": \"Recursos eliminados com sucesso\",\n    \"delete-resource\": \"Eliminar Recurso\",\n    \"delete-selected-resources\": \"Eliminar Recursos Selecionados\",\n    \"fetching-data\": \"A obter dados…\",\n    \"file-drag-drop-prompt\": \"Arraste e largue o seu ficheiro aqui para carregar\",\n    \"linked-amount\": \"Quantidade associada\",\n    \"no-files-selected\": \"Nenhum ficheiro selecionado\",\n    \"no-resources\": \"Sem recursos.\",\n    \"no-unused-resources\": \"Sem recursos não utilizados\",\n    \"reset-link\": \"Reiniciar Link\",\n    \"reset-link-prompt\": \"Tem a certeza de que deseja reiniciar o link? Isto irá quebrar todos os usos atuais. ESTA AÇÃO É IRREVERSÍVEL\",\n    \"reset-resource-link\": \"Reiniciar Link do Recurso\",\n    \"unused-resources\": \"Recursos não utilizados\"\n  },\n  \"router\": {\n    \"back-to-top\": \"Voltar ao Topo\",\n    \"go-to-home\": \"Ir para a Página Inicial\"\n  },\n  \"setting\": {\n    \"member\": {\n      \"admin\": \"Administrador\",\n      \"archive-member\": \"Arquivar membro\",\n      \"archive-success\": \"{{username}} arquivado com sucesso\",\n      \"archive-warning\": \"Tem a certeza de que deseja arquivar {{username}}?\",\n      \"archive-warning-description\": \"Arquivar desativa a conta. Pode restaurá-la ou eliminá-la mais tarde.\",\n      \"create-a-member\": \"Criar um membro\",\n      \"delete-member\": \"Eliminar Membro\",\n      \"delete-success\": \"{{username}} eliminado com sucesso\",\n      \"delete-warning\": \"Tem a certeza de que deseja eliminar {{username}}?\",\n      \"delete-warning-description\": \"ESTA AÇÃO É IRREVERSÍVEL\",\n      \"restore-success\": \"{{username}} restaurado com sucesso\",\n      \"user\": \"Utilizador\",\n      \"label\": \"Membro\",\n      \"list-title\": \"Lista de membros\"\n    },\n    \"my-account\": {\n      \"label\": \"A Minha Conta\"\n    },\n    \"preference\": {\n      \"default-memo-sort-option\": \"Ordenação padrão dos memos\",\n      \"default-memo-visibility\": \"Visibilidade padrão dos memos\",\n      \"theme\": \"Tema\",\n      \"label\": \"Preferências\"\n    },\n    \"shortcut\": {\n      \"delete-confirm\": \"Tem a certeza de que deseja eliminar o atalho `{{title}}`?\",\n      \"delete-success\": \"Atalho `{{title}}` eliminado com sucesso\"\n    },\n    \"sso\": {\n      \"authorization-endpoint\": \"Ponto de Autorização\",\n      \"client-id\": \"ID do Cliente\",\n      \"client-secret\": \"Segredo do Cliente\",\n      \"confirm-delete\": \"Tem a certeza de que deseja eliminar a configuração \\\"{{name}}\\\" do SSO? ESTA AÇÃO É IRREVERSÍVEL\",\n      \"create-sso\": \"Criar SSO\",\n      \"custom\": \"Personalizado\",\n      \"delete-sso\": \"Confirmar eliminação\",\n      \"disabled-password-login-warning\": \"O login por palavra-passe está desativado, tenha cuidado ao remover provedores de identidade\",\n      \"display-name\": \"Nome de Exibição\",\n      \"identifier\": \"Identificador\",\n      \"identifier-filter\": \"Filtro de Identificador\",\n      \"no-sso-found\": \"Nenhum SSO encontrado.\",\n      \"redirect-url\": \"URL de Redirecionamento\",\n      \"scopes\": \"Âmbitos\",\n      \"single-sign-on\": \"A configurar o Single Sign-On (SSO) para autenticação.\",\n      \"sso-created\": \"SSO {{name}} criado\",\n      \"sso-list\": \"Lista de SSO\",\n      \"sso-updated\": \"SSO {{name}} atualizado\",\n      \"template\": \"Modelo\",\n      \"token-endpoint\": \"Ponto de Token\",\n      \"update-sso\": \"Atualizar SSO\",\n      \"user-endpoint\": \"Ponto do Utilizador\",\n      \"label\": \"SSO\"\n    },\n    \"storage\": {\n      \"accesskey\": \"Chave de Acesso\",\n      \"accesskey-placeholder\": \"Chave de Acesso / ID de Acesso\",\n      \"bucket\": \"Bucket\",\n      \"bucket-placeholder\": \"Nome do Bucket\",\n      \"create-a-service\": \"Criar um serviço\",\n      \"create-storage\": \"Criar Armazenamento\",\n      \"current-storage\": \"Armazenamento de objetos atual\",\n      \"delete-storage\": \"Eliminar Armazenamento\",\n      \"endpoint\": \"Ponto Final\",\n      \"filepath-template\": \"Modelo de caminho de ficheiro\",\n      \"local-storage-path\": \"Caminho do Armazenamento Local\",\n      \"path\": \"Caminho do Armazenamento\",\n      \"path-description\": \"Pode usar as mesmas variáveis dinâmicas do armazenamento local, como {filename}\",\n      \"path-placeholder\": \"caminho/personalizado\",\n      \"presign-placeholder\": \"URL de pré-assinatura, opcional\",\n      \"region\": \"Região\",\n      \"region-placeholder\": \"Nome da Região\",\n      \"s3-compatible-url\": \"URL compatível com S3\",\n      \"secretkey\": \"Chave Secreta\",\n      \"secretkey-placeholder\": \"Chave Secreta / Chave de Acesso\",\n      \"storage-services\": \"Serviços de Armazenamento\",\n      \"type-database\": \"Base de Dados\",\n      \"type-local\": \"Sistema de ficheiros local\",\n      \"update-a-service\": \"Atualizar um serviço\",\n      \"update-local-path\": \"Atualizar Caminho do Armazenamento Local\",\n      \"update-local-path-description\": \"O caminho do armazenamento local é relativo ao seu ficheiro de base de dados\",\n      \"update-storage\": \"Atualizar Armazenamento\",\n      \"url-prefix\": \"Prefixo de URL\",\n      \"url-prefix-placeholder\": \"Prefixo de URL personalizado, opcional\",\n      \"url-suffix\": \"Sufixo de URL\",\n      \"url-suffix-placeholder\": \"Sufixo de URL personalizado, opcional\",\n      \"warning-text\": \"Tem a certeza de que deseja eliminar o serviço de armazenamento \\\"{{name}}\\\"? ESTA AÇÃO É IRREVERSÍVEL\",\n      \"label\": \"Armazenamento\"\n    },\n    \"system\": {\n      \"additional-script\": \"Script adicional\",\n      \"additional-script-placeholder\": \"Código JavaScript adicional\",\n      \"additional-style\": \"Estilo adicional\",\n      \"additional-style-placeholder\": \"Código CSS adicional\",\n      \"allow-user-signup\": \"Permitir registo de utilizadores\",\n      \"customize-server\": {\n        \"description\": \"Descrição\",\n        \"icon-url\": \"URL do Ícone\",\n        \"locale\": \"Localização do Servidor\",\n        \"title\": \"Personalizar Servidor\"\n      },\n      \"disable-password-login\": \"Desativar login por palavra-passe\",\n      \"disable-password-login-final-warning\": \"Escreva \\\"CONFIRM\\\" se tiver a certeza.\",\n      \"disable-password-login-warning\": \"Isto desativará o login por palavra-passe para todos os utilizadores. Não será possível iniciar sessão sem reverter esta definição na base de dados se os seus provedores de identidade falharem. Deve ter cuidado ao remover um provedor de identidade.\",\n      \"display-with-updated-time\": \"Exibir com hora atualizada\",\n      \"enable-auto-compact\": \"Ativar compactação automática\",\n      \"enable-double-click-to-edit\": \"Ativar duplo clique para editar\",\n      \"enable-password-login\": \"Ativar login por palavra-passe\",\n      \"enable-password-login-warning\": \"Isto permitirá login por palavra-passe para todos os utilizadores. Continue apenas se quiser permitir login por SSO e palavra-passe.\",\n      \"max-upload-size\": \"Tamanho máximo de upload (MiB)\",\n      \"max-upload-size-hint\": \"O valor recomendado é 32 MiB.\",\n      \"removed-completed-task-list-items\": \"Ativar remoção de tarefas concluídas\",\n      \"server-name\": \"Nome do Servidor\",\n      \"title\": \"Geral\",\n      \"label\": \"Sistema\"\n    },\n    \"version\": \"Versão\",\n    \"access-token\": {\n      \"access-token-copied-to-clipboard\": \"Token de acesso copiado para a área de transferência\",\n      \"access-token-deleted\": \"Token de acesso `{{description}}` eliminado\",\n      \"access-token-deletion\": \"Tem a certeza de que deseja eliminar o token de acesso `{{description}}`?\",\n      \"access-token-deletion-description\": \"Esta ação é irreversível. Terá de atualizar quaisquer serviços que utilizem este token para usar um novo token.\",\n      \"create-dialog\": {\n        \"access-token-created\": \"Token de acesso `{{description}}` criado\",\n        \"create-access-token\": \"Criar Token de Acesso\",\n        \"created-at\": \"Criado em\",\n        \"description\": \"Descrição\",\n        \"duration-1m\": \"1 Mês\",\n        \"duration-8h\": \"8 Horas\",\n        \"duration-never\": \"Nunca\",\n        \"expiration\": \"Expiração\",\n        \"expires-at\": \"Expira em\",\n        \"some-description\": \"Alguma descrição...\"\n      },\n      \"description\": \"Uma lista de todos os tokens de acesso para a sua conta.\",\n      \"title\": \"Tokens de Acesso\",\n      \"token\": \"Token\"\n    },\n    \"account\": {\n      \"change-password\": \"Alterar palavra-passe\",\n      \"email-note\": \"Opcional\",\n      \"export-memos\": \"Exportar Memos\",\n      \"nickname-note\": \"Exibido no banner\",\n      \"openapi-reset\": \"Reiniciar Chave OpenAPI\",\n      \"openapi-sample-post\": \"Olá #memos de {{url}}\",\n      \"openapi-title\": \"OpenAPI\",\n      \"reset-api\": \"Reiniciar API\",\n      \"title\": \"Informações da Conta\",\n      \"update-information\": \"Atualizar Informações\",\n      \"username-note\": \"Usado para iniciar sessão\"\n    },\n    \"instance\": {\n      \"disallow-change-nickname\": \"Desativar alteração de apelido\",\n      \"disallow-change-username\": \"Desativar alteração de nome de utilizador\",\n      \"disallow-password-auth\": \"Desativar autenticação por palavra-passe\",\n      \"disallow-user-registration\": \"Desativar registo de utilizadores\",\n      \"monday\": \"Segunda-feira\",\n      \"saturday\": \"Sábado\",\n      \"sunday\": \"Domingo\",\n      \"week-start-day\": \"Dia de início da semana\"\n    },\n    \"memo\": {\n      \"content-length-limit\": \"Limite de comprimento do conteúdo (Bytes)\",\n      \"enable-blur-nsfw-content\": \"Ativar desfoque de conteúdo sensível (NSFW)\",\n      \"enable-memo-comments\": \"Ativar comentários em memos\",\n      \"enable-memo-location\": \"Ativar localização em memos\",\n      \"reactions\": \"Reações\",\n      \"title\": \"Definições relacionadas com memos\",\n      \"label\": \"Memo\"\n    },\n    \"webhook\": {\n      \"create-dialog\": {\n        \"an-easy-to-remember-name\": \"Um nome fácil de lembrar\",\n        \"create-webhook\": \"Criar webhook\",\n        \"create-webhook-success\": \"Webhook `{{name}}` criado\",\n        \"edit-webhook\": \"Editar webhook\",\n        \"payload-url\": \"URL do payload\",\n        \"title\": \"Título\",\n        \"url-example-post-receive\": \"https://exemplo.com/postreceive\"\n      },\n      \"delete-dialog\": {\n        \"delete-webhook-description\": \"Esta ação é irreversível.\",\n        \"delete-webhook-success\": \"Webhook `{{name}}` eliminado com sucesso\",\n        \"delete-webhook-title\": \"Tem a certeza de que deseja eliminar o webhook `{{name}}`?\"\n      },\n      \"no-webhooks-found\": \"Nenhum webhook encontrado.\",\n      \"title\": \"Webhooks\",\n      \"url\": \"URL\"\n    }\n  },\n  \"tag\": {\n    \"all-tags\": \"Todas as Etiquetas\",\n    \"create-tag\": \"Criar Etiqueta\",\n    \"create-tags-guide\": \"Pode criar etiquetas digitando `#etiqueta`.\",\n    \"delete-confirm\": \"Tem a certeza que quer eliminar esta etiqueta? Todos os memos relacionados serão arquivados.\",\n    \"delete-success\": \"Etiqueta eliminada com sucesso\",\n    \"delete-tag\": \"Eliminar Etiqueta\",\n    \"new-name\": \"Novo Nome\",\n    \"no-tag-found\": \"Nenhuma etiqueta encontrada\",\n    \"old-name\": \"Nome Antigo\",\n    \"rename-error-empty\": \"O nome da etiqueta não pode estar vazio ou conter espaços\",\n    \"rename-error-repeat\": \"O novo nome não pode ser igual ao antigo\",\n    \"rename-success\": \"Etiqueta renomeada com sucesso\",\n    \"rename-tag\": \"Renomear etiqueta\",\n    \"rename-tip\": \"Todos os seus memos com esta etiqueta serão atualizados.\"\n  },\n  \"tooltip\": {\n    \"link-memo\": \"Associar Memo\",\n    \"markdown-menu\": \"Markdown\",\n    \"select-location\": \"Localização\",\n    \"select-visibility\": \"Visibilidade\",\n    \"tags\": \"Etiquetas\",\n    \"upload-attachment\": \"Carregar Anexo(s)\"\n  }\n}\n"
  },
  {
    "path": "web/src/locales/ru.json",
    "content": "{\n  \"about\": {\n    \"blogs\": \"Блоги\",\n    \"description\": \"Приватный, легкий сервис для заметок. Легко сохраняйте и делитесь вашими мыслями.\",\n    \"documents\": \"Документы\",\n    \"github-repository\": \"Репозиторий GitHub\",\n    \"official-website\": \"Официальный сайт\"\n  },\n  \"auth\": {\n    \"create-your-account\": \"Создание учетной записи\",\n    \"host-tip\": \"Вы регистрируете учетную запись владельца сайта\",\n    \"new-password\": \"Новый пароль\",\n    \"repeat-new-password\": \"Повторить новый пароль\",\n    \"sign-in-tip\": \"У вас уже есть учетная запись?\",\n    \"sign-up-tip\": \"У вас еще нет учетной записи?\"\n  },\n  \"common\": {\n    \"about\": \"О Memos\",\n    \"add\": \"Добавить\",\n    \"admin\": \"Администратор\",\n    \"all\": \"Все\",\n    \"archive\": \"Архивировать\",\n    \"archived\": \"Архив\",\n    \"attachments\": \"Файлы\",\n    \"auto-expand\": \"Авто-раскрытие\",\n    \"avatar\": \"Аватар\",\n    \"basic\": \"Базовые\",\n    \"beta\": \"Бета\",\n    \"calendar\": \"Календарь\",\n    \"cancel\": \"Отменить\",\n    \"change\": \"Заменить\",\n    \"clear\": \"Очистить\",\n    \"close\": \"Закрыть\",\n    \"collapse\": \"Свернуть\",\n    \"confirm\": \"Подтвердить\",\n    \"copy\": \"Копировать\",\n    \"create\": \"Создать\",\n    \"created-at\": \"Создано\",\n    \"database\": \"База данных\",\n    \"day\": \"День\",\n    \"days\": {\n      \"fri\": \"Пт.\",\n      \"mon\": \"Пон.\",\n      \"sat\": \"Сб.\",\n      \"sun\": \"Вс.\",\n      \"thu\": \"Чт.\",\n      \"tue\": \"Вт.\",\n      \"wed\": \"Ср.\"\n    },\n    \"delete\": \"Удалить\",\n    \"description\": \"Описание\",\n    \"edit\": \"Редактировать\",\n    \"email\": \"Эл. почта\",\n    \"expand\": \"Раскрыть\",\n    \"explore\": \"Обзор\",\n    \"file\": \"Файл\",\n    \"filter\": \"Фильтр\",\n    \"home\": \"Главная\",\n    \"image\": \"Изображение\",\n    \"in\": \"В\",\n    \"inbox\": \"Уведомления\",\n    \"input\": \"Напишите...\",\n    \"language\": \"Язык\",\n    \"last-updated-at\": \"Изменено\",\n    \"learn-more\": \"Узнать больше\",\n    \"link\": \"Ссылка\",\n    \"map\": \"Карта\",\n    \"mark\": \"Связать\",\n    \"memo\": \"Заметка\",\n    \"memos\": \"Заметки\",\n    \"more\": \"Еще\",\n    \"name\": \"Название\",\n    \"new\": \"Новая запись\",\n    \"nickname\": \"Псевдоним\",\n    \"null\": \"Пусто\",\n    \"or\": \"или\",\n    \"password\": \"Пароль\",\n    \"pin\": \"Закрепить\",\n    \"pinned\": \"Закреплено\",\n    \"preview\": \"Предпросмотр\",\n    \"profile\": \"Профиль\",\n    \"properties\": \"Свойства\",\n    \"referenced-by\": \"Упоминания\",\n    \"referencing\": \"Связи\",\n    \"relations\": \"Карта связей\",\n    \"remember-me\": \"Запомнить меня\",\n    \"rename\": \"Переименовать\",\n    \"reset\": \"Сбросить\",\n    \"resources\": \"Ресурсы\",\n    \"restore\": \"Восстановить\",\n    \"role\": \"Роль\",\n    \"save\": \"Сохранить\",\n    \"search\": \"Поиск\",\n    \"select\": \"Выбрать\",\n    \"settings\": \"Настройки\",\n    \"share\": \"Поделиться\",\n    \"shortcut-filter\": \"Условие отбора\",\n    \"shortcuts\": \"Ярлыки\",\n    \"sign-in\": \"Войти\",\n    \"sign-in-with\": \"Войти через {{provider}}\",\n    \"sign-out\": \"Выйти\",\n    \"sign-up\": \"Зарегистрироваться\",\n    \"statistics\": \"Статистика\",\n    \"tags\": \"Теги\",\n    \"title\": \"Заголовок\",\n    \"today\": \"Сегодня\",\n    \"tree-mode\": \"Иерархический вид\",\n    \"type\": \"Тип\",\n    \"unpin\": \"Открепить\",\n    \"update\": \"Изменить\",\n    \"upload\": \"Загрузить\",\n    \"user\": \"Пользователь\",\n    \"username\": \"Имя пользователя\",\n    \"version\": \"Версия\",\n    \"visibility\": \"Видимость\",\n    \"yourself\": \"Вы\"\n  },\n  \"editor\": {\n    \"add-your-comment-here\": \"Напишите свой комментарий...\",\n    \"any-thoughts\": \"Напишите что-нибудь...\",\n    \"exit-focus-mode\": \"Выйти из режима фокуса\",\n    \"focus-mode\": \"Режим фокуса\",\n    \"no-changes-detected\": \"Изменений не обнаружено\",\n    \"save\": \"Сохранить\",\n    \"saving\": \"Сохранение...\",\n    \"slash-commands\": \"Введите `/` для команд\"\n  },\n  \"inbox\": {\n    \"failed-to-load\": \"Не удалось загрузить элемент уведомления\",\n    \"memo-comment\": \"{{user}} добавил комментарий к вашей заметке {{memo}}.\",\n    \"no-archived\": \"Нет архивных уведомлений\",\n    \"no-unread\": \"Нет непрочитанных уведомлений\",\n    \"unread\": \"Непрочитанные\"\n  },\n  \"markdown\": {\n    \"checkbox\": \"Список задач\",\n    \"code-block\": \"Блок кода\",\n    \"content-syntax\": \"Синтаксис форматирования\"\n  },\n  \"memo\": {\n    \"archived-at\": \"В архиве\",\n    \"click-to-hide-nsfw-content\": \"Нажмите, чтобы скрыть контент 18+\",\n    \"click-to-show-nsfw-content\": \"Нажмите, чтобы посмотреть\",\n    \"code\": \"Код\",\n    \"comment\": {\n      \"self\": \"Комментарии\",\n      \"write-a-comment\": \"Добавить комментарий\"\n    },\n    \"copy-content\": \"Копировать контент\",\n    \"copy-link\": \"Скопировать ссылку\",\n    \"count-memos-in-date\": \"{{count}} {{memos}} за {{date}}\",\n    \"delete-confirm\": \"Вы уверены, что хотите удалить эту запись?\\nЭТО ДЕЙСТВИЕ НЕЛЬЗЯ ОТМЕНИТЬ!\",\n    \"delete-confirm-description\": \"Это действие нельзя отменить. Вложения, ссылки и упоминания также будут удалены.\",\n    \"direction\": \"Сортировка\",\n    \"direction-asc\": \"По возрастанию\",\n    \"direction-desc\": \"По убыванию\",\n    \"display-time\": \"Время отображения\",\n    \"filters\": {\n      \"has-code\": \"hasCode\",\n      \"has-link\": \"hasLink\",\n      \"has-task-list\": \"hasTaskList\"\n    },\n    \"links\": \"Ссылки\",\n    \"load-more\": \"Загрузить еще\",\n    \"no-archived-memos\": \"Нет заархивированных записей.\",\n    \"no-memos\": \"Нет заметок.\",\n    \"order-by\": \"Сортировать по\",\n    \"search-placeholder\": \"Поиск записей\",\n    \"show-less\": \"Показать меньше\",\n    \"show-more\": \"Подробнее\",\n    \"to-do\": \"Задачи\",\n    \"view-detail\": \"Подробно\",\n    \"visibility\": {\n      \"disabled\": \"Публичные записи отключены\",\n      \"private\": \"Видно только вам\",\n      \"protected\": \"Видно только авторизованным пользователям\",\n      \"public\": \"Публичная\"\n    }\n  },\n  \"message\": {\n    \"archived-successfully\": \"Перемещено в архив\",\n    \"change-memo-created-time\": \"Изменить время создания записи\",\n    \"copied\": \"Скопировано\",\n    \"deleted-successfully\": \"Успешное удаление\",\n    \"description-is-required\": \"Необходимо заполнить \\\"Описание\\\"\",\n    \"failed-to-embed-memo\": \"Не удалось встроить заметку\",\n    \"fill-all\": \"Пожалуйста, заполните все поля\",\n    \"fill-all-required-fields\": \"Пожалуйста, заполните все необходимые поля\",\n    \"maximum-upload-size-is\": \"Максимальный размер для загрузки {{size}} МБ\",\n    \"memo-not-found\": \"Запись не найдена.\",\n    \"new-password-not-match\": \"Новый пароль не совпадает.\",\n    \"no-data\": \"Здесь ничего нет\",\n    \"password-changed\": \"Пароль изменён\",\n    \"password-not-match\": \"Пароли не совпадают.\",\n    \"restored-successfully\": \"Успешно восстановлено.\",\n    \"succeed-copy-content\": \"Контент успешно скопирован.\",\n    \"succeed-copy-link\": \"Ссылка скопирована в буфер обмена.\",\n    \"update-succeed\": \"Успешно обновлено\",\n    \"user-not-found\": \"Пользователь не найден\"\n  },\n  \"reference\": {\n    \"add-references\": \"Добавить связь\",\n    \"embedded-usage\": \"Вставить контент\",\n    \"no-memos-found\": \"Ничего не найдено\",\n    \"search-placeholder\": \"Поиск по содержимому\"\n  },\n  \"resource\": {\n    \"clear\": \"Удалить неиспользуемые\",\n    \"copy-link\": \"Скопировать ссылку\",\n    \"create-dialog\": {\n      \"external-link\": {\n        \"file-name\": \"Название файла\",\n        \"file-name-placeholder\": \"Название файла\",\n        \"link\": \"Ссылка\",\n        \"link-placeholder\": \"https://ссылка.на.ваш.ресурс\",\n        \"option\": \"Внешняя ссылка\",\n        \"type\": \"Тип\",\n        \"type-placeholder\": \"Тип файла\"\n      },\n      \"local-file\": {\n        \"choose\": \"Выберите файл…\",\n        \"option\": \"Локальный файл\"\n      },\n      \"title\": \"Создать ресурс\",\n      \"upload-method\": \"Способ загрузки\"\n    },\n    \"delete-all-unused\": \"Удалить все неиспользуемые\",\n    \"delete-all-unused-confirm\": \"Вы уверены, что хотите удалить все неиспользуемые ресурсы? ЭТО ДЕЙСТВИЕ НЕЛЬЗЯ ОТМЕНИТЬ\",\n    \"delete-all-unused-error\": \"Не удалось удалить неиспользуемые ресурсы\",\n    \"delete-all-unused-success\": \"Ресурсы успешно удалены\",\n    \"delete-resource\": \"Удалить ресурс\",\n    \"delete-selected-resources\": \"Удаление выбранных ресурсов\",\n    \"fetching-data\": \"загрузка данных...\",\n    \"file-drag-drop-prompt\": \"Перетащите ваш файл сюда, чтобы загрузить его\",\n    \"linked-amount\": \"Количество записей\",\n    \"no-files-selected\": \"Файлы не выбраны\",\n    \"no-resources\": \"Нет ресурсов.\",\n    \"no-unused-resources\": \"Нет неиспользуемых ресурсов\",\n    \"reset-link\": \"Удалить ссылку\",\n    \"reset-link-prompt\": \"Вы уверены, что хотите удалить ссылку? Все использования этой ссылки будут сломаны.\\nЭТО ДЕЙСТВИЕ НЕЛЬЗЯ ОТМЕНИТЬ\",\n    \"reset-resource-link\": \"Удаление ссылки на ресурс\",\n    \"unused-resources\": \"Неиспользуемые\"\n  },\n  \"router\": {\n    \"back-to-top\": \"В начало\",\n    \"go-to-home\": \"Домой\"\n  },\n  \"setting\": {\n    \"member\": {\n      \"admin\": \"Администратор\",\n      \"archive-member\": \"Деактивировать\",\n      \"archive-success\": \"Пользователь {{username}} успешно деактивирован\",\n      \"archive-warning\": \"Вы уверены, что хотите деактивировать пользователя {{username}}?\",\n      \"archive-warning-description\": \"Деактивация отключает учетную запись. Вы можете восстановить или удалить её позже.\",\n      \"create-a-member\": \"Создать\",\n      \"delete-member\": \"Удалить\",\n      \"delete-success\": \"Пользователь {{username}} успешно удален\",\n      \"delete-warning\": \"Вы уверены, что хотите удалить пользователя {{username}}?\\nЭТО ДЕЙСТВИЕ НЕЛЬЗЯ ОТМЕНИТЬ!\",\n      \"delete-warning-description\": \"ЭТО ДЕЙСТВИЕ НЕЛЬЗЯ ОТМЕНИТЬ\",\n      \"restore-success\": \"Пользователь {{username}} успешно восстановлен\",\n      \"user\": \"Пользователь\",\n      \"label\": \"Пользователи\",\n      \"list-title\": \"Список пользователей\"\n    },\n    \"my-account\": {\n      \"label\": \"Мой аккаунт\"\n    },\n    \"preference\": {\n      \"default-memo-sort-option\": \"Отображаемое время записи\",\n      \"default-memo-visibility\": \"Видимость записей по умолчанию\",\n      \"theme\": \"Тема\",\n      \"label\": \"Настройки\"\n    },\n    \"shortcut\": {\n      \"delete-confirm\": \"Вы уверены, что хотите удалить ярлык `{{title}}`?\",\n      \"delete-success\": \"Ярлык `{{title}}` успешно удален\"\n    },\n    \"sso\": {\n      \"authorization-endpoint\": \"Конечная точка авторизации\",\n      \"client-id\": \"ID клиента\",\n      \"client-secret\": \"Секретный ключ клиента\",\n      \"confirm-delete\": \"Вы уверены, что хотите удалить SSO-провайдер \\\"{{name}}\\\"?\\nЭТО ДЕЙСТВИЕ НЕЛЬЗЯ ОТМЕНИТЬ!\",\n      \"create-sso\": \"Подключение SSO-провайдера\",\n      \"custom\": \"Собственный\",\n      \"delete-sso\": \"Подтвердите удаление\",\n      \"disabled-password-login-warning\": \"Вход по паролю отключен, будьте особенно осторожны при удалении провайдеров идентификации\",\n      \"display-name\": \"Псевдоним\",\n      \"identifier\": \"Имя пользователя\",\n      \"identifier-filter\": \"Фильтр по \\\"Имя пользователя\\\" (RegExp)\",\n      \"no-sso-found\": \"Нет настроенных SSO-провайдеров\",\n      \"redirect-url\": \"URL-адрес перенаправления\",\n      \"scopes\": \"Области\",\n      \"single-sign-on\": \"Настройка единого входа (SSO) для аутентификации\",\n      \"sso-created\": \"SSO-провайдер {{name}} создан\",\n      \"sso-list\": \"SSO-провайдеры\",\n      \"sso-updated\": \"SSO-провайдер {{name}} обновлен\",\n      \"template\": \"Шаблон\",\n      \"token-endpoint\": \"Конечная точка токена\",\n      \"update-sso\": \"Изменение SSO-провайдера\",\n      \"user-endpoint\": \"Конечная точка пользователя\",\n      \"label\": \"SSO\"\n    },\n    \"storage\": {\n      \"accesskey\": \"Ключ доступа\",\n      \"accesskey-placeholder\": \"Ключ доступа / идентификатор доступа\",\n      \"bucket\": \"Корзина\",\n      \"bucket-placeholder\": \"Название бакета\",\n      \"create-a-service\": \"Создать сервис\",\n      \"create-storage\": \"Создать хранилище\",\n      \"current-storage\": \"Текущее объектное хранилище\",\n      \"delete-storage\": \"Удалить хранилище\",\n      \"endpoint\": \"Конечная точка\",\n      \"filepath-template\": \"Шаблон имени файла\",\n      \"local-storage-path\": \"Путь к локальному хранилищу\",\n      \"path\": \"Путь к хранилищу\",\n      \"path-description\": \"Вы можете использовать переменные от локального хранилища, например {filename}\",\n      \"path-placeholder\": \"пользовательский/путь\",\n      \"presign-placeholder\": \"Генерировать временную публичную ссылку, необязательно\",\n      \"region\": \"Регион\",\n      \"region-placeholder\": \"Название региона\",\n      \"s3-compatible-url\": \"S3-совместимый URL\",\n      \"secretkey\": \"Секретный ключ\",\n      \"secretkey-placeholder\": \"Секретный ключ / ключ доступа\",\n      \"storage-services\": \"Список хранилищ\",\n      \"type-database\": \"База данных\",\n      \"type-local\": \"Локальное хранилище\",\n      \"update-a-service\": \"Обновить сервис\",\n      \"update-local-path\": \"Обновить путь к локальному хранилищу\",\n      \"update-local-path-description\": \"Путь к локальному хранилищу - это относительный путь к файлу вашей базы данных\",\n      \"update-storage\": \"Обновить хранилище\",\n      \"url-prefix\": \"Префикс URL\",\n      \"url-prefix-placeholder\": \"Пользовательский префикс URL, необязательно\",\n      \"url-suffix\": \"суффикс URL\",\n      \"url-suffix-placeholder\": \"Пользовательский суффикс URL, необязательно\",\n      \"warning-text\": \"Вы уверены, что хотите удалить хранилище `{{name}}`? ЭТО ДЕЙСТВИЕ НЕЛЬЗЯ ОТМЕНИТЬ\",\n      \"label\": \"Хранилище\"\n    },\n    \"system\": {\n      \"additional-script\": \"Дополнительный скрипт\",\n      \"additional-script-placeholder\": \"Напишите ваш JavaScript-код здесь\",\n      \"additional-style\": \"Дополнительный стиль\",\n      \"additional-style-placeholder\": \"Напишите ваш CSS-код здесь\",\n      \"allow-user-signup\": \"Разрешить регистрацию пользователей\",\n      \"customize-server\": {\n        \"description\": \"Описание\",\n        \"icon-url\": \"URL иконки\",\n        \"locale\": \"Локализация\",\n        \"title\": \"Настроить сервер\"\n      },\n      \"disable-password-login\": \"Отключить вход по паролю\",\n      \"disable-password-login-final-warning\": \"Пожалуйста, введите `ПОДТВЕРДИТЬ`, если вы знаете, что делаете.\",\n      \"disable-password-login-warning\": \"Это отключит вход по паролю для всех пользователей. Невозможно будет войти без изменения этого параметра в базе данных, если настроенные провайдеры идентификации не работают. Также будьте особенно осторожны при удалении провайдера идентификации.\",\n      \"display-with-updated-time\": \"Время изменения - основное\",\n      \"enable-auto-compact\": \"Включить автоматическое сжатие\",\n      \"enable-double-click-to-edit\": \"Редактировать двойным кликом\",\n      \"enable-password-login\": \"Разрешить вход по паролю\",\n      \"enable-password-login-warning\": \"Это разрешит вход по паролю для всех пользователей. Продолжайте, только если хотите, чтобы пользователи входили и по SSO и по паролю.\",\n      \"max-upload-size\": \"Макс. размер файла (МБ)\",\n      \"max-upload-size-hint\": \"Рекомендуемое значение 32 MБ\",\n      \"removed-completed-task-list-items\": \"Включить удаление выполненных\",\n      \"server-name\": \"Имя сервера\",\n      \"title\": \"Общие\",\n      \"label\": \"Система\"\n    },\n    \"version\": \"Версия\",\n    \"access-token\": {\n      \"access-token-copied-to-clipboard\": \"Токен скопирован в буфер обмена\",\n      \"access-token-deleted\": \"Токен доступа `{{description}}` удален\",\n      \"access-token-deletion\": \"Вы действительно хотите удалить токен доступа `{{description}}`?\\nЭТО ДЕЙСТВИЕ НЕЛЬЗЯ ОТМЕНИТЬ.\",\n      \"access-token-deletion-description\": \"Это действие нельзя отменить. Вам нужно будет обновить любые службы, использующие этот токен, чтобы они использовали новый токен.\",\n      \"create-dialog\": {\n        \"access-token-created\": \"Токен доступа `{{description}}` создан\",\n        \"create-access-token\": \"Создание токена доступа\",\n        \"created-at\": \"Создан\",\n        \"description\": \"Описание\",\n        \"duration-1m\": \"1 месяц\",\n        \"duration-8h\": \"8 часов\",\n        \"duration-never\": \"Никогда\",\n        \"expiration\": \"Срок действия\",\n        \"expires-at\": \"Истекает\",\n        \"some-description\": \"Введите описание...\"\n      },\n      \"description\": \"Список всех токенов доступа для вашей учетной записи.\",\n      \"title\": \"Токены доступа\",\n      \"token\": \"Токен\"\n    },\n    \"account\": {\n      \"change-password\": \"Изменить пароль\",\n      \"email-note\": \"Опционально\",\n      \"export-memos\": \"Экспорт заметок\",\n      \"nickname-note\": \"Отображается в системе\",\n      \"openapi-reset\": \"Очистить ключ OpenAPI\",\n      \"openapi-sample-post\": \"Привет #memos от {{url}}\",\n      \"openapi-title\": \"OpenAPI\",\n      \"reset-api\": \"Очистить API\",\n      \"title\": \"Информация об аккаунте\",\n      \"update-information\": \"Обновить информацию\",\n      \"username-note\": \"Используется для входа\"\n    },\n    \"instance\": {\n      \"disallow-change-nickname\": \"Запретить смену псевдонима\",\n      \"disallow-change-username\": \"Запретить смену имени пользователя\",\n      \"disallow-password-auth\": \"Запретить вход по паролю\",\n      \"disallow-user-registration\": \"Запретить регистрацию пользователей\",\n      \"monday\": \"Понедельник\",\n      \"saturday\": \"Суббота\",\n      \"sunday\": \"Воскресенье\",\n      \"week-start-day\": \"Начало недели\"\n    },\n    \"memo\": {\n      \"content-length-limit\": \"Макс. длина заметки (байт)\",\n      \"enable-blur-nsfw-content\": \"\\\"Размывать\\\" заметки с тегами\",\n      \"enable-memo-comments\": \"Комментарии\",\n      \"enable-memo-location\": \"Геометки\",\n      \"reactions\": \"Реакции\",\n      \"title\": \"Настройки заметок\",\n      \"label\": \"Заметки\"\n    },\n    \"webhook\": {\n      \"create-dialog\": {\n        \"an-easy-to-remember-name\": \"Название, которое легко запомнить\",\n        \"create-webhook\": \"Создание вебхука\",\n        \"create-webhook-success\": \"Вебхук `{{name}}` создан\",\n        \"edit-webhook\": \"Изменить вебхук\",\n        \"payload-url\": \"Ссылка\",\n        \"title\": \"Название\",\n        \"url-example-post-receive\": \"https://example.com/postreceive\"\n      },\n      \"delete-dialog\": {\n        \"delete-webhook-description\": \"Это действие нельзя отменить.\",\n        \"delete-webhook-success\": \"Вебхук `{{name}}` успешно удален\",\n        \"delete-webhook-title\": \"Вы уверены, что хотите удалить вебхук `{{name}}`?\"\n      },\n      \"no-webhooks-found\": \"Нет вебхуков\",\n      \"title\": \"Вебхуки\",\n      \"url\": \"Ссылка\"\n    }\n  },\n  \"tag\": {\n    \"all-tags\": \"Все теги\",\n    \"create-tag\": \"Создать тег\",\n    \"create-tags-guide\": \"Создавайте теги, используя `#tag` в заметках. Теги `#tag/subtag` можно показывать иерархично.\",\n    \"delete-confirm\": \"Вы точно хотите удалить этот тег?\",\n    \"delete-success\": \"Тег успешно удален\",\n    \"delete-tag\": \"Удалить тег\",\n    \"new-name\": \"Новое имя\",\n    \"no-tag-found\": \"Нет тегов\",\n    \"old-name\": \"Старое имя\",\n    \"rename-error-empty\": \"Тег не может быть пустым или содержать пробелы\",\n    \"rename-error-repeat\": \"Новое имя должно отличаться от старого\",\n    \"rename-success\": \"Тег обновлен\",\n    \"rename-tag\": \"Переименование тега\",\n    \"rename-tip\": \"Все ваши заметки с этим тегом будут обновлены\"\n  },\n  \"tooltip\": {\n    \"link-memo\": \"Связать заметку\",\n    \"markdown-menu\": \"Markdown\",\n    \"select-location\": \"Местоположение\",\n    \"select-visibility\": \"Видимость\",\n    \"tags\": \"Теги\",\n    \"upload-attachment\": \"Загрузить вложения\"\n  }\n}\n"
  },
  {
    "path": "web/src/locales/sl.json",
    "content": "{\n  \"about\": {\n    \"blogs\": \"Blogi\",\n    \"description\": \"Lahka storitev za zapiske, kjer je na prvem mestu zasebnost. Preprosto zajemite in delite svoje odlične misli.\",\n    \"documents\": \"Dokumenti\",\n    \"github-repository\": \"GitHub Repo\",\n    \"official-website\": \"Uradna stran\"\n  },\n  \"auth\": {\n    \"create-your-account\": \"Ustvari nov račun\",\n    \"host-tip\": \"Registrirani ste kot gostitelj strani.\",\n    \"new-password\": \"Novo geslo\",\n    \"repeat-new-password\": \"Ponovi novo geslo\",\n    \"sign-in-tip\": \"Že imaš račun?\",\n    \"sign-up-tip\": \"Še nimaš računa?\"\n  },\n  \"common\": {\n    \"about\": \"O programu\",\n    \"add\": \"Dodaj\",\n    \"admin\": \"Admin\",\n    \"all\": \"Vse\",\n    \"archive\": \"Arhiv\",\n    \"archived\": \"Arhivirani\",\n    \"attachments\": \"Priloge\",\n    \"auto-expand\": \"Samodejno razširi\",\n    \"avatar\": \"Avatar\",\n    \"basic\": \"Osnovni\",\n    \"beta\": \"Beta\",\n    \"calendar\": \"Koledar\",\n    \"cancel\": \"Prekliči\",\n    \"change\": \"Spremeni\",\n    \"clear\": \"Počisti\",\n    \"close\": \"Zapri\",\n    \"collapse\": \"Strni\",\n    \"confirm\": \"Potrdi\",\n    \"copy\": \"Kopiraj\",\n    \"create\": \"Dodaj\",\n    \"created-at\": \"Ustvarjeno\",\n    \"database\": \"Baza\",\n    \"day\": \"Dan\",\n    \"days\": {\n      \"fri\": \"Pet\",\n      \"mon\": \"Pon\",\n      \"sat\": \"Sob\",\n      \"sun\": \"Ned\",\n      \"thu\": \"Čet\",\n      \"tue\": \"Tor\",\n      \"wed\": \"Sre\"\n    },\n    \"delete\": \"Izbriši\",\n    \"description\": \"Opis\",\n    \"edit\": \"Uredi\",\n    \"email\": \"E-pošta\",\n    \"expand\": \"Razširi\",\n    \"explore\": \"Razišči\",\n    \"file\": \"Datoteka\",\n    \"filter\": \"Filtriraj\",\n    \"home\": \"Domov\",\n    \"image\": \"Slika\",\n    \"in\": \"v\",\n    \"inbox\": \"Prejeto\",\n    \"input\": \"Vnos\",\n    \"language\": \"Jezik\",\n    \"last-updated-at\": \"Zadnja posodobitev\",\n    \"learn-more\": \"Spoznaj več\",\n    \"link\": \"Povezava\",\n    \"map\": \"Zemljevid\",\n    \"mark\": \"Označi\",\n    \"memo\": \"Beležka\",\n    \"memos\": \"Beležke\",\n    \"more\": \"Več\",\n    \"name\": \"Ime\",\n    \"new\": \"Novo\",\n    \"nickname\": \"Nadimek\",\n    \"null\": \"Nič\",\n    \"or\": \"ali\",\n    \"password\": \"Geslo\",\n    \"pin\": \"Pripni\",\n    \"pinned\": \"Pripeto\",\n    \"preview\": \"Predogled\",\n    \"profile\": \"Profil\",\n    \"properties\": \"Lastnosti\",\n    \"referenced-by\": \"Nanaša se na\",\n    \"referencing\": \"Nanašanje\",\n    \"relations\": \"Relacije\",\n    \"remember-me\": \"Zapomni se me\",\n    \"rename\": \"Preimenuj\",\n    \"reset\": \"Ponastavi\",\n    \"resources\": \"Viri\",\n    \"restore\": \"Obnovi\",\n    \"role\": \"Vloga\",\n    \"save\": \"Shrani\",\n    \"search\": \"Iskanje\",\n    \"select\": \"Izberi\",\n    \"settings\": \"Nastavitve\",\n    \"share\": \"Deli\",\n    \"shortcut-filter\": \"Filter bližnjic\",\n    \"shortcuts\": \"Bližnjice\",\n    \"sign-in\": \"Prijavi se\",\n    \"sign-in-with\": \"Prijavi se z {{provider}}\",\n    \"sign-out\": \"Odjavi se\",\n    \"sign-up\": \"Registriraj\",\n    \"statistics\": \"Statistika\",\n    \"tags\": \"Značke\",\n    \"today\": \"Danes\",\n    \"title\": \"Naslov\",\n    \"tree-mode\": \"Drevesni način\",\n    \"type\": \"Tip\",\n    \"unpin\": \"Odpni\",\n    \"update\": \"Posodobi\",\n    \"upload\": \"Naloži\",\n    \"user\": \"Uporabnik\",\n    \"username\": \"Uporabniško ime\",\n    \"version\": \"Verzija\",\n    \"visibility\": \"Vidnost\",\n    \"yourself\": \"Ti\"\n  },\n  \"editor\": {\n    \"add-your-comment-here\": \"Tu dodaj svoj komentar...\",\n    \"any-thoughts\": \"Kakšne misli...\",\n    \"exit-focus-mode\": \"Izhod iz načina osredotočanja\",\n    \"focus-mode\": \"Način osredotočanja\",\n    \"no-changes-detected\": \"Ni zaznanih sprememb\",\n    \"save\": \"Shrani\",\n    \"saving\": \"Shranjujem...\",\n    \"slash-commands\": \"Vnesi `/` za ukaze\"\n  },\n  \"inbox\": {\n    \"failed-to-load\": \"Ni bilo mogoče naložiti elementa prejetega\",\n    \"memo-comment\": \"{{user}} je komentiral tvojo {{memo}}.\",\n    \"no-archived\": \"Ni arhiviranih obvestil\",\n    \"no-unread\": \"Ni neprebranih obvestil\",\n    \"unread\": \"Neprebrano\",\n    \"version-update\": \"Na voljo je nova verzija {{version}}!\"\n  },\n  \"markdown\": {\n    \"checkbox\": \"Potrditveno polje\",\n    \"code-block\": \"Kodni blok\",\n    \"content-syntax\": \"Sintaksa vsebine\"\n  },\n  \"memo\": {\n    \"archived-at\": \"Arhivirano ob\",\n    \"click-to-hide-nsfw-content\": \"Kliknite za skrivanje NSFW vsebine\",\n    \"click-to-show-nsfw-content\": \"Kliknite za prikaz NSFW vsebine\",\n    \"code\": \"Koda\",\n    \"comment\": {\n      \"self\": \"Komentarji\",\n      \"write-a-comment\": \"Napiši komentar\"\n    },\n    \"copy-link\": \"Kopiraj povezavo\",\n    \"copy-content\": \"Kopiraj vsebino\",\n    \"count-memos-in-date\": \"{{count}} {{memos}} v {{date}}\",\n    \"delete-confirm\": \"Ali ste prepričani, da želite izbrisati to beležko? TO DEJANJE JE NEPOVRATNO\",\n    \"delete-confirm-description\": \"To dejanje je nepovratno. Priložene datoteke, povezave in reference bodo prav tako odstranjene.\",\n    \"direction\": \"Smer\",\n    \"direction-asc\": \"Naraščajoče\",\n    \"direction-desc\": \"Padajoče\",\n    \"display-time\": \"Prikaži čas\",\n    \"filters\": {\n      \"has-code\": \"imaKodo\",\n      \"has-link\": \"imaPovezavo\",\n      \"has-task-list\": \"imaSeznamOpravil\"\n    },\n    \"links\": \"Povezave\",\n    \"load-more\": \"Naloži več\",\n    \"no-archived-memos\": \"Ni arhiviranih beležk.\",\n    \"no-memos\": \"Ni beležk.\",\n    \"order-by\": \"Razvrsti po\",\n    \"search-placeholder\": \"Poišči beležke\",\n    \"show-less\": \"Prikaži manj\",\n    \"show-more\": \"Prikaži več\",\n    \"to-do\": \"Opravila\",\n    \"view-detail\": \"Poglej podrobnosti\",\n    \"visibility\": {\n      \"disabled\": \"Javne beležke so onemogočene\",\n      \"private\": \"Zasebno\",\n      \"protected\": \"Za uporabnike\",\n      \"public\": \"Javno\"\n    }\n  },\n  \"message\": {\n    \"archived-successfully\": \"Uspešno arhivirano\",\n    \"change-memo-created-time\": \"Spremeni čas izdelave beležke\",\n    \"copied\": \"Skopirano\",\n    \"deleted-successfully\": \"Uspešno izbrisano\",\n    \"description-is-required\": \"Potreben je opis.\",\n    \"failed-to-embed-memo\": \"Beležke ni bilo mogoče vdelati\",\n    \"fill-all\": \"Prosim izpolnite vsa polja.\",\n    \"fill-all-required-fields\": \"Prosim izpolnite vsa zahtevana polja.\",\n    \"maximum-upload-size-is\": \"Največja dovoljena velikost nalaganja je {{size}} MiB\",\n    \"memo-not-found\": \"Ne najdem beležke.\",\n    \"new-password-not-match\": \"Novi gesli se ne ujemata.\",\n    \"no-data\": \"Ne najdem podatkov.\",\n    \"password-changed\": \"Geslo je spremenjeno\",\n    \"password-not-match\": \"Gesli se ne ujemata.\",\n    \"restored-successfully\": \"Uspešno obnovljeno\",\n    \"succeed-copy-content\": \"Vsebina je uspešno skopirana.\",\n    \"succeed-copy-link\": \"Povezava je uspešno skopirana.\",\n    \"update-succeed\": \"Posodobitev je uspešna\",\n    \"user-not-found\": \"Ne najdem uporabnika\"\n  },\n  \"reference\": {\n    \"add-references\": \"Dodaj referenco\",\n    \"embedded-usage\": \"Uporabi kot vdelano vsebino\",\n    \"no-memos-found\": \"Ne najdem beležk\",\n    \"search-placeholder\": \"Poišči po vsebini\"\n  },\n  \"resource\": {\n    \"clear\": \"Počisti\",\n    \"copy-link\": \"Kopiraj povezavo\",\n    \"create-dialog\": {\n      \"external-link\": {\n        \"file-name\": \"Ime datoteke\",\n        \"file-name-placeholder\": \"Ime datoteke\",\n        \"link\": \"Povezava\",\n        \"link-placeholder\": \"https://povezava.do/vasega/vira\",\n        \"option\": \"Zunanja povezava\",\n        \"type\": \"Tip\",\n        \"type-placeholder\": \"Tip datoteke\"\n      },\n      \"local-file\": {\n        \"choose\": \"Izberite datoteko...\",\n        \"option\": \"Lokalna datoteka\"\n      },\n      \"title\": \"Dodaj vir\",\n      \"upload-method\": \"Metoda nalaganja\"\n    },\n    \"delete-all-unused\": \"Izbriši vse neuporabljene\",\n    \"delete-all-unused-confirm\": \"Ali ste prepričani, da želite izbrisati vse neuporabljene vire? TO DEJANJE JE NEPOVRATNO\",\n    \"delete-all-unused-error\": \"Neuporabljenih virov ni bilo mogoče izbrisati\",\n    \"delete-all-unused-success\": \"Viri so bili uspešno izbrisani\",\n    \"delete-resource\": \"Izbriši vir\",\n    \"delete-selected-resources\": \"Izbrišite izbrane vire\",\n    \"fetching-data\": \"Pridobivam podatke...\",\n    \"file-drag-drop-prompt\": \"Za nalaganje, tu povlecite in spustite vaše datoteke\",\n    \"linked-amount\": \"Povezana količina beležk\",\n    \"no-files-selected\": \"Nobena datoteka ni označena\",\n    \"no-resources\": \"Ni virov.\",\n    \"no-unused-resources\": \"Ni neuporabljenih virov\",\n    \"reset-link\": \"Ponastavi povezavo\",\n    \"reset-link-prompt\": \"Ali ste prepričani, da želite ponastaviti povezavo? To bo prekinilo vse trenutne uporabe povezav. TO DEJANJE JE NEPOVRATNO\",\n    \"reset-resource-link\": \"Ponastavi povezavo do vira\",\n    \"unused-resources\": \"Neuporabljeni viri\"\n  },\n  \"router\": {\n    \"back-to-top\": \"Nazaj na vrh\",\n    \"go-to-home\": \"Nazaj domov\"\n  },\n  \"setting\": {\n    \"member\": {\n      \"admin\": \"Admin\",\n      \"archive-member\": \"Arhiviraj uporabnika\",\n      \"archive-success\": \"{{username}} je uspešno arhiviran\",\n      \"archive-warning\": \"Ali ste prepričani, da želite arhivirati {{username}}?\",\n      \"archive-warning-description\": \"Arhiviranje onemogoči račun. Lahko ga obnovite ali izbrišete kasneje.\",\n      \"create-a-member\": \"Dodaj novega uporabnika\",\n      \"delete-member\": \"Izbriši uporabnika\",\n      \"delete-success\": \"{{username}} je uspešno izbrisan\",\n      \"delete-warning\": \"Ali ste prepričani, da želite izbrisati {{username}}? TO DEJANJE JE NEPOVRATNO\",\n      \"delete-warning-description\": \"TO DEJANJE JE NEPOVRATNO\",\n      \"restore-success\": \"{{username}} je uspešno obnovljen\",\n      \"user\": \"Uporabnik\",\n      \"label\": \"Uporabnik\",\n      \"list-title\": \"Seznam uporabnikov\"\n    },\n    \"my-account\": {\n      \"label\": \"Moj račun\"\n    },\n    \"preference\": {\n      \"default-memo-sort-option\": \"Čas prikaza beležke\",\n      \"default-memo-visibility\": \"Privzeta vidnost beležke\",\n      \"theme\": \"Tema\",\n      \"label\": \"Nastavitve\"\n    },\n    \"shortcut\": {\n      \"delete-confirm\": \"Ali ste prepričani, da želite izbrisati bližnjico \\\"{{title}}\\\"?\",\n      \"delete-success\": \"Bližnjica \\\"{{title}}\\\" je uspešno izbrisana\"\n    },\n    \"sso\": {\n      \"authorization-endpoint\": \"Končna točka avtorizacije\",\n      \"client-id\": \"ID odjemalca\",\n      \"client-secret\": \"Geslo odjemalca\",\n      \"confirm-delete\": \"Ali ste prepričani, da želite izbrisati konfiguracijo SSO \\\"{{name}}\\\"? TO DEJANJE JE NEPOVRATNO\",\n      \"create-sso\": \"Izdelaj SSO\",\n      \"custom\": \"Po meri\",\n      \"delete-sso\": \"Potrdi brisanje\",\n      \"disabled-password-login-warning\": \"Prijava z geslom je onemogočena, bodite še posebej previdni pri odstranjevanju ponudnikov identitete\",\n      \"display-name\": \"Prikazno ime\",\n      \"identifier\": \"Identifikator\",\n      \"identifier-filter\": \"Filter identifikatorja\",\n      \"no-sso-found\": \"Ne najdem nobenega SSO.\",\n      \"redirect-url\": \"Preusmeritveni URL\",\n      \"scopes\": \"Območja\",\n      \"single-sign-on\": \"Nastavitve enotne prijave (SSO) za preverjanje pristnosti\",\n      \"sso-created\": \"SSO {{name}} je ustvarjen\",\n      \"sso-list\": \"SSO seznam\",\n      \"sso-updated\": \"SSO {{name}} je posodobljen\",\n      \"template\": \"Predloga\",\n      \"token-endpoint\": \"Končna točka žetona\",\n      \"update-sso\": \"Posodobi SSO\",\n      \"user-endpoint\": \"Končna točka uporabnika\",\n      \"label\": \"SSO\"\n    },\n    \"storage\": {\n      \"accesskey\": \"Dostopni ključ\",\n      \"accesskey-placeholder\": \"Dostopni ključ / ID dostopa\",\n      \"bucket\": \"Vedro\",\n      \"bucket-placeholder\": \"Ime vedra\",\n      \"create-a-service\": \"Ustvari storitev\",\n      \"create-storage\": \"Ustvari shrambo\",\n      \"current-storage\": \"Trenutna shramba predmetov\",\n      \"delete-storage\": \"Izbriši shrambo\",\n      \"endpoint\": \"Končna točka\",\n      \"filepath-template\": \"Predloga poti do datoteke\",\n      \"local-storage-path\": \"Lokalna pot shrambe\",\n      \"path\": \"Pot shrambe\",\n      \"path-description\": \"Uporabite lahko iste dinamične spremenljivke iz lokalne shrambe, kot je {filename}\",\n      \"path-placeholder\": \"pot/po/meri\",\n      \"presign-placeholder\": \"URL pred-prijave, opcijsko\",\n      \"region\": \"Regija\",\n      \"region-placeholder\": \"Ime regije\",\n      \"s3-compatible-url\": \"S3 združljiv URL\",\n      \"secretkey\": \"Skriti ključ\",\n      \"secretkey-placeholder\": \"Skriti ključ / ključ dostopa\",\n      \"storage-services\": \"Seznam storitev shramb\",\n      \"type-database\": \"Baza podatkov\",\n      \"type-local\": \"Lokalni datotečni sistem\",\n      \"update-a-service\": \"Posodobi storitev\",\n      \"update-local-path\": \"Posodobite pot do lokalne shrambe\",\n      \"update-local-path-description\": \"Pot lokalne shrambe je relativna pot do vaše datoteke zbirke podatkov\",\n      \"update-storage\": \"Posodobi shrambo\",\n      \"url-prefix\": \"URL predpona\",\n      \"url-prefix-placeholder\": \"URL predpona po meri, opcijsko\",\n      \"url-suffix\": \"URL pripona\",\n      \"url-suffix-placeholder\": \"URL pripona po meri, opcijsko\",\n      \"warning-text\": \"Ali ste prepričani, da želite izbrisati storitev shrambe \\\"{{name}}\\\"? TO DEJANJE JE NEPOVRATNO\",\n      \"label\": \"Shramba\"\n    },\n    \"system\": {\n      \"additional-script\": \"Dodatne skripte\",\n      \"additional-script-placeholder\": \"Dodatna JavaScript koda\",\n      \"additional-style\": \"Dodatni stili\",\n      \"additional-style-placeholder\": \"Dodatna CSS koda\",\n      \"allow-user-signup\": \"Omogočite kreiranje novih uporabnikov\",\n      \"customize-server\": {\n        \"description\": \"Opis\",\n        \"icon-url\": \"URL ikone\",\n        \"locale\": \"Jezik strežnika\",\n        \"title\": \"Prilagodi strežnik\"\n      },\n      \"disable-password-login\": \"Onemogoči prijavo z geslom\",\n      \"disable-password-login-final-warning\": \"Prosim vnesite \\\"POTRDI\\\", če veste kaj počnete.\",\n      \"disable-password-login-warning\": \"To bo onemogočilo prijavo z geslom za vse uporabnike. Brez ponovnega vklopa te nastavitve v bazi, prijava ne bo več možna. Enako tudi če identiteta enotne prijave zataji. Bodite tudi zelo previdni pri odstranjevalju identitete enotne prijave\",\n      \"display-with-updated-time\": \"Prikaži s časom posodobitve\",\n      \"enable-auto-compact\": \"Omogoči samodejno zgoščevanje\",\n      \"enable-double-click-to-edit\": \"Omogoči dvojni klik za urejanje\",\n      \"enable-password-login\": \"Omogoči prijavo z geslom\",\n      \"enable-password-login-warning\": \"To bo omogočilo prijavo z geslom za vse uporabnike. Nadaljujte samo, če želite, da se uporabniki lahko prijavijo z uporabo enotne prijave in gesla\",\n      \"max-upload-size\": \"Največja velikost nalaganja (MiB)\",\n      \"max-upload-size-hint\": \"Priporočena velikost je 32 MiB.\",\n      \"removed-completed-task-list-items\": \"Omogoči odstranitev končanih\",\n      \"server-name\": \"Ime strežnika\",\n      \"title\": \"Splošno\",\n      \"label\": \"Sistem\"\n    },\n    \"version\": \"Verzija\",\n    \"access-token\": {\n      \"access-token-copied-to-clipboard\": \"Žeton za dostop je kopiran v odložišče\",\n      \"access-token-deleted\": \"Dostopni žeton `{{description}}` je izbrisan\",\n      \"access-token-deletion\": \"Ali ste prepričani, da želite izbrisati dostopni žeton {{description}}? TO DEJANJE JE NEPOVRATNO.\",\n      \"access-token-deletion-description\": \"To dejanje je nepovratno. Morali boste posodobiti vse storitve, ki uporabljajo ta žeton, da uporabljajo novega.\",\n      \"create-dialog\": {\n        \"access-token-created\": \"Dostopni žeton `{{description}}` je ustvarjen\",\n        \"create-access-token\": \"Ustvari žeton za dostop\",\n        \"created-at\": \"Ustvarjen\",\n        \"description\": \"Opis\",\n        \"duration-1m\": \"1 mesec\",\n        \"duration-8h\": \"8 ur\",\n        \"duration-never\": \"Nikoli\",\n        \"expiration\": \"Potek\",\n        \"expires-at\": \"Poteče\",\n        \"some-description\": \"Nek opis...\"\n      },\n      \"description\": \"Seznam vseh dostopnih žetonov za vaš račun.\",\n      \"title\": \"Dostopni žetoni\",\n      \"token\": \"Žeton\"\n    },\n    \"account\": {\n      \"change-password\": \"Zamenjaj geslo\",\n      \"email-note\": \"Opcijsko\",\n      \"export-memos\": \"Izvozi beležke\",\n      \"nickname-note\": \"Prikazano v pasici\",\n      \"openapi-reset\": \"Ponastavi OpenAPI ključ\",\n      \"openapi-sample-post\": \"Pozdrav #memos iz {{url}}\",\n      \"openapi-title\": \"OpenAPI\",\n      \"reset-api\": \"Ponastavi API\",\n      \"title\": \"Podatki o računu\",\n      \"update-information\": \"Posodobi informacije\",\n      \"username-note\": \"Uporablja se za prijavo\"\n    },\n    \"instance\": {\n      \"disallow-change-nickname\": \"Onemogoči spremembo vzdevka\",\n      \"disallow-change-username\": \"Onemogoči spremembo uporabniškega imena\",\n      \"disallow-password-auth\": \"Onemogoči preverjanje gesla\",\n      \"disallow-user-registration\": \"Onemogoči registracije uporabnikov\",\n      \"monday\": \"Ponedeljek\",\n      \"saturday\": \"Sobota\",\n      \"sunday\": \"Nedelja\",\n      \"week-start-day\": \"Dan začetka tedna\"\n    },\n    \"memo\": {\n      \"content-length-limit\": \"Omejitev dolžine vsebine (bajt)\",\n      \"enable-blur-nsfw-content\": \"Omogoči zameglitev občutljive vsebine (NSFW)\",\n      \"enable-memo-comments\": \"Omogoči komentarje na beležkah\",\n      \"enable-memo-location\": \"Omogoči lokacijo beležk\",\n      \"reactions\": \"Odzivi\",\n      \"title\": \"Nastavitve povezane z beležko\",\n      \"label\": \"Beležka\"\n    },\n    \"webhook\": {\n      \"create-dialog\": {\n        \"an-easy-to-remember-name\": \"Ime, ki si ga lahko zapomniš\",\n        \"create-webhook\": \"Ustvari webhook\",\n        \"create-webhook-success\": \"Webhook \\\"{{name}}\\\" je ustvarjen\",\n        \"edit-webhook\": \"Uredi webhook\",\n        \"payload-url\": \"Payload URL\",\n        \"title\": \"Naslov\",\n        \"url-example-post-receive\": \"https://primer.com/postreceive\"\n      },\n      \"delete-dialog\": {\n        \"delete-webhook-description\": \"To dejanje je nepovratno.\",\n        \"delete-webhook-success\": \"Webhook \\\"{{name}}\\\" je uspešno izbrisan\",\n        \"delete-webhook-title\": \"Ali ste prepričani, da želite izbrisati webhook \\\"{{name}}\\\"?\"\n      },\n      \"no-webhooks-found\": \"Ne najdem nobenega webhooka.\",\n      \"title\": \"Webhooks\",\n      \"url\": \"URL\"\n    }\n  },\n  \"tag\": {\n    \"all-tags\": \"Vse značke\",\n    \"create-tag\": \"Kreiraj značko\",\n    \"create-tags-guide\": \"Značko lahko ustvarite z vnosom `#tag`.\",\n    \"delete-confirm\": \"Ste prepričani, da želite odstraniti to značko? Vse povezane beležke bodo arhivirane.\",\n    \"delete-success\": \"Značka uspešno izbrisana\",\n    \"delete-tag\": \"Izbriši značko\",\n    \"new-name\": \"Novo ime\",\n    \"no-tag-found\": \"Ne najdem značk\",\n    \"old-name\": \"Staro ime\",\n    \"rename-error-empty\": \"Ime značke ne sme biti prazno ali vsebovati presledkov\",\n    \"rename-error-repeat\": \"Novo ime ne sme biti enako staremu\",\n    \"rename-success\": \"Značka uspešno preimenovana\",\n    \"rename-tag\": \"Preimenuj značko\",\n    \"rename-tip\": \"Vse vaše beležke s to značko bodo posodobljene.\"\n  },\n  \"tooltip\": {\n    \"link-memo\": \"Povezava do beležke\",\n    \"markdown-menu\": \"Markdown\",\n    \"select-location\": \"Lokacija\",\n    \"select-visibility\": \"Vidnost\",\n    \"tags\": \"Značke\",\n    \"upload-attachment\": \"Naloži prilogo(e)\"\n  }\n}\n"
  },
  {
    "path": "web/src/locales/sv.json",
    "content": "{\n  \"about\": {\n    \"blogs\": \"Bloggar\",\n    \"description\": \"En integritetsfokuserad, lättviktig anteckningstjänst. Fånga och dela dina tankar enkelt.\",\n    \"documents\": \"Dokument\",\n    \"github-repository\": \"GitHub-lagringsplats\",\n    \"official-website\": \"Officiell webbplats\"\n  },\n  \"auth\": {\n    \"create-your-account\": \"Skapa ditt konto\",\n    \"host-tip\": \"Du registerar dig som webbplatsvärd.\",\n    \"new-password\": \"Nytt lösenord\",\n    \"repeat-new-password\": \"Upprepa det nya lösenordet\",\n    \"sign-in-tip\": \"Har du redan ett konto?\",\n    \"sign-up-tip\": \"Har du inget konto än?\"\n  },\n  \"common\": {\n    \"about\": \"Om\",\n    \"add\": \"Lägg till\",\n    \"admin\": \"Administratör\",\n    \"all\": \"Alla\",\n    \"archive\": \"Arkivera\",\n    \"archived\": \"Arkiverad\",\n    \"attachments\": \"Bilagor\",\n    \"auto-expand\": \"Autoexpandera\",\n    \"avatar\": \"Profilbild\",\n    \"basic\": \"Grundläggande\",\n    \"beta\": \"Beta\",\n    \"calendar\": \"Kalender\",\n    \"cancel\": \"Avbryt\",\n    \"change\": \"Ändra\",\n    \"clear\": \"Rensa\",\n    \"close\": \"Stäng\",\n    \"collapse\": \"Fäll ihop\",\n    \"confirm\": \"Bekräfta\",\n    \"copy\": \"Kopiera\",\n    \"create\": \"Skapa\",\n    \"created-at\": \"Skapad\",\n    \"database\": \"Databas\",\n    \"day\": \"Dag\",\n    \"days\": {\n      \"fri\": \"Fre\",\n      \"mon\": \"Mån\",\n      \"sat\": \"Lör\",\n      \"sun\": \"Sön\",\n      \"thu\": \"Tors\",\n      \"tue\": \"Tis\",\n      \"wed\": \"Ons\"\n    },\n    \"delete\": \"Radera\",\n    \"description\": \"Beskrivning\",\n    \"edit\": \"Redigera\",\n    \"email\": \"E-post\",\n    \"expand\": \"Expandera\",\n    \"explore\": \"Utforska\",\n    \"file\": \"Fil\",\n    \"filter\": \"Filter\",\n    \"home\": \"Hem\",\n    \"image\": \"Bild\",\n    \"in\": \"I\",\n    \"inbox\": \"Inkorg\",\n    \"input\": \"Inmatning\",\n    \"language\": \"Språk\",\n    \"last-updated-at\": \"Senast uppdaterad\",\n    \"learn-more\": \"Läs mer\",\n    \"link\": \"Länk\",\n    \"map\": \"Karta\",\n    \"mark\": \"Markera\",\n    \"memo\": \"Anteckning\",\n    \"memos\": \"Anteckningar\",\n    \"more\": \"Mer\",\n    \"name\": \"Namn\",\n    \"new\": \"Ny\",\n    \"nickname\": \"Smeknamn\",\n    \"null\": \"Null\",\n    \"or\": \"eller\",\n    \"password\": \"Lösenord\",\n    \"pin\": \"Fäst\",\n    \"pinned\": \"Fäst\",\n    \"preview\": \"Förhandsgranska\",\n    \"profile\": \"Profil\",\n    \"properties\": \"Egenskaper\",\n    \"referenced-by\": \"Refererad av\",\n    \"referencing\": \"Refererar\",\n    \"relations\": \"Relationer\",\n    \"remember-me\": \"Kom ihåg mig\",\n    \"rename\": \"Byt namn\",\n    \"reset\": \"Återställ\",\n    \"resources\": \"Resurser\",\n    \"restore\": \"Återställ\",\n    \"role\": \"Roll\",\n    \"save\": \"Spara\",\n    \"search\": \"Sök\",\n    \"select\": \"Välj\",\n    \"settings\": \"Inställningar\",\n    \"share\": \"Dela\",\n    \"shortcut-filter\": \"Genvägsfilter\",\n    \"shortcuts\": \"Genvägar\",\n    \"sign-in\": \"Logga in\",\n    \"sign-in-with\": \"Logga in med {{provider}}\",\n    \"sign-out\": \"Logga ut\",\n    \"sign-up\": \"Bli medlem\",\n    \"statistics\": \"Statistik\",\n    \"tags\": \"Taggar\",\n    \"title\": \"Titel\",\n    \"today\": \"Idag\",\n    \"tree-mode\": \"Trädvy\",\n    \"type\": \"Typ\",\n    \"unpin\": \"Ta bort fäst\",\n    \"update\": \"Uppdatera\",\n    \"upload\": \"Ladda upp\",\n    \"user\": \"Användare\",\n    \"username\": \"Användarnamn\",\n    \"version\": \"Version\",\n    \"visibility\": \"Synlighet\",\n    \"yourself\": \"Själv\"\n  },\n  \"editor\": {\n    \"add-your-comment-here\": \"Lägg till din kommentar här...\",\n    \"any-thoughts\": \"Några tankar...\",\n    \"exit-focus-mode\": \"Avsluta fokusläge\",\n    \"focus-mode\": \"Fokusläge\",\n    \"no-changes-detected\": \"Inga ändringar upptäckta\",\n    \"save\": \"Spara\",\n    \"saving\": \"Sparar...\",\n    \"slash-commands\": \"Skriv `/` för kommandon\"\n  },\n  \"inbox\": {\n    \"failed-to-load\": \"Misslyckades att ladda inkorgsobjekt\",\n    \"memo-comment\": \"{{user}} har kommenterat din {{memo}}.\",\n    \"no-archived\": \"Inga arkiverade notifieringar\",\n    \"no-unread\": \"Inga olästa notifieringar\",\n    \"unread\": \"Oläst\"\n  },\n  \"markdown\": {\n    \"checkbox\": \"Kryssruta\",\n    \"code-block\": \"Kodblock\",\n    \"content-syntax\": \"Innehållssyntax\"\n  },\n  \"memo\": {\n    \"archived-at\": \"Arkiverad\",\n    \"click-to-hide-nsfw-content\": \"Klicka för att dölja känsligt innehåll (NSFW)\",\n    \"click-to-show-nsfw-content\": \"Klicka för att visa känsligt innehåll (NSFW)\",\n    \"code\": \"Kod\",\n    \"comment\": {\n      \"self\": \"Kommentarer\",\n      \"write-a-comment\": \"Skriv en kommentar\"\n    },\n    \"copy-content\": \"Kopiera innehåll\",\n    \"copy-link\": \"Kopiera länk\",\n    \"count-memos-in-date\": \"{{count}} {{memos}} i {{date}}\",\n    \"delete-confirm\": \"Är du säker på att du vill radera denna anteckning? DENNA ÅTGÄRD ÄR OÅTERKALLELIG\",\n    \"delete-confirm-description\": \"Denna åtgärd är oåterkallelig. Bilagor, länk och referenser kommer också att tas bort.\",\n    \"direction\": \"Riktning\",\n    \"direction-asc\": \"Stigande\",\n    \"direction-desc\": \"Fallande\",\n    \"display-time\": \"Visa tid\",\n    \"filters\": {\n      \"has-code\": \"harKod\",\n      \"has-link\": \"harLänk\",\n      \"has-task-list\": \"harAttGöraLista\"\n    },\n    \"links\": \"Länkar\",\n    \"load-more\": \"Ladda mer\",\n    \"no-archived-memos\": \"Inga arkiverade anteckningar.\",\n    \"no-memos\": \"Inga anteckningar.\",\n    \"order-by\": \"Sortera efter\",\n    \"search-placeholder\": \"Sök anteckningar...\",\n    \"show-less\": \"Visa mindre\",\n    \"show-more\": \"Visa mer\",\n    \"to-do\": \"Att göra\",\n    \"view-detail\": \"Visa detaljer\",\n    \"visibility\": {\n      \"disabled\": \"Offentliga anteckningar är inaktiverade\",\n      \"private\": \"Privat\",\n      \"protected\": \"Skyddad\",\n      \"public\": \"Offentlig\"\n    }\n  },\n  \"message\": {\n    \"archived-successfully\": \"Arkiverad framgångsrikt\",\n    \"change-memo-created-time\": \"Ändra anteckningens skapandetid\",\n    \"copied\": \"Kopierad\",\n    \"deleted-successfully\": \"Raderad framgångsrikt\",\n    \"description-is-required\": \"Beskrivning krävs\",\n    \"failed-to-embed-memo\": \"Kunde inte bädda in anteckning\",\n    \"fill-all\": \"Var god fyll i alla fält.\",\n    \"fill-all-required-fields\": \"Var god fyll i alla obligatoriska fält\",\n    \"maximum-upload-size-is\": \"Maximal tillåten uppladdningsstorlek är {{size}} MiB\",\n    \"memo-not-found\": \"Anteckning hittades inte.\",\n    \"new-password-not-match\": \"Nya lösenord matchar inte.\",\n    \"no-data\": \"Ingen data hittades.\",\n    \"password-changed\": \"Lösenord ändrat\",\n    \"password-not-match\": \"Lösenorden matchar inte.\",\n    \"restored-successfully\": \"Återställdes framgångsrikt\",\n    \"succeed-copy-content\": \"Innehåll kopierades framgångsrikt.\",\n    \"succeed-copy-link\": \"Länk kopierades framgångsrikt.\",\n    \"update-succeed\": \"Uppdatering lyckades\",\n    \"user-not-found\": \"Användaren hittades inte\"\n  },\n  \"reference\": {\n    \"add-references\": \"Lägg till referenser\",\n    \"embedded-usage\": \"Använd som inbäddat innehåll\",\n    \"no-memos-found\": \"Inga anteckningar hittades\",\n    \"search-placeholder\": \"Sök innehåll\"\n  },\n  \"resource\": {\n    \"clear\": \"Rensa\",\n    \"copy-link\": \"Kopiera länk\",\n    \"create-dialog\": {\n      \"external-link\": {\n        \"file-name\": \"Filnamn\",\n        \"file-name-placeholder\": \"Filnamn\",\n        \"link\": \"Länk\",\n        \"link-placeholder\": \"https://länk.till/din/resurs\",\n        \"option\": \"Extern länk\",\n        \"type\": \"Typ\",\n        \"type-placeholder\": \"Filtyp\"\n      },\n      \"local-file\": {\n        \"choose\": \"Välj en fil…\",\n        \"option\": \"Lokal fil\"\n      },\n      \"title\": \"Skapa resurs\",\n      \"upload-method\": \"Uppladdningsmetod\"\n    },\n    \"delete-all-unused\": \"Ta bort alla oanvända\",\n    \"delete-all-unused-confirm\": \"Är du säker på att du vill ta bort alla oanvända resurser? DENNA ÅTGÄRD ÄR OÅTERKALLELIG\",\n    \"delete-all-unused-error\": \"Misslyckades att ta bort oanvända resurser\",\n    \"delete-all-unused-success\": \"Resurser raderades framgångsrikt\",\n    \"delete-resource\": \"Ta bort resurs\",\n    \"delete-selected-resources\": \"Ta bort valda resurser\",\n    \"fetching-data\": \"Hämtar data…\",\n    \"file-drag-drop-prompt\": \"Dra och släpp din fil här för att ladda upp\",\n    \"linked-amount\": \"Länkat antal\",\n    \"no-files-selected\": \"Inga filer valda\",\n    \"no-resources\": \"Inga resurser.\",\n    \"no-unused-resources\": \"Inga oanvända resurser\",\n    \"reset-link\": \"Återställ länk\",\n    \"reset-link-prompt\": \"Är du säker på att återställa länken? Detta bryter alla nuvarande länkanvändningar. DENNA ÅTGÄRD ÄR OÅTERKALLELIG\",\n    \"reset-resource-link\": \"Återställ resurslänk\",\n    \"unused-resources\": \"Oanvända resurser\"\n  },\n  \"router\": {\n    \"back-to-top\": \"Tillbaka till toppen\",\n    \"go-to-home\": \"Gå till hem\"\n  },\n  \"setting\": {\n    \"member\": {\n      \"admin\": \"Administratör\",\n      \"archive-member\": \"Arkivera medlem\",\n      \"archive-success\": \"{{username}} arkiverades framgångsrikt\",\n      \"archive-warning\": \"Är du säker på att arkivera {{username}}?\",\n      \"archive-warning-description\": \"Arkivering inaktiverar kontot. Du kan återställa eller ta bort det senare.\",\n      \"create-a-member\": \"Skapa en medlem\",\n      \"delete-member\": \"Radera medlem\",\n      \"delete-success\": \"{{username}} raderades framgångsrikt\",\n      \"delete-warning\": \"Är du säker på att radera {{username}}? DENNA ÅTGÄRD ÄR OÅTERKALLELIG\",\n      \"delete-warning-description\": \"DENNA ÅTGÄRD ÄR OÅTERKALLELIG\",\n      \"restore-success\": \"{{username}} återställdes framgångsrikt\",\n      \"user\": \"Användare\",\n      \"label\": \"Medlem\",\n      \"list-title\": \"Medlemslista\"\n    },\n    \"my-account\": {\n      \"label\": \"Mitt konto\"\n    },\n    \"preference\": {\n      \"default-memo-sort-option\": \"Anteckning visningstid\",\n      \"default-memo-visibility\": \"Standard synlighet för anteckningar\",\n      \"theme\": \"Tema\",\n      \"label\": \"Preferenser\"\n    },\n    \"shortcut\": {\n      \"delete-confirm\": \"Är du säker på att du vill ta bort genvägen `{{title}}`?\",\n      \"delete-success\": \"Genväg `{{title}}` raderades framgångsrikt\"\n    },\n    \"sso\": {\n      \"authorization-endpoint\": \"Auktoriseringsendpoint\",\n      \"client-id\": \"Klient-ID\",\n      \"client-secret\": \"Klienthemlighet\",\n      \"confirm-delete\": \"Är du säker på att ta bort SSO-konfigurationen \\\"{{name}}\\\"? DENNA ÅTGÄRD ÄR OÅTERKALLELIG\",\n      \"create-sso\": \"Skapa SSO\",\n      \"custom\": \"Anpassad\",\n      \"delete-sso\": \"Bekräfta borttagning\",\n      \"disabled-password-login-warning\": \"Lösenordsinloggning är inaktiverad, var extra försiktig när du tar bort identitetsleverantörer\",\n      \"display-name\": \"Visningsnamn\",\n      \"identifier\": \"Identifierare\",\n      \"identifier-filter\": \"Identifierarfilter\",\n      \"no-sso-found\": \"Ingen SSO hittades.\",\n      \"redirect-url\": \"Omdirigerings-URL\",\n      \"scopes\": \"Omfattningar\",\n      \"single-sign-on\": \"Konfigurerar Single Sign-On (SSO) för autentisering\",\n      \"sso-created\": \"SSO {{name}} skapad\",\n      \"sso-list\": \"SSO-lista\",\n      \"sso-updated\": \"SSO {{name}} uppdaterad\",\n      \"template\": \"Mall\",\n      \"token-endpoint\": \"Token-endpoint\",\n      \"update-sso\": \"Uppdatera SSO\",\n      \"user-endpoint\": \"Användarendpoint\",\n      \"label\": \"SSO\"\n    },\n    \"storage\": {\n      \"accesskey\": \"Åtkomstnyckel\",\n      \"accesskey-placeholder\": \"Åtkomstnyckel / Åtkomst-ID\",\n      \"bucket\": \"Bucket\",\n      \"bucket-placeholder\": \"Bucket-namn\",\n      \"create-a-service\": \"Skapa en tjänst\",\n      \"create-storage\": \"Skapa lagring\",\n      \"current-storage\": \"Nuvarande objektlagring\",\n      \"delete-storage\": \"Ta bort lagring\",\n      \"endpoint\": \"Ändpunkt\",\n      \"filepath-template\": \"Filvägsmall\",\n      \"local-storage-path\": \"Lokal lagringsväg\",\n      \"path\": \"Lagringsväg\",\n      \"path-description\": \"Du kan använda samma dynamiska variabler som i lokal lagring, t.ex. {filename}\",\n      \"path-placeholder\": \"anpassad/väg\",\n      \"presign-placeholder\": \"Försignerad URL, valfritt\",\n      \"region\": \"Region\",\n      \"region-placeholder\": \"Regionsnamn\",\n      \"s3-compatible-url\": \"S3-kompatibel URL\",\n      \"secretkey\": \"Hemlig nyckel\",\n      \"secretkey-placeholder\": \"Hemlig nyckel / Åtkomstnyckel\",\n      \"storage-services\": \"Lagringstjänster\",\n      \"type-database\": \"Databas\",\n      \"type-local\": \"Lokalt filsystem\",\n      \"update-a-service\": \"Uppdatera en tjänst\",\n      \"update-local-path\": \"Uppdatera lokal lagringsväg\",\n      \"update-local-path-description\": \"Lokal lagringsväg är en relativ väg till din databasfil\",\n      \"update-storage\": \"Uppdatera lagring\",\n      \"url-prefix\": \"URL-prefix\",\n      \"url-prefix-placeholder\": \"Anpassat URL-prefix, valfritt\",\n      \"url-suffix\": \"URL-suffix\",\n      \"url-suffix-placeholder\": \"Anpassat URL-suffix, valfritt\",\n      \"warning-text\": \"Är du säker på att ta bort lagringstjänsten \\\"{{name}}\\\"? DENNA ÅTGÄRD ÄR OÅTERKALLELIG\",\n      \"label\": \"Lagring\"\n    },\n    \"system\": {\n      \"additional-script\": \"Ytterligare skript\",\n      \"additional-script-placeholder\": \"Ytterligare JavaScript kod\",\n      \"additional-style\": \"Ytterligare stil\",\n      \"additional-style-placeholder\": \"Ytterligare CSS kod\",\n      \"allow-user-signup\": \"Tillåt användarregistrering\",\n      \"customize-server\": {\n        \"description\": \"Beskrivning\",\n        \"icon-url\": \"Ikon-URL\",\n        \"locale\": \"Serverns språk\",\n        \"title\": \"Anpassa server\"\n      },\n      \"disable-password-login\": \"Inaktivera lösenordsinloggning\",\n      \"disable-password-login-final-warning\": \"Skriv \\\"CONFIRM\\\" om du vet vad du gör.\",\n      \"disable-password-login-warning\": \"Detta inaktiverar lösenordsinloggning för alla användare. Det går inte att logga in utan att återställa denna inställning i databasen om dina identitetsleverantörer misslyckas. Var extra försiktig när du tar bort en identitetsleverantör.\",\n      \"display-with-updated-time\": \"Visa med uppdaterad tid\",\n      \"enable-auto-compact\": \"Aktivera automatisk komprimering\",\n      \"enable-double-click-to-edit\": \"Aktivera dubbelklick för att redigera\",\n      \"enable-password-login\": \"Aktivera lösenordsinloggning\",\n      \"enable-password-login-warning\": \"Detta aktiverar lösenordsinloggning för alla användare. Fortsätt endast om du vill att användare ska kunna logga in med både SSO och lösenord\",\n      \"max-upload-size\": \"Maximal uppladdningsstorlek (MiB)\",\n      \"max-upload-size-hint\": \"Rekommenderat värde är 32 MiB.\",\n      \"removed-completed-task-list-items\": \"Aktivera borttagning av avklarade att-göra-poster\",\n      \"server-name\": \"Servernamn\",\n      \"title\": \"Allmänt\",\n      \"label\": \"System\"\n    },\n    \"version\": \"Version\",\n    \"access-token\": {\n      \"access-token-copied-to-clipboard\": \"Åtkomsttoken kopierad till urklipp\",\n      \"access-token-deleted\": \"Åtkomsttoken `{{description}}` raderad\",\n      \"access-token-deletion\": \"Är du säker på att ta bort åtkomsttoken {{description}}? DENNA ÅTGÄRD ÄR OÅTERKALLELIG.\",\n      \"access-token-deletion-description\": \"Denna åtgärd är oåterkallelig. Du måste uppdatera alla tjänster som använder denna token för att använda en ny token.\",\n      \"create-dialog\": {\n        \"access-token-created\": \"Åtkomsttoken `{{description}}` skapad\",\n        \"create-access-token\": \"Skapa åtkomsttoken\",\n        \"created-at\": \"Skapad\",\n        \"description\": \"Beskrivning\",\n        \"duration-1m\": \"1 månad\",\n        \"duration-8h\": \"8 timmar\",\n        \"duration-never\": \"Aldrig\",\n        \"expiration\": \"Utgång\",\n        \"expires-at\": \"Går ut\",\n        \"some-description\": \"Någon beskrivning...\"\n      },\n      \"description\": \"En lista över alla åtkomsttoken för ditt konto.\",\n      \"title\": \"Åtkomsttoken\",\n      \"token\": \"Token\"\n    },\n    \"account\": {\n      \"change-password\": \"Ändra lösenord\",\n      \"email-note\": \"Valfritt\",\n      \"export-memos\": \"Exportera anteckningar\",\n      \"nickname-note\": \"Visas i bannern\",\n      \"openapi-reset\": \"Återställ OpenAPI-nyckel\",\n      \"openapi-sample-post\": \"Hej #memos från {{url}}\",\n      \"openapi-title\": \"OpenAPI\",\n      \"reset-api\": \"Återställ API\",\n      \"title\": \"Kontoinformation\",\n      \"update-information\": \"Uppdatera informationen\",\n      \"username-note\": \"Används för att logga in\"\n    },\n    \"instance\": {\n      \"disallow-change-nickname\": \"Förbjud ändring av smeknamn\",\n      \"disallow-change-username\": \"Förbjud ändring av användarnamn\",\n      \"disallow-password-auth\": \"Förbjud lösenordsautentisering\",\n      \"disallow-user-registration\": \"Förbjud användarregistrering\",\n      \"monday\": \"Måndag\",\n      \"saturday\": \"Lördag\",\n      \"sunday\": \"Söndag\",\n      \"week-start-day\": \"Veckans första dag\"\n    },\n    \"memo\": {\n      \"content-length-limit\": \"Innehållslängdsgräns (Byte)\",\n      \"enable-blur-nsfw-content\": \"Aktivera suddighet för känsligt innehåll (NSFW)\",\n      \"enable-memo-comments\": \"Aktivera kommentarer på anteckningar\",\n      \"enable-memo-location\": \"Aktivera plats för anteckning\",\n      \"reactions\": \"Reaktioner\",\n      \"title\": \"Inställningar för anteckningar\",\n      \"label\": \"Anteckning\"\n    },\n    \"webhook\": {\n      \"create-dialog\": {\n        \"an-easy-to-remember-name\": \"Ett lättnamn\",\n        \"create-webhook\": \"Skapa webhook\",\n        \"create-webhook-success\": \"Webhook `{{name}}` skapad\",\n        \"edit-webhook\": \"Redigera webhook\",\n        \"payload-url\": \"Payload-URL\",\n        \"title\": \"Titel\",\n        \"url-example-post-receive\": \"https://exempel.se/postreceive\"\n      },\n      \"delete-dialog\": {\n        \"delete-webhook-description\": \"Denna åtgärd är oåterkallelig.\",\n        \"delete-webhook-success\": \"Webhook `{{name}}` raderades framgångsrikt\",\n        \"delete-webhook-title\": \"Är du säker på att du vill ta bort webhook `{{name}}`?\"\n      },\n      \"no-webhooks-found\": \"Inga webhooks hittades.\",\n      \"title\": \"Webhooks\",\n      \"url\": \"URL\"\n    }\n  },\n  \"tag\": {\n    \"all-tags\": \"Alla taggar\",\n    \"create-tag\": \"Skapa tagg\",\n    \"create-tags-guide\": \"Du kan skapa taggar genom att skriva `#tag`.\",\n    \"delete-confirm\": \"Är du säker på att ta bort denna tagg? Alla relaterade anteckningar kommer att arkiveras.\",\n    \"delete-success\": \"Tagg raderades framgångsrikt\",\n    \"delete-tag\": \"Ta bort tagg\",\n    \"new-name\": \"Nytt namn\",\n    \"no-tag-found\": \"Ingen tagg hittades\",\n    \"old-name\": \"Gammalt namn\",\n    \"rename-error-empty\": \"Taggnamn kan inte vara tomt eller innehålla mellanslag\",\n    \"rename-error-repeat\": \"Nytt namn kan inte vara samma som det gamla namnet\",\n    \"rename-success\": \"Tagg bytte namn framgångsrikt\",\n    \"rename-tag\": \"Byt namn på tagg\",\n    \"rename-tip\": \"Alla dina anteckningar med denna tagg kommer att uppdateras\"\n  },\n  \"tooltip\": {\n    \"link-memo\": \"Länk till anteckning\",\n    \"markdown-menu\": \"Markdown\",\n    \"select-location\": \"Plats\",\n    \"select-visibility\": \"Synlighet\",\n    \"tags\": \"Taggar\",\n    \"upload-attachment\": \"Ladda upp bilagor\"\n  }\n}\n"
  },
  {
    "path": "web/src/locales/th.json",
    "content": "{\n  \"about\": {\n    \"blogs\": \"บล็อก\",\n    \"description\": \"บริการจดโน้ตที่เน้นความเป็นส่วนตัวและน้ำหนักเบา จับและแบ่งปันความคิดดีๆ ของคุณได้อย่างง่ายดาย\",\n    \"documents\": \"เอกสาร\",\n    \"github-repository\": \"ที่เก็บ GitHub\",\n    \"official-website\": \"เว็บไซต์ทางการ\"\n  },\n  \"auth\": {\n    \"create-your-account\": \"สร้างบัญชีใหม่\",\n    \"host-tip\": \"คุณกำลังลงทะเบียนเป็นโฮสต์ของไซต์\",\n    \"new-password\": \"รหัสผ่านใหม่\",\n    \"repeat-new-password\": \"รหัสผ่านใหม่อีกครั้ง\",\n    \"sign-in-tip\": \"มีบัญชีอยู่แล้ว?\",\n    \"sign-up-tip\": \"ยังไม่มีบัญชี?\"\n  },\n  \"common\": {\n    \"about\": \"เกี่ยวกับ\",\n    \"add\": \"เพิ่ม\",\n    \"admin\": \"ผู้ดูแล\",\n    \"all\": \"ทั้งหมด\",\n    \"archive\": \"เก็บถาวร\",\n    \"archived\": \"เก็บถาวรแล้ว\",\n    \"attachments\": \"ไฟล์แนบ\",\n    \"auto-expand\": \"ขยายอัตโนมัติ\",\n    \"avatar\": \"สัญลักษณ์\",\n    \"basic\": \"ทั่วไป\",\n    \"beta\": \"รุ่นเบต้า\",\n    \"calendar\": \"ปฏิทิน\",\n    \"cancel\": \"ยกเลิก\",\n    \"change\": \"เปลี่ยน\",\n    \"clear\": \"ล้าง\",\n    \"close\": \"ปิด\",\n    \"collapse\": \"ย่อ\",\n    \"confirm\": \"ยืนยัน\",\n    \"copy\": \"คัดลอก\",\n    \"create\": \"สร้าง\",\n    \"created-at\": \"สร้างเมื่อ\",\n    \"database\": \"ฐานข้อมูล\",\n    \"day\": \"วัน\",\n    \"days\": {\n      \"fri\": \"ศุกร์\",\n      \"mon\": \"จันทร์\",\n      \"sat\": \"เสาร์\",\n      \"sun\": \"อาทิตย์\",\n      \"thu\": \"พฤหัสบดี\",\n      \"tue\": \"อังคาร\",\n      \"wed\": \"พุธ\"\n    },\n    \"delete\": \"ลบ\",\n    \"description\": \"คำอธิบาย\",\n    \"edit\": \"แก้ไข\",\n    \"email\": \"อีเมล\",\n    \"expand\": \"ขยาย\",\n    \"explore\": \"สำรวจ\",\n    \"file\": \"ไฟล์\",\n    \"filter\": \"ตัวกรอง\",\n    \"home\": \"หน้าหลัก\",\n    \"image\": \"รูปภาพ\",\n    \"in\": \"ใน\",\n    \"inbox\": \"กล่องจดหมาย\",\n    \"input\": \"อินพุต\",\n    \"language\": \"ภาษา\",\n    \"last-updated-at\": \"อัปเดตล่าสุดเมื่อ\",\n    \"learn-more\": \"เรียนรู้เพิ่มเติม\",\n    \"link\": \"ลิงก์\",\n    \"map\": \"แผนที่\",\n    \"mark\": \"ทำเครื่องหมาย\",\n    \"memo\": \"บันทึกช่วยจำ\",\n    \"memos\": \"บันทึกช่วยจำ\",\n    \"more\": \"เพิ่มเติม\",\n    \"name\": \"ชื่อ\",\n    \"new\": \"ใหม่\",\n    \"nickname\": \"ชื่อเล่น\",\n    \"null\": \"ว่างเปล่า\",\n    \"or\": \"หรือ\",\n    \"password\": \"รหัสผ่าน\",\n    \"pin\": \"ปักหมุด\",\n    \"pinned\": \"ปักหมุดแล้ว\",\n    \"preview\": \"ตัวอย่าง\",\n    \"profile\": \"ข้อมูลส่วนตัว\",\n    \"properties\": \"คุณสมบัติ\",\n    \"referenced-by\": \"อ้างอิงโดย\",\n    \"referencing\": \"กำลังอ้างอิง\",\n    \"relations\": \"ความสัมพันธ์\",\n    \"remember-me\": \"จำฉันไว้\",\n    \"rename\": \"เปลี่ยนชื่อ\",\n    \"reset\": \"ตั้งค่าใหม่\",\n    \"resources\": \"แหล่งข้อมูล\",\n    \"restore\": \"คืนค่า\",\n    \"role\": \"บทบาท\",\n    \"save\": \"บันทึก\",\n    \"search\": \"ค้นหา\",\n    \"select\": \"เลือก\",\n    \"settings\": \"ตั้งค่า\",\n    \"share\": \"แชร์\",\n    \"shortcut-filter\": \"ตัวกรองทางลัด\",\n    \"shortcuts\": \"ทางลัด\",\n    \"sign-in\": \"เข้าระบบ\",\n    \"sign-in-with\": \"เข้าระบบโดย {{provider}}\",\n    \"sign-out\": \"ออกจากระบบ\",\n    \"sign-up\": \"สมัครใช้งาน\",\n    \"statistics\": \"สถิติ\",\n    \"tags\": \"ป้ายกำกับ\",\n    \"title\": \"ชื่อเรื่อง\",\n    \"today\": \"วันนี้\",\n    \"tree-mode\": \"โหมดต้นไม้\",\n    \"type\": \"ประเภท\",\n    \"unpin\": \"ลบการปักหมุด\",\n    \"update\": \"อัปเดต\",\n    \"upload\": \"อัปโหลด\",\n    \"user\": \"ผู้ใช้\",\n    \"username\": \"ชื่อผู้ใช้\",\n    \"version\": \"เวอร์ชั่น\",\n    \"visibility\": \"การมองเห็น\",\n    \"yourself\": \"ตัวคุณ\"\n  },\n  \"editor\": {\n    \"add-your-comment-here\": \"เพิ่มความคิดเห็นของคุณที่นี่...\",\n    \"any-thoughts\": \"ความคิดใดๆ...\",\n    \"exit-focus-mode\": \"ออกจากโหมดโฟกัส\",\n    \"focus-mode\": \"โหมดโฟกัส\",\n    \"no-changes-detected\": \"ไม่มีการเปลี่ยนแปลง\",\n    \"save\": \"บันทึก\",\n    \"saving\": \"กำลังบันทึก...\",\n    \"slash-commands\": \"พิมพ์ `/` สำหรับคำสั่ง\"\n  },\n  \"inbox\": {\n    \"failed-to-load\": \"ไม่สามารถโหลดรายการกล่องจดหมายได้\",\n    \"memo-comment\": \"{{user}} มีความคิดเห็นเกี่ยวกับ {{memo}} ของคุณ\",\n    \"no-archived\": \"ไม่มีการแจ้งเตือนที่เก็บถาวร\",\n    \"no-unread\": \"ไม่มีการแจ้งเตือนที่ยังไม่อ่าน\",\n    \"unread\": \"ยังไม่อ่าน\"\n  },\n  \"markdown\": {\n    \"checkbox\": \"กล่องกาเครื่องหมาย\",\n    \"code-block\": \"บล็อกโค้ด\",\n    \"content-syntax\": \"ไวยากรณ์เนื้อหา\"\n  },\n  \"memo\": {\n    \"archived-at\": \"เก็บถาวรไว้ที่\",\n    \"click-to-hide-nsfw-content\": \"คลิกเพื่อซ่อนเนื้อหาไม่เหมาะสม (NSFW)\",\n    \"click-to-show-nsfw-content\": \"คลิกเพื่อแสดงเนื้อหาไม่เหมาะสม (NSFW)\",\n    \"code\": \"โค้ด\",\n    \"comment\": {\n      \"self\": \"ความคิดเห็น\",\n      \"write-a-comment\": \"เขียนความคิดเห็น\"\n    },\n    \"copy-content\": \"คัดลอกเนื้อหา\",\n    \"copy-link\": \"คัดลอกลิงก์\",\n    \"count-memos-in-date\": \"{{count}} {{memos}} ใน {{date}}\",\n    \"delete-confirm\": \"คุณแน่ใจหรือไม่ ว่าต้องการลบบันทึกนี้? การกระทำนี้ไม่สามารถย้อนกลับได้\",\n    \"delete-confirm-description\": \"การดำเนินการนี้ไม่สามารถย้อนกลับได้ ไฟล์แนบ ลิงก์ และการอ้างอิงจะถูกลบออกด้วย\",\n    \"direction\": \"ทิศทาง\",\n    \"direction-asc\": \"จากน้อยไปมาก\",\n    \"direction-desc\": \"จากมากไปน้อย\",\n    \"display-time\": \"แสดงเวลา\",\n    \"filters\": {\n      \"has-code\": \"มีโค้ด\",\n      \"has-link\": \"มีลิงก์\",\n      \"has-task-list\": \"มีรายการที่ต้องทำ\"\n    },\n    \"links\": \"ลิงก์\",\n    \"load-more\": \"โหลดเพิ่มเติม\",\n    \"no-archived-memos\": \"ไม่มีบันทึกช่วยจำที่ถูกเก็บถาวร\",\n    \"no-memos\": \"ไม่มีบันทึกช่วยจำ\",\n    \"order-by\": \"เรียงตาม\",\n    \"search-placeholder\": \"ค้นหาบันทึกช่วยจำ...\",\n    \"show-less\": \"แสดงน้อยลง\",\n    \"show-more\": \"แสดงเพิ่มเติม\",\n    \"to-do\": \"สิ่งที่ต้องทำ\",\n    \"view-detail\": \"ดูรายละเอียด\",\n    \"visibility\": {\n      \"disabled\": \"บันทึกช่วยจำสาธารณะถูกปิดการใช้งาน\",\n      \"private\": \"ส่วนตัว\",\n      \"protected\": \"พื้นที่ทำงาน\",\n      \"public\": \"สาธารณะ\"\n    }\n  },\n  \"message\": {\n    \"archived-successfully\": \"เก็บถาวรเรียบร้อยแล้ว\",\n    \"change-memo-created-time\": \"เปลี่ยนเวลาที่สร้างบันทึกช่วยจำ\",\n    \"copied\": \"คัดลอกแล้ว\",\n    \"deleted-successfully\": \"ลบเรียบร้อยแล้ว\",\n    \"description-is-required\": \"ต้องระบุคำอธิบาย\",\n    \"failed-to-embed-memo\": \"ฝังบันทึกช่วยจำไม่สำเร็จ\",\n    \"fill-all\": \"กรุณากรอกข้อมูลให้ครบทุกช่อง\",\n    \"fill-all-required-fields\": \"กรุณากรอกข้อมูลที่จำเป็นให้ครบ\",\n    \"maximum-upload-size-is\": \"ขนาดการอัปโหลดสูงสุดที่อนุญาตคือ {{size}} MiB\",\n    \"memo-not-found\": \"ไม่พบบันทึกช่วยจำ\",\n    \"new-password-not-match\": \"รหัสผ่านใหม่ไม่ตรงกัน\",\n    \"no-data\": \"ไม่พบข้อมูล\",\n    \"password-changed\": \"เปลี่ยนรหัสผ่านแล้ว\",\n    \"password-not-match\": \"รหัสผ่านไม่ตรงกัน\",\n    \"restored-successfully\": \"กู้คืนเรียบร้อยแล้ว\",\n    \"succeed-copy-content\": \"คัดลอกเนื้อหาเรียบร้อยแล้ว\",\n    \"succeed-copy-link\": \"คัดลอกลิงก์เรียบร้อยแล้ว\",\n    \"update-succeed\": \"อัปเดตเรียบร้อยแล้ว\",\n    \"user-not-found\": \"ไม่พบผู้ใช้งาน\"\n  },\n  \"reference\": {\n    \"add-references\": \"เพิ่มข้อมูลอ้างอิง\",\n    \"embedded-usage\": \"ใช้เป็นเนื้อหาแบบฝัง\",\n    \"no-memos-found\": \"ไม่พบบันทึกช่วยจำ\",\n    \"search-placeholder\": \"ค้นหาเนื้อหา\"\n  },\n  \"resource\": {\n    \"clear\": \"ล้าง\",\n    \"copy-link\": \"คัดลอกลิงก์\",\n    \"create-dialog\": {\n      \"external-link\": {\n        \"file-name\": \"ชื่อไฟล์\",\n        \"file-name-placeholder\": \"ชื่อไฟล์\",\n        \"link\": \"ลิงก์\",\n        \"link-placeholder\": \"https://ลิงก์.ไปยัง/ทรัพยากร/ของคุณ\",\n        \"option\": \"ลิงก์ภายนอก\",\n        \"type\": \"ประเภท\",\n        \"type-placeholder\": \"ชนิดไฟล์\"\n      },\n      \"local-file\": {\n        \"choose\": \"เลือกไฟล์…\",\n        \"option\": \"ไฟล์ในเครื่อง\"\n      },\n      \"title\": \"สร้างทรัพยากร\",\n      \"upload-method\": \"วิธีการอัปโหลด\"\n    },\n    \"delete-all-unused\": \"ลบทรัพยากรที่ไม่ได้ใช้ทั้งหมด\",\n    \"delete-all-unused-confirm\": \"คุณแน่ใจหรือไม่ที่จะลบทรัพยากรที่ไม่ได้ใช้ทั้งหมด? การกระทำนี้ไม่สามารถย้อนกลับได้\",\n    \"delete-all-unused-error\": \"ไม่สามารถลบทรัพยากรที่ไม่ได้ใช้ได้\",\n    \"delete-all-unused-success\": \"ลบทรัพยากรเรียบร้อยแล้ว\",\n    \"delete-resource\": \"ลบทรัพยากร\",\n    \"delete-selected-resources\": \"ลบทรัพยากรที่เลือก\",\n    \"fetching-data\": \"กำลังเรียกข้อมูล…\",\n    \"file-drag-drop-prompt\": \"ลากและวางไฟล์ของคุณที่นี่เพื่ออัปโหลดไฟล์\",\n    \"linked-amount\": \"จำนวนที่ผูกไว้\",\n    \"no-files-selected\": \"ไม่มีไฟล์ที่ถูกเลือก\",\n    \"no-resources\": \"ไม่มีทรัพยากร\",\n    \"no-unused-resources\": \"ไม่มีทรัพยากรที่ไม่ได้ใช้\",\n    \"reset-link\": \"รีเซ็ตลิงก์\",\n    \"reset-link-prompt\": \"คุณแน่ใจหรือว่าจะรีเซ็ตลิงก์? การดำเนินการนี้จะทำลายการใช้งานลิงก์ปัจจุบันทั้งหมด การกระทำนี้ไม่สามารถย้อนกลับได้\",\n    \"reset-resource-link\": \"รีเซ็ตลิงก์ทรัพยากร\",\n    \"unused-resources\": \"ทรัพยากรที่ไม่ได้ใช้\"\n  },\n  \"router\": {\n    \"back-to-top\": \"กลับสู่ด้านบน\",\n    \"go-to-home\": \"ไปที่หน้าหลัก\"\n  },\n  \"setting\": {\n    \"member\": {\n      \"admin\": \"ผู้ดูแล\",\n      \"archive-member\": \"เก็บถาวรสมาชิก\",\n      \"archive-success\": \"{{username}} เก็บถาวรเรียบร้อยแล้ว\",\n      \"archive-warning\": \"คุณแน่ใจหรือว่าจะเก็บถาวร {{username}}?\",\n      \"archive-warning-description\": \"การเก็บถาวรจะปิดใช้งานบัญชี คุณสามารถกู้คืนหรือลบได้ภายหลัง\",\n      \"create-a-member\": \"สร้างสมาชิก\",\n      \"delete-member\": \"ลบสมาชิก\",\n      \"delete-success\": \"{{username}} ลบเรียบร้อยแล้ว\",\n      \"delete-warning\": \"คุณแน่ใจหรือว่าจะลบ {{username}}? การกระทำนี้ไม่สามารถย้อนกลับได้\",\n      \"delete-warning-description\": \"การกระทำนี้ไม่สามารถย้อนกลับได้\",\n      \"restore-success\": \"{{username}} กู้คืนเรียบร้อยแล้ว\",\n      \"user\": \"ผู้ใช้\",\n      \"label\": \"สมาชิก\",\n      \"list-title\": \"รายชื่อสมาชิก\"\n    },\n    \"my-account\": {\n      \"label\": \"บัญชีผู้ใช้ของฉัน\"\n    },\n    \"preference\": {\n      \"default-memo-sort-option\": \"เวลาแสดงบันทึกช่วยจำ\",\n      \"default-memo-visibility\": \"การมองเห็นบันทึกเริ่มต้น\",\n      \"theme\": \"ธีม\",\n      \"label\": \"การตั้งค่า\"\n    },\n    \"shortcut\": {\n      \"delete-confirm\": \"คุณแน่ใจหรือไม่ที่จะลบทางลัด `{{title}}`?\",\n      \"delete-success\": \"ทางลัด `{{title}}` ลบเรียบร้อยแล้ว\"\n    },\n    \"sso\": {\n      \"authorization-endpoint\": \"Endpoint การอนุญาต\",\n      \"client-id\": \"รหัสไคลเอ็นต์\",\n      \"client-secret\": \"ความลับไคลเอ็นต์\",\n      \"confirm-delete\": \"คุณแน่ใจหรือไม่ว่าจะลบการกำหนดค่า SSO \\\"{{name}}\\\" การกระทำนี้ไม่สามารถย้อนกลับได้\",\n      \"create-sso\": \"สร้าง SSO\",\n      \"custom\": \"ปรับแต่ง\",\n      \"delete-sso\": \"ยืนยันการลบ\",\n      \"disabled-password-login-warning\": \"การเข้าสู่ระบบด้วยรหัสผ่านถูกปิดใช้งาน โปรดระมัดระวังเป็นพิเศษเมื่อลบผู้ให้บริการข้อมูลประจำตัว\",\n      \"display-name\": \"ชื่อที่แสดง\",\n      \"identifier\": \"ตัวระบุ\",\n      \"identifier-filter\": \"ตัวกรอง Identifier\",\n      \"no-sso-found\": \"ไม่พบ SSO\",\n      \"redirect-url\": \"เปลี่ยนเส้นทาง URL\",\n      \"scopes\": \"ขอบเขต\",\n      \"single-sign-on\": \"กำลังตั้งค่า Single Sign-On (SSO) สำหรับการยืนยันตัวตน\",\n      \"sso-created\": \"SSO {{name}} สร้างแล้ว\",\n      \"sso-list\": \"รายการ SSO\",\n      \"sso-updated\": \"SSO {{name}} อัปเดตแล้ว\",\n      \"template\": \"เทมเพลต\",\n      \"token-endpoint\": \"Endpoint โทเค็น\",\n      \"update-sso\": \"อัปเดต SSO\",\n      \"user-endpoint\": \"Endpoint ผู้ใช้\",\n      \"label\": \"SSO\"\n    },\n    \"storage\": {\n      \"accesskey\": \"คีย์การเข้าถึง\",\n      \"accesskey-placeholder\": \"คีย์การเข้าถึง / รหัสการเข้าถึง\",\n      \"bucket\": \"บักเก็ต\",\n      \"bucket-placeholder\": \"ชื่อ Bucket\",\n      \"create-a-service\": \"สร้างบริการ\",\n      \"create-storage\": \"สร้างพื้นที่จัดเก็บข้อมูล\",\n      \"current-storage\": \"พื้นที่ object storage ปัจจุบัน\",\n      \"delete-storage\": \"ลบพื้นที่เก็บข้อมูล\",\n      \"endpoint\": \"จุดปลายทาง\",\n      \"filepath-template\": \"แม่แบบเส้นทางไฟล์\",\n      \"local-storage-path\": \"เส้นทางการจัดเก็บในเครื่อง\",\n      \"path\": \"ที่จัดเก็บ\",\n      \"path-description\": \"คุณสามารถใช้ตัวแปรไดนามิกเดียวกันจากที่จัดเก็บในตัวเครื่อง เช่น {filename}\",\n      \"path-placeholder\": \"กำหนดเอง/เส้นทาง\",\n      \"presign-placeholder\": \"Pre-sign URL ไม่บังคับ\",\n      \"region\": \"ภูมิภาค\",\n      \"region-placeholder\": \"ชื่อภูมิภาค\",\n      \"s3-compatible-url\": \"URL ที่เข้ากันได้กับ S3\",\n      \"secretkey\": \"คีย์ลับ\",\n      \"secretkey-placeholder\": \"คีย์ลับ / คีย์การเข้าถึง\",\n      \"storage-services\": \"บริการจัดเก็บข้อมูล\",\n      \"type-database\": \"ฐานข้อมูล\",\n      \"type-local\": \"ระบบไฟล์ในเครื่อง\",\n      \"update-a-service\": \"อัปเดตบริการ\",\n      \"update-local-path\": \"อัปเดตเส้นทางการจัดเก็บในเครื่อง\",\n      \"update-local-path-description\": \"เส้นทางการจัดเก็บข้อมูลในเครื่องเป็นเส้นทางสัมพันธ์กับไฟล์ฐานข้อมูลของคุณ\",\n      \"update-storage\": \"อัปเดตที่เก็บข้อมูล\",\n      \"url-prefix\": \"คำนำหน้า URL\",\n      \"url-prefix-placeholder\": \"URL prefix ที่กำหนดเอง ไม่บังคับ\",\n      \"url-suffix\": \"คำต่อท้าย URL\",\n      \"url-suffix-placeholder\": \"URL suffix ที่กำหนดเอง (ไม่บังคับ)\",\n      \"warning-text\": \"คุณแน่ใจหรือว่าจะลบบริการพื้นที่เก็บข้อมูล \\\"{{name}}\\\"? การกระทำนี้ไม่สามารถย้อนกลับได้\",\n      \"label\": \"พื้นที่จัดเก็บ\"\n    },\n    \"system\": {\n      \"additional-script\": \"สคริปต์เพิ่มเติม\",\n      \"additional-script-placeholder\": \"โค้ดจาวาสคริปต์เพิ่มเติม\",\n      \"additional-style\": \"สไตล์เพิ่มเติม\",\n      \"additional-style-placeholder\": \"โค้ด CSS เพิ่มเติม\",\n      \"allow-user-signup\": \"อนุญาตให้ผู้ใช้ลงทะเบียน\",\n      \"customize-server\": {\n        \"description\": \"คำอธิบาย\",\n        \"icon-url\": \"URL ไอคอน\",\n        \"locale\": \"ตำแหน่งเซิร์ฟเวอร์\",\n        \"title\": \"ปรับแต่งเซิร์ฟเวอร์\"\n      },\n      \"disable-password-login\": \"ปิดการใช้งานรหัสผ่านเข้าสู่ระบบ\",\n      \"disable-password-login-final-warning\": \"กรุณาพิมพ์ \\\"CONFIRM\\\" หากคุณรู้ว่าคุณกำลังทำอะไรอยู่\",\n      \"disable-password-login-warning\": \"สิ่งนี้จะปิดการใช้งานการเข้าสู่ระบบด้วยรหัสผ่านสำหรับผู้ใช้ทั้งหมด ไม่สามารถเข้าสู่ระบบโดยไม่คืนการตั้งค่านี้ในฐานข้อมูลได้ หากผู้ให้บริการข้อมูลประจำตัวที่กำหนดค่าไว้ของคุณล้มเหลว คุณจะต้องระมัดระวังเป็นพิเศษเมื่อลบผู้ให้บริการข้อมูลระบุตัวตน\",\n      \"display-with-updated-time\": \"แสดงผลด้วยเวลาที่อัปเดต\",\n      \"enable-auto-compact\": \"เปิดใช้งานการกะทัดรัดอัตโนมัติ\",\n      \"enable-double-click-to-edit\": \"เปิดใช้งานดับเบิลคลิกเพื่อแก้ไข\",\n      \"enable-password-login\": \"เปิดใช้งานการเข้าสู่ระบบด้วยรหัสผ่าน\",\n      \"enable-password-login-warning\": \"สิ่งนี้จะเปิดใช้งานการเข้าสู่ระบบด้วยรหัสผ่านสำหรับผู้ใช้ทุกคน ดำเนินการต่อหากคุณต้องการให้ผู้ใช้สามารถเข้าสู่ระบบโดยใช้ทั้ง SSO และรหัสผ่าน\",\n      \"max-upload-size\": \"ขนาดการอัปโหลดสูงสุด (MiB)\",\n      \"max-upload-size-hint\": \"ค่าที่แนะนำคือ 32 MiB\",\n      \"removed-completed-task-list-items\": \"เปิดใช้งานการลบรายการที่ทำแล้ว\",\n      \"server-name\": \"ชื่อเซิร์ฟเวอร์\",\n      \"title\": \"ทั่วไป\",\n      \"label\": \"ระบบ\"\n    },\n    \"version\": \"เวอร์ชั่น\",\n    \"access-token\": {\n      \"access-token-copied-to-clipboard\": \"คัดลอกโทเค็นการเข้าถึงไปยังคลิปบอร์ดแล้ว\",\n      \"access-token-deleted\": \"โทเค็นการเข้าถึง `{{description}}` ถูกลบแล้ว\",\n      \"access-token-deletion\": \"คุณแน่ใจหรือว่าจะลบโทเค็นการเข้าถึง {{description}}? การกระทำนี้ไม่สามารถย้อนกลับได้\",\n      \"access-token-deletion-description\": \"การกระทำนี้ไม่สามารถย้อนกลับได้ คุณจะต้องอัปเดตบริการใดๆ ที่ใช้โทเค็นนี้เพื่อใช้โทเค็นใหม่\",\n      \"create-dialog\": {\n        \"access-token-created\": \"สร้างโทเค็นการเข้าถึง `{{description}}` เรียบร้อยแล้ว\",\n        \"create-access-token\": \"สร้างโทเค็นการเข้าถึง\",\n        \"created-at\": \"สร้างเมื่อ\",\n        \"description\": \"คำอธิบาย\",\n        \"duration-1m\": \"1 เดือน\",\n        \"duration-8h\": \"8 ชั่วโมง\",\n        \"duration-never\": \"ไม่หมดอายุ\",\n        \"expiration\": \"วันหมดอายุ\",\n        \"expires-at\": \"หมดอายุเมื่อ\",\n        \"some-description\": \"คำอธิบาย...\"\n      },\n      \"description\": \"รายการโทเค็นการเข้าถึงทั้งหมดสำหรับบัญชีของคุณ\",\n      \"title\": \"โทเค็นการเข้าถึง\",\n      \"token\": \"โทเค็น\"\n    },\n    \"account\": {\n      \"change-password\": \"เปลี่ยนรหัสผ่าน\",\n      \"email-note\": \"ไม่บังคับ\",\n      \"export-memos\": \"ส่งออกบันทึกช่วยจำ\",\n      \"nickname-note\": \"แสดงในแบนเนอร์\",\n      \"openapi-reset\": \"รีเซ็ตคีย์ OpenAPI\",\n      \"openapi-sample-post\": \"สวัสดี #บันทึกจาก {{url}}\",\n      \"openapi-title\": \"OpenAPI\",\n      \"reset-api\": \"รีเซ็ต API\",\n      \"title\": \"ข้อมูลเกี่ยวกับบัญชี\",\n      \"update-information\": \"อัปเดทข้อมูล\",\n      \"username-note\": \"ใช้เพื่อเข้าสู่ระบบ\"\n    },\n    \"instance\": {\n      \"disallow-change-nickname\": \"ไม่อนุญาตให้เปลี่ยนชื่อเล่น\",\n      \"disallow-change-username\": \"ไม่อนุญาตให้เปลี่ยนชื่อผู้ใช้\",\n      \"disallow-password-auth\": \"ไม่อนุญาตให้ยืนยันตัวตนด้วยรหัสผ่าน\",\n      \"disallow-user-registration\": \"ไม่อนุญาตให้ลงทะเบียนผู้ใช้\",\n      \"monday\": \"วันจันทร์\",\n      \"saturday\": \"วันเสาร์\",\n      \"sunday\": \"วันอาทิตย์\",\n      \"week-start-day\": \"วันเริ่มต้นสัปดาห์\"\n    },\n    \"memo\": {\n      \"content-length-limit\": \"จำกัดความยาวเนื้อหา (ไบต์)\",\n      \"enable-blur-nsfw-content\": \"เปิดใช้งานการเบลอเนื้อหาไม่เหมาะสม (NSFW)\",\n      \"enable-memo-comments\": \"เปิดใช้งานความคิดเห็นในบันทึก\",\n      \"enable-memo-location\": \"เปิดใช้งานตำแหน่งในบันทึก\",\n      \"reactions\": \"ปฏิกิริยา\",\n      \"title\": \"การตั้งค่าที่เกี่ยวข้องกับบันทึก\",\n      \"label\": \"บันทึก\"\n    },\n    \"webhook\": {\n      \"create-dialog\": {\n        \"an-easy-to-remember-name\": \"ชื่อที่จำง่าย\",\n        \"create-webhook\": \"สร้าง webhook\",\n        \"create-webhook-success\": \"สร้าง webhook `{{name}}` เรียบร้อยแล้ว\",\n        \"edit-webhook\": \"แก้ไข webhook\",\n        \"payload-url\": \"URL ข้อมูล\",\n        \"title\": \"ชื่อเรื่อง\",\n        \"url-example-post-receive\": \"https://ตัวอย่าง.com/postreceive\"\n      },\n      \"delete-dialog\": {\n        \"delete-webhook-description\": \"การกระทำนี้ไม่สามารถย้อนกลับได้\",\n        \"delete-webhook-success\": \"ลบ webhook `{{name}}` เรียบร้อยแล้ว\",\n        \"delete-webhook-title\": \"คุณแน่ใจหรือไม่ที่จะลบ webhook `{{name}}`?\"\n      },\n      \"no-webhooks-found\": \"ไม่พบ webhook\",\n      \"title\": \"Webhook\",\n      \"url\": \"URL\"\n    }\n  },\n  \"tag\": {\n    \"all-tags\": \"แท็กทั้งหมด\",\n    \"create-tag\": \"สร้างแท็ก\",\n    \"create-tags-guide\": \"คุณสามารถสร้างแท็กได้โดยป้อน `#tag`\",\n    \"delete-confirm\": \"คุณแน่ใจหรือว่าจะลบแท็กนี้? บันทึกที่เกี่ยวข้องทั้งหมดจะถูกเก็บถาวร\",\n    \"delete-success\": \"ลบแท็กเรียบร้อยแล้ว\",\n    \"delete-tag\": \"ลบแท็ก\",\n    \"new-name\": \"ชื่อใหม่\",\n    \"no-tag-found\": \"ไม่พบแท็ก\",\n    \"old-name\": \"ชื่อเก่า\",\n    \"rename-error-empty\": \"ชื่อแท็กต้องไม่ว่างเปล่าหรือมีช่องว่าง\",\n    \"rename-error-repeat\": \"ชื่อใหม่ต้องไม่เหมือนกับชื่อเดิม\",\n    \"rename-success\": \"เปลี่ยนชื่อแท็กสำเร็จ\",\n    \"rename-tag\": \"เปลี่ยนชื่อแท็ก\",\n    \"rename-tip\": \"บันทึกทั้งหมดที่มีแท็กนี้จะถูกอัปเดต\"\n  },\n  \"tooltip\": {\n    \"link-memo\": \"ลิงก์บันทึกช่วยจำ\",\n    \"markdown-menu\": \"เมนูมาร์กดาวน์\",\n    \"select-location\": \"เลือกตำแหน่ง\",\n    \"select-visibility\": \"เลือกการมองเห็น\",\n    \"tags\": \"ป้ายกำกับ\",\n    \"upload-attachment\": \"อัปโหลดไฟล์แนบ\"\n  }\n}\n"
  },
  {
    "path": "web/src/locales/tr.json",
    "content": "{\n  \"about\": {\n    \"blogs\": \"Bloglar\",\n    \"description\": \"Gizlilik öncelikli, hafif bir not alma servisi. Harika düşüncelerinizi kolayca yakalayın ve paylaşın.\",\n    \"documents\": \"Belgeler\",\n    \"github-repository\": \"GitHub Deposu\",\n    \"official-website\": \"Resmi Web Sitesi\"\n  },\n  \"auth\": {\n    \"create-your-account\": \"Hesabınızı oluşturun\",\n    \"host-tip\": \"Site Yöneticisi olarak kayıt oluyorsunuz.\",\n    \"new-password\": \"Yeni şifre\",\n    \"repeat-new-password\": \"Yeni şifreyi tekrar girin\",\n    \"sign-in-tip\": \"Zaten hesabınız var mı?\",\n    \"sign-up-tip\": \"Henüz hesabınız yok mu?\"\n  },\n  \"common\": {\n    \"about\": \"Hakkında\",\n    \"add\": \"Ekle\",\n    \"admin\": \"Yönetici\",\n    \"all\": \"Tümü\",\n    \"archive\": \"Arşiv\",\n    \"archived\": \"Arşivlendi\",\n    \"attachments\": \"Ekler\",\n    \"auto-expand\": \"Otomatik genişlet\",\n    \"avatar\": \"Profil Resmi\",\n    \"basic\": \"Temel\",\n    \"beta\": \"Beta sürümü\",\n    \"calendar\": \"Takvim\",\n    \"cancel\": \"İptal\",\n    \"change\": \"Değiştir\",\n    \"clear\": \"Temizle\",\n    \"close\": \"Kapat\",\n    \"collapse\": \"Daralt\",\n    \"confirm\": \"Onayla\",\n    \"copy\": \"Kopyala\",\n    \"create\": \"Oluştur\",\n    \"created-at\": \"Oluşturulma Tarihi\",\n    \"database\": \"Veritabanı\",\n    \"day\": \"Gün\",\n    \"days\": {\n      \"fri\": \"Cum\",\n      \"mon\": \"Pzt\",\n      \"sat\": \"Cmt\",\n      \"sun\": \"Paz\",\n      \"thu\": \"Per\",\n      \"tue\": \"Sal\",\n      \"wed\": \"Çar\"\n    },\n    \"delete\": \"Sil\",\n    \"description\": \"Açıklama\",\n    \"edit\": \"Düzenle\",\n    \"email\": \"E-posta\",\n    \"expand\": \"Genişlet\",\n    \"explore\": \"Keşfet\",\n    \"file\": \"Dosya\",\n    \"filter\": \"Filtrele\",\n    \"home\": \"Ana Sayfa\",\n    \"image\": \"Görsel\",\n    \"in\": \"İçinde\",\n    \"inbox\": \"Gelen Kutusu\",\n    \"input\": \"Girdi\",\n    \"language\": \"Dil\",\n    \"last-updated-at\": \"Son güncelleme\",\n    \"learn-more\": \"Daha fazla bilgi\",\n    \"link\": \"Bağlantı\",\n    \"map\": \"Harita\",\n    \"mark\": \"İşaretle\",\n    \"memo\": \"Not\",\n    \"memos\": \"Notlar\",\n    \"more\": \"Daha fazla\",\n    \"name\": \"İsim\",\n    \"new\": \"Yeni\",\n    \"nickname\": \"Takma Ad\",\n    \"null\": \"Boş\",\n    \"or\": \"veya\",\n    \"password\": \"Şifre\",\n    \"pin\": \"Sabitle\",\n    \"pinned\": \"Sabitlendi\",\n    \"preview\": \"Önizleme\",\n    \"profile\": \"Profil\",\n    \"properties\": \"Özellikler\",\n    \"referenced-by\": \"Referanslayan\",\n    \"referencing\": \"Referanslanan\",\n    \"relations\": \"İlişkiler\",\n    \"remember-me\": \"Beni hatırla\",\n    \"rename\": \"Yeniden adlandır\",\n    \"reset\": \"Sıfırla\",\n    \"resources\": \"Kaynaklar\",\n    \"restore\": \"Geri yükle\",\n    \"role\": \"Rol\",\n    \"save\": \"Kaydet\",\n    \"search\": \"Ara\",\n    \"select\": \"Seç\",\n    \"settings\": \"Ayarlar\",\n    \"share\": \"Paylaş\",\n    \"shortcut-filter\": \"Kısayol filtresi\",\n    \"shortcuts\": \"Kısayollar\",\n    \"sign-in\": \"Giriş yap\",\n    \"sign-in-with\": \"{{provider}} ile giriş yap\",\n    \"sign-out\": \"Çıkış yap\",\n    \"sign-up\": \"Kayıt ol\",\n    \"statistics\": \"İstatistikler\",\n    \"tags\": \"Etiketler\",\n    \"title\": \"Başlık\",\n    \"today\": \"Bugün\",\n    \"tree-mode\": \"Ağaç modu\",\n    \"type\": \"Tür\",\n    \"unpin\": \"Sabitlemeyi kaldır\",\n    \"update\": \"Güncelle\",\n    \"upload\": \"Yükle\",\n    \"user\": \"Kullanıcı\",\n    \"username\": \"Kullanıcı adı\",\n    \"version\": \"Sürüm\",\n    \"visibility\": \"Görünürlük\",\n    \"yourself\": \"Kendiniz\"\n  },\n  \"editor\": {\n    \"add-your-comment-here\": \"Yorumunuzu buraya ekleyin...\",\n    \"any-thoughts\": \"Düşünceleriniz...\",\n    \"exit-focus-mode\": \"Odak modundan çık\",\n    \"focus-mode\": \"Odak modu\",\n    \"no-changes-detected\": \"Değişiklik yok\",\n    \"save\": \"Kaydet\",\n    \"saving\": \"Kaydediliyor...\",\n    \"slash-commands\": \"Komutlar için `/` yazın\"\n  },\n  \"inbox\": {\n    \"failed-to-load\": \"Gelen kutusu öğesi yüklenemedi\",\n    \"memo-comment\": \"{{user}} {{memo}} notunuza yorum yaptı.\",\n    \"no-archived\": \"Arşivlenmiş bildirim yok\",\n    \"no-unread\": \"Okunmamış bildirim yok\",\n    \"unread\": \"Okunmamış\"\n  },\n  \"markdown\": {\n    \"checkbox\": \"Onay kutusu\",\n    \"code-block\": \"Kod bloğu\",\n    \"content-syntax\": \"İçerik sözdizimi\"\n  },\n  \"memo\": {\n    \"archived-at\": \"Arşivlenme tarihi\",\n    \"click-to-hide-nsfw-content\": \"NSFW içeriği gizlemek için tıklayın\",\n    \"click-to-show-nsfw-content\": \"NSFW içeriği göstermek için tıklayın\",\n    \"code\": \"Kod\",\n    \"comment\": {\n      \"self\": \"Yorumlar\",\n      \"write-a-comment\": \"Yorum yaz\"\n    },\n    \"copy-content\": \"İçeriği kopyala\",\n    \"copy-link\": \"Bağlantıyı Kopyala\",\n    \"count-memos-in-date\": \"{{date}} tarihinde {{count}} {{memos}}\",\n    \"delete-confirm\": \"Bu notu silmek istediğinizden emin misiniz? BU İŞLEM GERİ ALINAMAZ\",\n    \"delete-confirm-description\": \"Bu işlem geri alınamaz. Ekler, bağlantılar ve referanslar da kaldırılacaktır.\",\n    \"direction\": \"Yön\",\n    \"direction-asc\": \"Artan\",\n    \"direction-desc\": \"Azalan\",\n    \"display-time\": \"Görüntüleme Zamanı\",\n    \"filters\": {\n      \"has-code\": \"kodVar\",\n      \"has-link\": \"bağlantıVar\",\n      \"has-task-list\": \"görevListesiVar\"\n    },\n    \"links\": \"Bağlantılar\",\n    \"load-more\": \"Daha fazla yükle\",\n    \"no-archived-memos\": \"Arşivlenmiş not yok.\",\n    \"no-memos\": \"Not yok.\",\n    \"order-by\": \"Sıralama Ölçütü\",\n    \"search-placeholder\": \"Notlarda ara\",\n    \"show-less\": \"Daha az göster\",\n    \"show-more\": \"Daha fazla göster\",\n    \"to-do\": \"Yapılacaklar\",\n    \"view-detail\": \"Detayları Görüntüle\",\n    \"visibility\": {\n      \"disabled\": \"Herkese açık notlar devre dışı\",\n      \"private\": \"Özel\",\n      \"protected\": \"Çalışma Alanı\",\n      \"public\": \"Herkese Açık\"\n    }\n  },\n  \"message\": {\n    \"archived-successfully\": \"Başarıyla arşivlendi\",\n    \"change-memo-created-time\": \"Not oluşturma zamanını değiştir\",\n    \"copied\": \"Kopyalandı\",\n    \"deleted-successfully\": \"Başarıyla silindi\",\n    \"description-is-required\": \"Açıklama gerekli\",\n    \"failed-to-embed-memo\": \"Not gömme işlemi başarısız\",\n    \"fill-all\": \"Lütfen tüm alanları doldurun.\",\n    \"fill-all-required-fields\": \"Lütfen tüm gerekli alanları doldurun\",\n    \"maximum-upload-size-is\": \"Maksimum yükleme boyutu {{size}} MiB\",\n    \"memo-not-found\": \"Not bulunamadı.\",\n    \"new-password-not-match\": \"Yeni şifreler eşleşmiyor.\",\n    \"no-data\": \"Veri bulunamadı.\",\n    \"password-changed\": \"Şifre Değiştirildi\",\n    \"password-not-match\": \"Şifreler eşleşmiyor.\",\n    \"restored-successfully\": \"Başarıyla geri yüklendi\",\n    \"succeed-copy-content\": \"İçerik başarıyla kopyalandı.\",\n    \"succeed-copy-link\": \"Bağlantı başarıyla kopyalandı.\",\n    \"update-succeed\": \"Güncelleme başarılı\",\n    \"user-not-found\": \"Kullanıcı bulunamadı\"\n  },\n  \"reference\": {\n    \"add-references\": \"Referans ekle\",\n    \"embedded-usage\": \"Gömülü İçerik Olarak Kullan\",\n    \"no-memos-found\": \"Not bulunamadı\",\n    \"search-placeholder\": \"İçerik ara\"\n  },\n  \"resource\": {\n    \"clear\": \"Temizle\",\n    \"copy-link\": \"Bağlantıyı Kopyala\",\n    \"create-dialog\": {\n      \"external-link\": {\n        \"file-name\": \"Dosya adı\",\n        \"file-name-placeholder\": \"Dosya adı\",\n        \"link\": \"Bağlantı\",\n        \"link-placeholder\": \"https://kaynaginizin.baglantisi/dosyaniza\",\n        \"option\": \"Harici bağlantı\",\n        \"type\": \"Tür\",\n        \"type-placeholder\": \"Dosya türü\"\n      },\n      \"local-file\": {\n        \"choose\": \"Bir dosya seçin...\",\n        \"option\": \"Yerel dosya\"\n      },\n      \"title\": \"Kaynak Oluştur\",\n      \"upload-method\": \"Yükleme yöntemi\"\n    },\n    \"delete-all-unused\": \"Kullanılmayanların hepsini sil\",\n    \"delete-all-unused-confirm\": \"Kullanılmayan tüm kaynakları silmek istediğinizden emin misiniz? BU İŞLEM GERİ ALINAMAZ\",\n    \"delete-all-unused-error\": \"Kullanılmayan kaynaklar silinemedi\",\n    \"delete-all-unused-success\": \"Kaynaklar başarıyla silindi\",\n    \"delete-resource\": \"Kaynağı Sil\",\n    \"delete-selected-resources\": \"Seçili Kaynakları Sil\",\n    \"fetching-data\": \"Veriler alınıyor…\",\n    \"file-drag-drop-prompt\": \"Dosya yüklemek için dosyanızı buraya sürükleyip bırakın\",\n    \"linked-amount\": \"Bağlantı sayısı\",\n    \"no-files-selected\": \"Dosya seçilmedi\",\n    \"no-resources\": \"Kaynak yok.\",\n    \"no-unused-resources\": \"Kullanılmayan kaynak yok\",\n    \"reset-link\": \"Bağlantıyı Sıfırla\",\n    \"reset-link-prompt\": \"Bağlantıyı sıfırlamak istediğinizden emin misiniz? Bu işlem mevcut tüm bağlantı kullanımlarını bozacaktır. BU İŞLEM GERİ ALINAMAZ\",\n    \"reset-resource-link\": \"Kaynak Bağlantısını Sıfırla\",\n    \"unused-resources\": \"Kullanılmayan kaynaklar\"\n  },\n  \"router\": {\n    \"back-to-top\": \"Başa Dön\",\n    \"go-to-home\": \"Ana Sayfaya Git\"\n  },\n  \"setting\": {\n    \"member\": {\n      \"admin\": \"Yönetici\",\n      \"archive-member\": \"Üyeyi arşivle\",\n      \"archive-success\": \"{{username}} başarıyla arşivlendi\",\n      \"archive-warning\": \"{{username}} kullanıcısını arşivlemek istediğinizden emin misiniz?\",\n      \"archive-warning-description\": \"Arşivleme hesabı devre dışı bırakır. Daha sonra geri yükleyebilir veya silebilirsiniz.\",\n      \"create-a-member\": \"Üye oluştur\",\n      \"delete-member\": \"Üyeyi Sil\",\n      \"delete-success\": \"{{username}} başarıyla silindi\",\n      \"delete-warning\": \"{{username}} kullanıcısını silmek istediğinizden emin misiniz? BU İŞLEM GERİ ALINAMAZ\",\n      \"delete-warning-description\": \"BU İŞLEM GERİ ALINAMAZ\",\n      \"restore-success\": \"{{username}} başarıyla geri yüklendi\",\n      \"user\": \"Kullanıcı\",\n      \"label\": \"Üye\",\n      \"list-title\": \"Üye listesi\"\n    },\n    \"my-account\": {\n      \"label\": \"Hesabım\"\n    },\n    \"preference\": {\n      \"default-memo-sort-option\": \"Not görüntüleme zamanı\",\n      \"default-memo-visibility\": \"Varsayılan not görünürlüğü\",\n      \"theme\": \"Tema\",\n      \"label\": \"Tercihler\"\n    },\n    \"shortcut\": {\n      \"delete-confirm\": \"`{{title}}` kısayolunu silmek istediğinizden emin misiniz?\",\n      \"delete-success\": \"`{{title}}` kısayolu başarıyla silindi\"\n    },\n    \"sso\": {\n      \"authorization-endpoint\": \"Yetkilendirme uç noktası\",\n      \"client-id\": \"İstemci Kimliği\",\n      \"client-secret\": \"İstemci Gizli Anahtarı\",\n      \"confirm-delete\": \"\\\"{{name}}\\\" SSO yapılandırmasını silmek istediğinizden emin misiniz? BU İŞLEM GERİ ALINAMAZ\",\n      \"create-sso\": \"SSO Oluştur\",\n      \"custom\": \"Özel\",\n      \"delete-sso\": \"Silmeyi onayla\",\n      \"disabled-password-login-warning\": \"Şifre ile giriş devre dışı bırakıldı, kimlik sağlayıcıları kaldırırken çok dikkatli olun\",\n      \"display-name\": \"Görünen Ad\",\n      \"identifier\": \"Tanımlayıcı\",\n      \"identifier-filter\": \"Tanımlayıcı Filtresi\",\n      \"no-sso-found\": \"SSO bulunamadı.\",\n      \"redirect-url\": \"Yönlendirme URL'si\",\n      \"scopes\": \"Kapsamlar\",\n      \"single-sign-on\": \"Kimlik Doğrulama için Tek Oturum Açma (SSO) Yapılandırması\",\n      \"sso-created\": \"SSO {{name}} oluşturuldu\",\n      \"sso-list\": \"SSO Listesi\",\n      \"sso-updated\": \"SSO {{name}} güncellendi\",\n      \"template\": \"Şablon\",\n      \"token-endpoint\": \"Token uç noktası\",\n      \"update-sso\": \"SSO'yu Güncelle\",\n      \"user-endpoint\": \"Kullanıcı uç noktası\",\n      \"label\": \"SSO\"\n    },\n    \"storage\": {\n      \"accesskey\": \"Erişim anahtarı\",\n      \"accesskey-placeholder\": \"Erişim anahtarı / Erişim Kimliği\",\n      \"bucket\": \"Kova\",\n      \"bucket-placeholder\": \"Bucket adı\",\n      \"create-a-service\": \"Servis oluştur\",\n      \"create-storage\": \"Depolama Oluştur\",\n      \"current-storage\": \"Mevcut nesne depolama\",\n      \"delete-storage\": \"Depolamayı Sil\",\n      \"endpoint\": \"Uç nokta\",\n      \"filepath-template\": \"Dosya yolu şablonu\",\n      \"local-storage-path\": \"Yerel depolama yolu\",\n      \"path\": \"Depolama Yolu\",\n      \"path-description\": \"Yerel depolamadaki gibi {filename} gibi aynı dinamik değişkenleri kullanabilirsiniz\",\n      \"path-placeholder\": \"özel/yol\",\n      \"presign-placeholder\": \"Ön imzalı URL, isteğe bağlı\",\n      \"region\": \"Bölge\",\n      \"region-placeholder\": \"Bölge adı\",\n      \"s3-compatible-url\": \"S3 Uyumlu URL\",\n      \"secretkey\": \"Gizli anahtar\",\n      \"secretkey-placeholder\": \"Gizli anahtar / Erişim Anahtarı\",\n      \"storage-services\": \"Depolama servisleri\",\n      \"type-database\": \"Veritabanı\",\n      \"type-local\": \"Yerel dosya sistemi\",\n      \"update-a-service\": \"Servisi güncelle\",\n      \"update-local-path\": \"Yerel Depolama Yolunu Güncelle\",\n      \"update-local-path-description\": \"Yerel depolama yolu, veritabanı dosyanıza göre göreceli bir yoldur\",\n      \"update-storage\": \"Depolamayı Güncelle\",\n      \"url-prefix\": \"URL öneki\",\n      \"url-prefix-placeholder\": \"Özel URL öneki, isteğe bağlı\",\n      \"url-suffix\": \"URL soneki\",\n      \"url-suffix-placeholder\": \"Özel URL soneki, isteğe bağlı\",\n      \"warning-text\": \"\\\"{{name}}\\\" depolama servisini silmek istediğinizden emin misiniz? BU İŞLEM GERİ ALINAMAZ\",\n      \"label\": \"Depolama\"\n    },\n    \"system\": {\n      \"additional-script\": \"Ek script\",\n      \"additional-script-placeholder\": \"Ek JavaScript kodu\",\n      \"additional-style\": \"Ek stil\",\n      \"additional-style-placeholder\": \"Ek CSS kodu\",\n      \"allow-user-signup\": \"Kullanıcı kaydına izin ver\",\n      \"customize-server\": {\n        \"description\": \"Açıklama\",\n        \"icon-url\": \"İkon URL'si\",\n        \"locale\": \"Sunucu Yerel Ayarı\",\n        \"title\": \"Sunucuyu Özelleştir\"\n      },\n      \"disable-password-login\": \"Şifre ile girişi devre dışı bırak\",\n      \"disable-password-login-final-warning\": \"Ne yaptığınızı biliyorsanız lütfen \\\"CONFIRM\\\" yazın.\",\n      \"disable-password-login-warning\": \"Bu, tüm kullanıcılar için şifre ile girişi devre dışı bırakacaktır. Yapılandırılmış kimlik sağlayıcılarınız başarısız olursa, veritabanında bu ayarı geri almadan giriş yapmak mümkün olmayacaktır. Ayrıca bir kimlik sağlayıcıyı kaldırırken çok dikkatli olmanız gerekecektir\",\n      \"display-with-updated-time\": \"Güncellenme zamanıyla görüntüle\",\n      \"enable-auto-compact\": \"Otomatik sıkıştırmayı etkinleştir\",\n      \"enable-double-click-to-edit\": \"Düzenlemek için çift tıklamayı etkinleştir\",\n      \"enable-password-login\": \"Şifre ile girişi etkinleştir\",\n      \"enable-password-login-warning\": \"Bu, tüm kullanıcılar için şifre ile girişi etkinleştirecektir. Yalnızca kullanıcıların hem SSO hem de şifre kullanarak giriş yapmasını istiyorsanız devam edin\",\n      \"max-upload-size\": \"Maksimum yükleme boyutu (MiB)\",\n      \"max-upload-size-hint\": \"Önerilen değer 32 MiB'dir.\",\n      \"removed-completed-task-list-items\": \"Tamamlanan görev listesi öğelerinin kaldırılmasını etkinleştir\",\n      \"server-name\": \"Sunucu Adı\",\n      \"title\": \"Genel\",\n      \"label\": \"Sistem\"\n    },\n    \"version\": \"Sürüm\",\n    \"access-token\": {\n      \"access-token-copied-to-clipboard\": \"Erişim tokeni panoya kopyalandı\",\n      \"access-token-deleted\": \"`{{description}}` erişim tokeni silindi\",\n      \"access-token-deletion\": \"{{description}} erişim tokenini silmek istediğinizden emin misiniz? BU İŞLEM GERİ ALINAMAZ.\",\n      \"access-token-deletion-description\": \"Bu işlem geri alınamaz. Bu tokeni kullanan tüm hizmetleri yeni bir token kullanacak şekilde güncellemeniz gerekecektir.\",\n      \"create-dialog\": {\n        \"access-token-created\": \"`{{description}}` erişim tokeni oluşturuldu\",\n        \"create-access-token\": \"Erişim Tokeni Oluştur\",\n        \"created-at\": \"Oluşturulma Tarihi\",\n        \"description\": \"Açıklama\",\n        \"duration-1m\": \"1 Ay\",\n        \"duration-8h\": \"8 Saat\",\n        \"duration-never\": \"Asla\",\n        \"expiration\": \"Son Kullanma Tarihi\",\n        \"expires-at\": \"Sona Erme Tarihi\",\n        \"some-description\": \"Bir açıklama...\"\n      },\n      \"description\": \"Hesabınız için tüm erişim tokenlerinin listesi.\",\n      \"title\": \"Erişim Tokenleri\",\n      \"token\": \"Token\"\n    },\n    \"account\": {\n      \"change-password\": \"Şifre değiştir\",\n      \"email-note\": \"İsteğe bağlı\",\n      \"export-memos\": \"Notları Dışa Aktar\",\n      \"nickname-note\": \"Banner'da görüntülenir\",\n      \"openapi-reset\": \"OpenAPI Anahtarını Sıfırla\",\n      \"openapi-sample-post\": \"Merhaba #memos, {{url}} adresinden\",\n      \"openapi-title\": \"OpenAPI\",\n      \"reset-api\": \"API'yi Sıfırla\",\n      \"title\": \"Hesap Bilgileri\",\n      \"update-information\": \"Bilgileri Güncelle\",\n      \"username-note\": \"Giriş yapmak için kullanılır\"\n    },\n    \"instance\": {\n      \"disallow-change-nickname\": \"Takma ad değiştirmeyi yasakla\",\n      \"disallow-change-username\": \"Kullanıcı adı değiştirmeyi yasakla\",\n      \"disallow-password-auth\": \"Şifre ile kimlik doğrulamayı yasakla\",\n      \"disallow-user-registration\": \"Kullanıcı kaydını yasakla\",\n      \"monday\": \"Pazartesi\",\n      \"saturday\": \"Cumartesi\",\n      \"sunday\": \"Pazar\",\n      \"week-start-day\": \"Haftanın başlangıç günü\"\n    },\n    \"memo\": {\n      \"content-length-limit\": \"İçerik uzunluğu sınırı (Bayt)\",\n      \"enable-blur-nsfw-content\": \"NSFW içeriği bulanıklaştırmayı etkinleştir\",\n      \"enable-memo-comments\": \"Not yorumlarını etkinleştir\",\n      \"enable-memo-location\": \"Not konumunu etkinleştir\",\n      \"reactions\": \"Tepkiler\",\n      \"title\": \"Not ile ilgili ayarlar\",\n      \"label\": \"Not\"\n    },\n    \"webhook\": {\n      \"create-dialog\": {\n        \"an-easy-to-remember-name\": \"Hatırlaması kolay bir isim\",\n        \"create-webhook\": \"Webhook oluştur\",\n        \"create-webhook-success\": \"`{{name}}` webhook'u oluşturuldu\",\n        \"edit-webhook\": \"Webhook düzenle\",\n        \"payload-url\": \"Payload URL'si\",\n        \"title\": \"Başlık\",\n        \"url-example-post-receive\": \"https://ornnek.com/postreceive\"\n      },\n      \"delete-dialog\": {\n        \"delete-webhook-description\": \"Bu işlem geri alınamaz.\",\n        \"delete-webhook-success\": \"`{{name}}` webhook'u başarıyla silindi\",\n        \"delete-webhook-title\": \"`{{name}}` webhook'unu silmek istediğinizden emin misiniz?\"\n      },\n      \"no-webhooks-found\": \"Webhook bulunamadı.\",\n      \"title\": \"Webhook'lar\",\n      \"url\": \"URL\"\n    }\n  },\n  \"tag\": {\n    \"all-tags\": \"Tüm Etiketler\",\n    \"create-tag\": \"Etiket Oluştur\",\n    \"create-tags-guide\": \"`#etiket` yazarak etiket oluşturabilirsiniz.\",\n    \"delete-confirm\": \"Bu etiketi silmek istediğinizden emin misiniz? İlgili tüm notlar arşivlenecektir.\",\n    \"delete-success\": \"Etiket başarıyla silindi\",\n    \"delete-tag\": \"Etiketi Sil\",\n    \"new-name\": \"Yeni İsim\",\n    \"no-tag-found\": \"Etiket bulunamadı\",\n    \"old-name\": \"Eski İsim\",\n    \"rename-error-empty\": \"Etiket adı boş olamaz veya boşluk içeremez\",\n    \"rename-error-repeat\": \"Yeni isim eski isimle aynı olamaz\",\n    \"rename-success\": \"Etiket başarıyla yeniden adlandırıldı\",\n    \"rename-tag\": \"Etiketi yeniden adlandır\",\n    \"rename-tip\": \"Bu etiketle ilişkili tüm notların etiketini değiştirecektir.\"\n  },\n  \"tooltip\": {\n    \"link-memo\": \"Notu Bağla\",\n    \"markdown-menu\": \"Markdown menüsü\",\n    \"select-location\": \"Konum seç\",\n    \"select-visibility\": \"Görünürlük seç\",\n    \"tags\": \"Etiketler\",\n    \"upload-attachment\": \"Ek(leri) yükle\"\n  }\n}\n"
  },
  {
    "path": "web/src/locales/uk.json",
    "content": "{\n  \"about\": {\n    \"blogs\": \"Блоги\",\n    \"description\": \"Приватний і легкий сервіс для нотаток. Легко записуйте та діліться своїми чудовими думками.\",\n    \"documents\": \"Документи\",\n    \"github-repository\": \"GitHub-репозиторій\",\n    \"official-website\": \"Офіційний вебсайт\"\n  },\n  \"auth\": {\n    \"create-your-account\": \"Створіть обліковий запис\",\n    \"host-tip\": \"Ви реєструєтеся як власник сайту.\",\n    \"new-password\": \"Новий пароль\",\n    \"repeat-new-password\": \"Повторіть новий пароль\",\n    \"sign-in-tip\": \"Вже маєте обліковий запис?\",\n    \"sign-up-tip\": \"Ще не маєте облікового запису?\"\n  },\n  \"common\": {\n    \"about\": \"Про Memos\",\n    \"add\": \"Додати\",\n    \"admin\": \"Адмін\",\n    \"all\": \"Усі\",\n    \"archive\": \"Архів\",\n    \"archived\": \"Архівовано\",\n    \"attachments\": \"Вкладення\",\n    \"auto-expand\": \"Авторозгортання\",\n    \"avatar\": \"Аватар\",\n    \"basic\": \"Основний\",\n    \"beta\": \"Бета-версія\",\n    \"calendar\": \"Календар\",\n    \"cancel\": \"Скасувати\",\n    \"change\": \"Змінити\",\n    \"clear\": \"Очистити\",\n    \"close\": \"Закрити\",\n    \"collapse\": \"Згорнути\",\n    \"confirm\": \"Підтвердити\",\n    \"copy\": \"Копіювати\",\n    \"create\": \"Створити\",\n    \"created-at\": \"Створено\",\n    \"database\": \"База даних\",\n    \"day\": \"День\",\n    \"days\": {\n      \"fri\": \"П'ят.\",\n      \"mon\": \"Пон.\",\n      \"sat\": \"Суб.\",\n      \"sun\": \"Нед.\",\n      \"thu\": \"Чет.\",\n      \"tue\": \"Вів.\",\n      \"wed\": \"Сер.\"\n    },\n    \"delete\": \"Видалити\",\n    \"description\": \"Опис\",\n    \"edit\": \"Редагувати\",\n    \"email\": \"Ел. пошта\",\n    \"expand\": \"Розгорнути\",\n    \"explore\": \"Дослідити\",\n    \"file\": \"Файл\",\n    \"filter\": \"Фільтр\",\n    \"home\": \"Головна\",\n    \"image\": \"Зображення\",\n    \"in\": \"В\",\n    \"inbox\": \"Вхідні\",\n    \"input\": \"Ввід\",\n    \"language\": \"Мова\",\n    \"last-updated-at\": \"Останнє оновлення\",\n    \"learn-more\": \"Дізнатися більше\",\n    \"link\": \"Посилання\",\n    \"map\": \"Мапа\",\n    \"mark\": \"Позначити\",\n    \"memo\": \"Нотатка\",\n    \"memos\": \"Нотатки\",\n    \"more\": \"Більше\",\n    \"name\": \"Назва\",\n    \"new\": \"Новий\",\n    \"nickname\": \"Нікнейм\",\n    \"null\": \"Нуль\",\n    \"or\": \"або\",\n    \"password\": \"Пароль\",\n    \"pin\": \"Закріпити\",\n    \"pinned\": \"Закріплено\",\n    \"preview\": \"Попередній перегляд\",\n    \"profile\": \"Профіль\",\n    \"properties\": \"Властивості\",\n    \"referenced-by\": \"Згадується в\",\n    \"referencing\": \"Згадує\",\n    \"relations\": \"Зв'язки\",\n    \"remember-me\": \"Запам'ятати мене\",\n    \"rename\": \"Перейменувати\",\n    \"reset\": \"Скинути\",\n    \"resources\": \"Ресурси\",\n    \"restore\": \"Відновити\",\n    \"role\": \"Роль\",\n    \"save\": \"Зберегти\",\n    \"search\": \"Пошук\",\n    \"select\": \"Вибрати\",\n    \"settings\": \"Налаштування\",\n    \"share\": \"Поділитися\",\n    \"shortcut-filter\": \"Фільтр ярликів\",\n    \"shortcuts\": \"Ярлики\",\n    \"sign-in\": \"Увійти\",\n    \"sign-in-with\": \"Увійти через {{provider}}\",\n    \"sign-out\": \"Вийти\",\n    \"sign-up\": \"Зареєструватися\",\n    \"statistics\": \"Статистика\",\n    \"tags\": \"Теги\",\n    \"title\": \"Заголовок\",\n    \"today\": \"Сьогодні\",\n    \"tree-mode\": \"Деревовидний режим\",\n    \"type\": \"Тип\",\n    \"unpin\": \"Відкріпити\",\n    \"update\": \"Оновити\",\n    \"upload\": \"Завантажити\",\n    \"user\": \"Користувач\",\n    \"username\": \"Ім'я користувача\",\n    \"version\": \"Версія\",\n    \"visibility\": \"Видимість\",\n    \"yourself\": \"Ви\"\n  },\n  \"editor\": {\n    \"add-your-comment-here\": \"Додайте свій коментар тут...\",\n    \"any-thoughts\": \"Якісь думки...\",\n    \"exit-focus-mode\": \"Вийти з режиму фокусування\",\n    \"focus-mode\": \"Режим фокусування\",\n    \"no-changes-detected\": \"Змін не виявлено\",\n    \"save\": \"Зберегти\",\n    \"saving\": \"Збереження...\",\n    \"slash-commands\": \"Введіть `/` для команд\"\n  },\n  \"inbox\": {\n    \"failed-to-load\": \"Не вдалося завантажити елемент вхідних\",\n    \"memo-comment\": \"{{user}} залишив коментар до вашої {{memo}}.\",\n    \"no-archived\": \"Немає архівованих сповіщень\",\n    \"no-unread\": \"Немає непрочитаних сповіщень\",\n    \"unread\": \"Непрочитані\"\n  },\n  \"markdown\": {\n    \"checkbox\": \"Чекбокс\",\n    \"code-block\": \"Блок коду\",\n    \"content-syntax\": \"Синтаксис вмісту\"\n  },\n  \"memo\": {\n    \"archived-at\": \"Архівовано о\",\n    \"click-to-hide-nsfw-content\": \"Натисніть, щоб приховати NSFW-контент\",\n    \"click-to-show-nsfw-content\": \"Натисніть, щоб показати NSFW-контент\",\n    \"code\": \"Код\",\n    \"comment\": {\n      \"self\": \"Коментарі\",\n      \"write-a-comment\": \"Напишіть коментар\"\n    },\n    \"copy-content\": \"Копіювати вміст\",\n    \"copy-link\": \"Скопіювати посилання\",\n    \"count-memos-in-date\": \"{{count}} {{memos}} в {{date}}\",\n    \"delete-confirm\": \"Ви впевнені, що хочете видалити цю нотатку? ЦЯ ДІЯ Є НЕВІДКЛИКНОЮ\",\n    \"delete-confirm-description\": \"Ця дія є незворотною. Вкладення, посилання та посилання також буде видалено.\",\n    \"direction\": \"Напрямок\",\n    \"direction-asc\": \"За зростанням\",\n    \"direction-desc\": \"За спаданням\",\n    \"display-time\": \"Час відображення\",\n    \"filters\": {\n      \"has-code\": \"єКод\",\n      \"has-link\": \"єПосилання\",\n      \"has-task-list\": \"єСписокЗавдань\"\n    },\n    \"links\": \"Посилання\",\n    \"load-more\": \"Завантажити більше\",\n    \"no-archived-memos\": \"Немає архівованих нотаток.\",\n    \"no-memos\": \"Немає нотаток.\",\n    \"order-by\": \"Впорядкувати за\",\n    \"search-placeholder\": \"Пошук нотаток...\",\n    \"show-less\": \"Показувати менше\",\n    \"show-more\": \"Показати більше\",\n    \"to-do\": \"Список справ\",\n    \"view-detail\": \"Переглянути деталі\",\n    \"visibility\": {\n      \"disabled\": \"Публічні нотатки вимкнені\",\n      \"private\": \"Приватні\",\n      \"protected\": \"Робоча область\",\n      \"public\": \"Публічні\"\n    }\n  },\n  \"message\": {\n    \"archived-successfully\": \"Архівовано успішно\",\n    \"change-memo-created-time\": \"Змінити час створення нотатки\",\n    \"copied\": \"Скопійовано\",\n    \"deleted-successfully\": \"Успішно видалено\",\n    \"description-is-required\": \"Опис є обов’язковим\",\n    \"failed-to-embed-memo\": \"Не вдалося вбудувати нотатку\",\n    \"fill-all\": \"Будь ласка, заповніть всі поля.\",\n    \"fill-all-required-fields\": \"Будь ласка, заповніть усі обов’язкові поля\",\n    \"maximum-upload-size-is\": \"Максимально допустимий розмір завантаження {{size}} MiB\",\n    \"memo-not-found\": \"Нотатку не знайдено.\",\n    \"new-password-not-match\": \"Нові паролі не співпадають.\",\n    \"no-data\": \"Дані не знайдено.\",\n    \"password-changed\": \"Пароль змінено\",\n    \"password-not-match\": \"Паролі не співпадають.\",\n    \"restored-successfully\": \"Успішно відновлено\",\n    \"succeed-copy-content\": \"Вміст успішно скопійовано.\",\n    \"succeed-copy-link\": \"Посилання успішно скопійовано.\",\n    \"update-succeed\": \"Оновлення успішне\",\n    \"user-not-found\": \"Користувача не знайдено\"\n  },\n  \"reference\": {\n    \"add-references\": \"Додати посилання\",\n    \"embedded-usage\": \"Використовувати як вбудований контент\",\n    \"no-memos-found\": \"Немає знайдених нотаток\",\n    \"search-placeholder\": \"Пошук контенту\"\n  },\n  \"resource\": {\n    \"clear\": \"Очистити\",\n    \"copy-link\": \"Скопіювати посилання\",\n    \"create-dialog\": {\n      \"external-link\": {\n        \"file-name\": \"Назва файлу\",\n        \"file-name-placeholder\": \"Назва файлу\",\n        \"link\": \"Посилання\",\n        \"link-placeholder\": \"https://посилання.на.ваш.ресурс\",\n        \"option\": \"Зовнішнє посилання\",\n        \"type\": \"Тип\",\n        \"type-placeholder\": \"Тип файлу\"\n      },\n      \"local-file\": {\n        \"choose\": \"Виберіть файл…\",\n        \"option\": \"Локальний файл\"\n      },\n      \"title\": \"Створити ресурс\",\n      \"upload-method\": \"Метод завантаження\"\n    },\n    \"delete-all-unused\": \"Видалити всі невикористані\",\n    \"delete-all-unused-confirm\": \"Ви впевнені, що хочете видалити всі невикористані ресурси? ЦЯ ДІЯ Є НЕВІДКЛИКНОЮ\",\n    \"delete-all-unused-error\": \"Не вдалося видалити невикористані ресурси\",\n    \"delete-all-unused-success\": \"Ресурси успішно видалено\",\n    \"delete-resource\": \"Видалити ресурс\",\n    \"delete-selected-resources\": \"Видалити вибрані ресурси\",\n    \"fetching-data\": \"Отримання даних…\",\n    \"file-drag-drop-prompt\": \"Перетягніть файл сюди для завантаження\",\n    \"linked-amount\": \"Кількість посилань\",\n    \"no-files-selected\": \"Файли не вибрані\",\n    \"no-resources\": \"Немає ресурсів.\",\n    \"no-unused-resources\": \"Немає невикористаних ресурсів\",\n    \"reset-link\": \"Скинути посилання\",\n    \"reset-link-prompt\": \"Ви впевнені, що хочете скинути посилання? Це зруйнує всі поточні використання посилань. ЦЯ ДІЯ Є НЕВІДКЛИКНОЮ\",\n    \"reset-resource-link\": \"Скинути посилання на ресурс\",\n    \"unused-resources\": \"Невикористані ресурси\"\n  },\n  \"router\": {\n    \"back-to-top\": \"На початок\",\n    \"go-to-home\": \"На головну\"\n  },\n  \"setting\": {\n    \"member\": {\n      \"admin\": \"Адмін\",\n      \"archive-member\": \"Архівувати учасника\",\n      \"archive-success\": \"{{username}} успішно архівовано\",\n      \"archive-warning\": \"Ви впевнені, що хочете архівувати {{username}}?\",\n      \"archive-warning-description\": \"Архівація вимикає обліковий запис. Ви можете відновити або видалити його пізніше.\",\n      \"create-a-member\": \"Створити учасника\",\n      \"delete-member\": \"Видалити учасника\",\n      \"delete-success\": \"{{username}} успішно видалено\",\n      \"delete-warning\": \"Ви впевнені, що хочете видалити {{username}}? ЦЯ ДІЯ Є НЕВІДКЛИКНОЮ\",\n      \"delete-warning-description\": \"ЦЯ ДІЯ Є НЕВІДКЛИКНОЮ\",\n      \"restore-success\": \"{{username}} успішно відновлено\",\n      \"user\": \"Користувач\",\n      \"label\": \"Учасник\",\n      \"list-title\": \"Список учасників\"\n    },\n    \"my-account\": {\n      \"label\": \"Мій обліковий запис\"\n    },\n    \"preference\": {\n      \"default-memo-sort-option\": \"Час відображення нотаток\",\n      \"default-memo-visibility\": \"Типова видимість нотаток\",\n      \"theme\": \"Тема\",\n      \"label\": \"Налаштування\"\n    },\n    \"shortcut\": {\n      \"delete-confirm\": \"Ви впевнені, що хочете видалити ярлик `{{title}}`?\",\n      \"delete-success\": \"Ярлик `{{title}}` успішно видалено\"\n    },\n    \"sso\": {\n      \"authorization-endpoint\": \"Точка авторизації\",\n      \"client-id\": \"Ідентифікатор клієнта\",\n      \"client-secret\": \"Секрет клієнта\",\n      \"confirm-delete\": \"Ви впевнені, що хочете видалити конфігурацію SSO \\\"{{name}}\\\"? ЦЯ ДІЯ Є НЕВІДКЛИКНОЮ\",\n      \"create-sso\": \"Створити SSO\",\n      \"custom\": \"Користувацький\",\n      \"delete-sso\": \"Підтвердити видалення\",\n      \"disabled-password-login-warning\": \"Вхід за паролем вимкнено, будьте особливо обережні при видаленні постачальників ідентичності\",\n      \"display-name\": \"Відображуване ім'я\",\n      \"identifier\": \"Ідентифікатор\",\n      \"identifier-filter\": \"Фільтр ідентифікатора\",\n      \"no-sso-found\": \"SSO не знайдено.\",\n      \"redirect-url\": \"URL перенаправлення\",\n      \"scopes\": \"Обсяги\",\n      \"single-sign-on\": \"Конфігурація Single Sign-On (SSO) для автентифікації\",\n      \"sso-created\": \"SSO {{name}} створено\",\n      \"sso-list\": \"Список SSO\",\n      \"sso-updated\": \"SSO {{name}} оновлено\",\n      \"template\": \"Шаблон\",\n      \"token-endpoint\": \"Точка доступу токенів\",\n      \"update-sso\": \"Оновити SSO\",\n      \"user-endpoint\": \"Кінцева точка користувача\",\n      \"label\": \"SSO\"\n    },\n    \"storage\": {\n      \"accesskey\": \"Ключ доступу\",\n      \"accesskey-placeholder\": \"Ключ доступу / Ідентифікатор доступу\",\n      \"bucket\": \"Контейнер\",\n      \"bucket-placeholder\": \"Назва контейнера\",\n      \"create-a-service\": \"Створити службу\",\n      \"create-storage\": \"Створити сховище\",\n      \"current-storage\": \"Поточне сховище об'єктів\",\n      \"delete-storage\": \"Видалити сховище\",\n      \"endpoint\": \"Кінцева точка\",\n      \"filepath-template\": \"Шаблон шляху до файлу\",\n      \"local-storage-path\": \"Шлях до локального сховища\",\n      \"path\": \"Шлях до сховища\",\n      \"path-description\": \"Ви можете використовувати ті ж динамічні змінні з локального сховища, такі як {filename}\",\n      \"path-placeholder\": \"налаштований/шлях\",\n      \"presign-placeholder\": \"Пресигн URL, необов'язково\",\n      \"region\": \"Регіон\",\n      \"region-placeholder\": \"Назва регіону\",\n      \"s3-compatible-url\": \"S3 Сумісний URL\",\n      \"secretkey\": \"Секретний ключ\",\n      \"secretkey-placeholder\": \"Секретний ключ / Ключ доступу\",\n      \"storage-services\": \"Служби сховища\",\n      \"type-database\": \"База даних\",\n      \"type-local\": \"Локальна файлова система\",\n      \"update-a-service\": \"Оновити службу\",\n      \"update-local-path\": \"Оновити локальний шлях\",\n      \"update-local-path-description\": \"Локальний шлях є відносним шляхом до вашого файлу бази даних\",\n      \"update-storage\": \"Оновити сховище\",\n      \"url-prefix\": \"Префікс URL\",\n      \"url-prefix-placeholder\": \"Користувацький префікс URL, необов'язково\",\n      \"url-suffix\": \"Суфікс URL\",\n      \"url-suffix-placeholder\": \"Користувацький суфікс URL, необов'язково\",\n      \"warning-text\": \"Ви впевнені, що хочете видалити службу сховища \\\"{{name}}\\\"? ЦЯ ДІЯ Є НЕВІДКЛИКНОЮ\",\n      \"label\": \"Сховище\"\n    },\n    \"system\": {\n      \"additional-script\": \"Додатковий скрипт\",\n      \"additional-script-placeholder\": \"Додатковий JavaScript код\",\n      \"additional-style\": \"Додатковий стиль\",\n      \"additional-style-placeholder\": \"Додатковий CSS код\",\n      \"allow-user-signup\": \"Дозволити реєстрацію користувачів\",\n      \"customize-server\": {\n        \"description\": \"Опис\",\n        \"icon-url\": \"URL значка\",\n        \"locale\": \"Мова сервера\",\n        \"title\": \"Налаштування сервера\"\n      },\n      \"disable-password-login\": \"Вимкнути вхід за паролем\",\n      \"disable-password-login-final-warning\": \"Будь ласка, введіть \\\"ПІДТВЕРДЖУЮ\\\", якщо ви знаєте, що робите.\",\n      \"disable-password-login-warning\": \"Це вимкне вхід за паролем для всіх користувачів. Вхід без скасування цього налаштування в базі даних неможливий, якщо ваші налаштовані постачальники ідентичності не працюють. Також слід бути особливо обережними при видаленні постачальника ідентичності.\",\n      \"display-with-updated-time\": \"Відображати з оновленим часом\",\n      \"enable-auto-compact\": \"Увімкнути автоматичне стиснення\",\n      \"enable-double-click-to-edit\": \"Увімкнути двократне натискання для редагування\",\n      \"enable-password-login\": \"Увімкнути вхід за паролем\",\n      \"enable-password-login-warning\": \"Це дозволить вхід за паролем для всіх користувачів. Продовжуйте тільки якщо ви хочете, щоб користувачі могли входити як через SSO, так і за паролем.\",\n      \"max-upload-size\": \"Максимальний розмір завантаження (MiB)\",\n      \"max-upload-size-hint\": \"Рекомендується значення - 32 MiB.\",\n      \"removed-completed-task-list-items\": \"Увімкнути видалення виконаних завдань\",\n      \"server-name\": \"Назва сервера\",\n      \"title\": \"Загальні\",\n      \"label\": \"Система\"\n    },\n    \"version\": \"Версія\",\n    \"access-token\": {\n      \"access-token-copied-to-clipboard\": \"Токен доступу скопійовано в буфер обміну\",\n      \"access-token-deleted\": \"Токен доступу `{{description}}` видалено\",\n      \"access-token-deletion\": \"Ви впевнені, що хочете видалити токен доступу {{description}}? ЦЯ ДІЯ Є НЕВІДКЛИКНОЮ.\",\n      \"access-token-deletion-description\": \"Ця дія є незворотною. Вам потрібно буде оновити будь-які служби, що використовують цей токен, щоб використовувати новий токен.\",\n      \"create-dialog\": {\n        \"access-token-created\": \"Токен доступу `{{description}}` створено\",\n        \"create-access-token\": \"Створити токен доступу\",\n        \"created-at\": \"Створено\",\n        \"description\": \"Опис\",\n        \"duration-1m\": \"1 місяць\",\n        \"duration-8h\": \"8 годин\",\n        \"duration-never\": \"Ніколи\",\n        \"expiration\": \"Термін дії\",\n        \"expires-at\": \"Закінчується\",\n        \"some-description\": \"Деякий опис...\"\n      },\n      \"description\": \"Список усіх токенів доступу для вашого облікового запису.\",\n      \"title\": \"Токени доступу\",\n      \"token\": \"Токен\"\n    },\n    \"account\": {\n      \"change-password\": \"Змінити пароль\",\n      \"email-note\": \"Необов'язково\",\n      \"export-memos\": \"Експортувати нотатки\",\n      \"nickname-note\": \"Відображається в банері\",\n      \"openapi-reset\": \"Скинути OpenAPI ключ\",\n      \"openapi-sample-post\": \"Привіт #memos з {{url}}\",\n      \"openapi-title\": \"OpenAPI\",\n      \"reset-api\": \"Скинути API\",\n      \"title\": \"Інформація про обліковий запис\",\n      \"update-information\": \"Оновити інформацію\",\n      \"username-note\": \"Використовується для входу\"\n    },\n    \"instance\": {\n      \"disallow-change-nickname\": \"Заборонити зміну нікнейму\",\n      \"disallow-change-username\": \"Заборонити зміну імені користувача\",\n      \"disallow-password-auth\": \"Заборонити авторизацію за паролем\",\n      \"disallow-user-registration\": \"Заборонити реєстрацію користувачів\",\n      \"monday\": \"Понеділок\",\n      \"saturday\": \"Субота\",\n      \"sunday\": \"Неділя\",\n      \"week-start-day\": \"День початку тижня\"\n    },\n    \"memo\": {\n      \"content-length-limit\": \"Обмеження довжини вмісту (байт)\",\n      \"enable-blur-nsfw-content\": \"Увімкнути розмиття чутливого контенту (NSFW)\",\n      \"enable-memo-comments\": \"Увімкнути коментарі до нотаток\",\n      \"enable-memo-location\": \"Увімкнути місцезнаходження нотаток\",\n      \"reactions\": \"Реакції\",\n      \"title\": \"Налаштування, пов'язані з нотатками\",\n      \"label\": \"Нотатка\"\n    },\n    \"webhook\": {\n      \"create-dialog\": {\n        \"an-easy-to-remember-name\": \"Легке для запам'ятовування ім'я\",\n        \"create-webhook\": \"Створити вебхук\",\n        \"create-webhook-success\": \"Вебхук `{{name}}` створено\",\n        \"edit-webhook\": \"Редагувати вебхук\",\n        \"payload-url\": \"URL корисного навантаження\",\n        \"title\": \"Заголовок\",\n        \"url-example-post-receive\": \"https://приклад.com/postreceive\"\n      },\n      \"delete-dialog\": {\n        \"delete-webhook-description\": \"Ця дія є незворотною.\",\n        \"delete-webhook-success\": \"Вебхук `{{name}}` успішно видалено\",\n        \"delete-webhook-title\": \"Ви впевнені, що хочете видалити вебхук `{{name}}`?\"\n      },\n      \"no-webhooks-found\": \"Вебхуків не знайдено.\",\n      \"title\": \"Вебхуки\",\n      \"url\": \"Посилання\"\n    }\n  },\n  \"tag\": {\n    \"all-tags\": \"Усі теги\",\n    \"create-tag\": \"Створити тег\",\n    \"create-tags-guide\": \"Ви можете створити теги, ввівши `#тег`.\",\n    \"delete-confirm\": \"Ви впевнені, що хочете видалити цей тег? Всі пов'язані нотатки будуть архівовані.\",\n    \"delete-success\": \"Тег успішно видалено\",\n    \"delete-tag\": \"Видалити тег\",\n    \"new-name\": \"Нова назва\",\n    \"no-tag-found\": \"Тегів не знайдено\",\n    \"old-name\": \"Стара назва\",\n    \"rename-error-empty\": \"Назва тегу не може бути порожньою або містити пробіли\",\n    \"rename-error-repeat\": \"Нова назва не може співпадати зі старою\",\n    \"rename-success\": \"Тег успішно перейменовано\",\n    \"rename-tag\": \"Перейменувати тег\",\n    \"rename-tip\": \"Всі ваші нотатки з цим тегом також будуть оновлені\"\n  },\n  \"tooltip\": {\n    \"link-memo\": \"Посилання на нотатку\",\n    \"markdown-menu\": \"Markdown\",\n    \"select-location\": \"Місцезнаходження\",\n    \"select-visibility\": \"Видимість\",\n    \"tags\": \"Теги\",\n    \"upload-attachment\": \"Завантажити вкладення\"\n  }\n}\n"
  },
  {
    "path": "web/src/locales/vi.json",
    "content": "{\n  \"about\": {\n    \"blogs\": \"Blog\",\n    \"description\": \"Dịch vụ ghi chú nhẹ, ưu tiên quyền riêng tư. Dễ dàng ghi lại và chia sẻ những suy nghĩ tuyệt vời của bạn.\",\n    \"documents\": \"Tài liệu\",\n    \"github-repository\": \"Kho GitHub\",\n    \"official-website\": \"Trang web chính thức\"\n  },\n  \"auth\": {\n    \"create-your-account\": \"Tạo tài khoản của bạn\",\n    \"host-tip\": \"Bạn đang đăng ký với tư cách Quản trị viên Trang.\",\n    \"new-password\": \"Mật khẩu mới\",\n    \"repeat-new-password\": \"Nhập lại mật khẩu mới\",\n    \"sign-in-tip\": \"Đã có tài khoản?\",\n    \"sign-up-tip\": \"Chưa có tài khoản?\"\n  },\n  \"common\": {\n    \"about\": \"Giới thiệu\",\n    \"add\": \"Thêm\",\n    \"admin\": \"Quản trị\",\n    \"all\": \"Tất cả\",\n    \"archive\": \"Lưu trữ\",\n    \"archived\": \"Đã lưu trữ\",\n    \"attachments\": \"Tệp đính kèm\",\n    \"auto-expand\": \"Mở rộng tự động\",\n    \"avatar\": \"Ảnh đại diện\",\n    \"basic\": \"Cơ bản\",\n    \"beta\": \"Beta\",\n    \"calendar\": \"Lịch\",\n    \"cancel\": \"Hủy\",\n    \"change\": \"Thay đổi\",\n    \"clear\": \"Xóa\",\n    \"close\": \"Đóng\",\n    \"collapse\": \"Thu gọn\",\n    \"confirm\": \"Xác nhận\",\n    \"copy\": \"Sao chép\",\n    \"create\": \"Tạo\",\n    \"created-at\": \"Ngày tạo\",\n    \"database\": \"Cơ sở dữ liệu\",\n    \"day\": \"Ngày\",\n    \"days\": {\n      \"fri\": \"T6\",\n      \"mon\": \"T2\",\n      \"sat\": \"T7\",\n      \"sun\": \"CN\",\n      \"thu\": \"T5\",\n      \"tue\": \"T3\",\n      \"wed\": \"T4\"\n    },\n    \"delete\": \"Xóa\",\n    \"description\": \"Mô tả\",\n    \"edit\": \"Chỉnh sửa\",\n    \"email\": \"Thư điện tử\",\n    \"expand\": \"Mở rộng\",\n    \"explore\": \"Khám phá\",\n    \"file\": \"Tệp\",\n    \"filter\": \"Lọc\",\n    \"home\": \"Trang chủ\",\n    \"image\": \"Hình ảnh\",\n    \"in\": \"Trong\",\n    \"inbox\": \"Hộp thư đến\",\n    \"input\": \"Nhập liệu\",\n    \"language\": \"Ngôn ngữ\",\n    \"last-updated-at\": \"Cập nhật lần cuối\",\n    \"learn-more\": \"Tìm hiểu thêm\",\n    \"link\": \"Liên kết\",\n    \"map\": \"Bản đồ\",\n    \"mark\": \"Đánh dấu\",\n    \"memo\": \"Ghi chú\",\n    \"memos\": \"Ghi chú\",\n    \"more\": \"Thêm\",\n    \"name\": \"Tên\",\n    \"new\": \"Mới\",\n    \"nickname\": \"Biệt danh\",\n    \"null\": \"Trống\",\n    \"or\": \"hoặc\",\n    \"password\": \"Mật khẩu\",\n    \"pin\": \"Ghim\",\n    \"pinned\": \"Đã ghim\",\n    \"preview\": \"Xem trước\",\n    \"profile\": \"Hồ sơ\",\n    \"properties\": \"Thuộc tính\",\n    \"referenced-by\": \"Được tham chiếu bởi\",\n    \"referencing\": \"Đang tham chiếu\",\n    \"relations\": \"Liên kết\",\n    \"remember-me\": \"Ghi nhớ tôi\",\n    \"rename\": \"Đổi tên\",\n    \"reset\": \"Đặt lại\",\n    \"resources\": \"Tài nguyên\",\n    \"restore\": \"Khôi phục\",\n    \"role\": \"Vai trò\",\n    \"save\": \"Lưu\",\n    \"search\": \"Tìm kiếm\",\n    \"select\": \"Chọn\",\n    \"settings\": \"Cài đặt\",\n    \"share\": \"Chia sẻ\",\n    \"shortcut-filter\": \"Bộ lọc phím tắt\",\n    \"shortcuts\": \"Phím tắt\",\n    \"sign-in\": \"Đăng nhập\",\n    \"sign-in-with\": \"Đăng nhập bằng {{provider}}\",\n    \"sign-out\": \"Đăng xuất\",\n    \"sign-up\": \"Đăng ký\",\n    \"statistics\": \"Thống kê\",\n    \"tags\": \"Thẻ\",\n    \"title\": \"Tiêu đề\",\n    \"today\": \"Hôm nay\",\n    \"tree-mode\": \"Chế độ cây\",\n    \"type\": \"Loại\",\n    \"unpin\": \"Bỏ ghim\",\n    \"update\": \"Cập nhật\",\n    \"upload\": \"Tải lên\",\n    \"user\": \"Người dùng\",\n    \"username\": \"Tên người dùng\",\n    \"version\": \"Phiên bản\",\n    \"visibility\": \"Quyền xem\",\n    \"yourself\": \"Bản thân\"\n  },\n  \"editor\": {\n    \"add-your-comment-here\": \"Thêm bình luận của bạn tại đây...\",\n    \"any-thoughts\": \"Có suy nghĩ gì...\",\n    \"exit-focus-mode\": \"Thoát chế độ tập trung\",\n    \"focus-mode\": \"Chế độ tập trung\",\n    \"no-changes-detected\": \"Không phát hiện thay đổi\",\n    \"save\": \"Lưu\",\n    \"saving\": \"Đang lưu...\",\n    \"slash-commands\": \"Nhập `/` để sử dụng lệnh\"\n  },\n  \"inbox\": {\n    \"failed-to-load\": \"Không thể tải mục hộp thư\",\n    \"memo-comment\": \"{{user}} đã bình luận về {{memo}} của bạn.\",\n    \"no-archived\": \"Không có thông báo đã lưu trữ\",\n    \"no-unread\": \"Không có thông báo chưa đọc\",\n    \"unread\": \"Chưa đọc\"\n  },\n  \"markdown\": {\n    \"checkbox\": \"Hộp kiểm\",\n    \"code-block\": \"Khối mã\",\n    \"content-syntax\": \"Cú pháp nội dung\"\n  },\n  \"memo\": {\n    \"archived-at\": \"Đã lưu trữ lúc\",\n    \"click-to-hide-nsfw-content\": \"Nhấp để ẩn nội dung nhạy cảm\",\n    \"click-to-show-nsfw-content\": \"Nhấp để hiện nội dung nhạy cảm\",\n    \"code\": \"Mã\",\n    \"comment\": {\n      \"self\": \"Bình luận\",\n      \"write-a-comment\": \"Viết bình luận\"\n    },\n    \"copy-content\": \"Sao chép nội dung\",\n    \"copy-link\": \"Sao chép liên kết\",\n    \"count-memos-in-date\": \"{{count}} {{memos}} trong {{date}}\",\n    \"delete-confirm\": \"Bạn có chắc chắn muốn xóa ghi chú này không? HÀNH ĐỘNG NÀY KHÔNG THỂ HOÀN TÁC\",\n    \"delete-confirm-description\": \"Hành động này không thể hoàn tác. Tệp đính kèm, liên kết và tham chiếu cũng sẽ bị xóa.\",\n    \"direction\": \"Hướng\",\n    \"direction-asc\": \"Tăng dần\",\n    \"direction-desc\": \"Giảm dần\",\n    \"display-time\": \"Thời gian hiển thị\",\n    \"filters\": {\n      \"has-code\": \"cóMã\",\n      \"has-link\": \"cóLiênKết\",\n      \"has-task-list\": \"cóDanhSáchViệc\"\n    },\n    \"links\": \"Liên kết\",\n    \"load-more\": \"Tải thêm\",\n    \"no-archived-memos\": \"Không có ghi chú nào được lưu trữ.\",\n    \"no-memos\": \"Không có ghi chú nào.\",\n    \"order-by\": \"Sắp xếp theo\",\n    \"search-placeholder\": \"Tìm kiếm ghi chú\",\n    \"show-less\": \"Hiển thị ít hơn\",\n    \"show-more\": \"Hiển thị thêm\",\n    \"to-do\": \"Việc cần làm\",\n    \"view-detail\": \"Xem chi tiết\",\n    \"visibility\": {\n      \"disabled\": \"Ghi chú công khai đã bị vô hiệu hóa\",\n      \"private\": \"Riêng tư\",\n      \"protected\": \"Không gian làm việc\",\n      \"public\": \"Công khai\"\n    }\n  },\n  \"message\": {\n    \"archived-successfully\": \"Lưu trữ thành công\",\n    \"change-memo-created-time\": \"Thay đổi thời gian tạo ghi chú\",\n    \"copied\": \"Đã sao chép\",\n    \"deleted-successfully\": \"Xóa thành công\",\n    \"description-is-required\": \"Mô tả là bắt buộc\",\n    \"failed-to-embed-memo\": \"Không thể nhúng ghi chú\",\n    \"fill-all\": \"Vui lòng điền vào tất cả các trường.\",\n    \"fill-all-required-fields\": \"Vui lòng điền vào tất cả các trường bắt buộc\",\n    \"maximum-upload-size-is\": \"Kích thước tải lên tối đa là {{size}} MiB\",\n    \"memo-not-found\": \"Không tìm thấy ghi chú.\",\n    \"new-password-not-match\": \"Mật khẩu mới không khớp.\",\n    \"no-data\": \"Không tìm thấy dữ liệu.\",\n    \"password-changed\": \"Đã thay đổi mật khẩu\",\n    \"password-not-match\": \"Mật khẩu không khớp.\",\n    \"restored-successfully\": \"Khôi phục thành công\",\n    \"succeed-copy-content\": \"Đã sao chép nội dung thành công.\",\n    \"succeed-copy-link\": \"Sao chép liên kết thành công.\",\n    \"update-succeed\": \"Cập nhật thành công\",\n    \"user-not-found\": \"Không tìm thấy người dùng\"\n  },\n  \"reference\": {\n    \"add-references\": \"Thêm tham chiếu\",\n    \"embedded-usage\": \"Sử dụng như nội dung nhúng\",\n    \"no-memos-found\": \"Không tìm thấy ghi chú\",\n    \"search-placeholder\": \"Tìm kiếm nội dung\"\n  },\n  \"resource\": {\n    \"clear\": \"Xóa\",\n    \"copy-link\": \"Sao chép liên kết\",\n    \"create-dialog\": {\n      \"external-link\": {\n        \"file-name\": \"Tên tệp\",\n        \"file-name-placeholder\": \"Tên tệp\",\n        \"link\": \"Liên kết\",\n        \"link-placeholder\": \"https://duong-dan.den/tai-nguyen/cua-ban\",\n        \"option\": \"Liên kết ngoài\",\n        \"type\": \"Loại\",\n        \"type-placeholder\": \"Loại tệp\"\n      },\n      \"local-file\": {\n        \"choose\": \"Chọn một tệp…\",\n        \"option\": \"Tệp cục bộ\"\n      },\n      \"title\": \"Tạo tài nguyên\",\n      \"upload-method\": \"Phương thức tải lên\"\n    },\n    \"delete-all-unused\": \"Xóa tất cả không sử dụng\",\n    \"delete-all-unused-confirm\": \"Bạn có chắc chắn muốn xóa tất cả tài nguyên không sử dụng? HÀNH ĐỘNG NÀY KHÔNG THỂ HOÀN TÁC\",\n    \"delete-all-unused-error\": \"Không thể xóa tài nguyên không sử dụng\",\n    \"delete-all-unused-success\": \"Đã xóa tài nguyên thành công\",\n    \"delete-resource\": \"Xóa tài nguyên\",\n    \"delete-selected-resources\": \"Xóa các tài nguyên đã chọn\",\n    \"fetching-data\": \"Đang tải dữ liệu…\",\n    \"file-drag-drop-prompt\": \"Kéo và thả tệp của bạn vào đây để tải lên\",\n    \"linked-amount\": \"Số lượng liên kết\",\n    \"no-files-selected\": \"Chưa chọn tệp nào\",\n    \"no-resources\": \"Không có tài nguyên.\",\n    \"no-unused-resources\": \"Không có tài nguyên không sử dụng\",\n    \"reset-link\": \"Đặt lại liên kết\",\n    \"reset-link-prompt\": \"Bạn có chắc chắn muốn đặt lại liên kết? Điều này sẽ làm hỏng tất cả các liên kết hiện tại. HÀNH ĐỘNG NÀY KHÔNG THỂ HOÀN TÁC\",\n    \"reset-resource-link\": \"Đặt lại liên kết tài nguyên\",\n    \"unused-resources\": \"Tài nguyên không sử dụng\"\n  },\n  \"router\": {\n    \"back-to-top\": \"Lên đầu trang\",\n    \"go-to-home\": \"Về trang chủ\"\n  },\n  \"setting\": {\n    \"member\": {\n      \"admin\": \"Quản trị viên\",\n      \"archive-member\": \"Lưu trữ thành viên\",\n      \"archive-success\": \"{{username}} đã được lưu trữ thành công\",\n      \"archive-warning\": \"Bạn có chắc chắn muốn lưu trữ {{username}}?\",\n      \"archive-warning-description\": \"Lưu trữ sẽ vô hiệu hóa tài khoản. Bạn có thể khôi phục hoặc xóa sau.\",\n      \"create-a-member\": \"Tạo thành viên mới\",\n      \"delete-member\": \"Xóa thành viên\",\n      \"delete-success\": \"{{username}} đã được xóa thành công\",\n      \"delete-warning\": \"Bạn có chắc chắn muốn xóa {{username}}? HÀNH ĐỘNG NÀY KHÔNG THỂ HOÀN TÁC\",\n      \"delete-warning-description\": \"HÀNH ĐỘNG NÀY KHÔNG THỂ HOÀN TÁC\",\n      \"restore-success\": \"{{username}} đã được khôi phục thành công\",\n      \"user\": \"Người dùng\",\n      \"label\": \"Thành viên\",\n      \"list-title\": \"Danh sách thành viên\"\n    },\n    \"my-account\": {\n      \"label\": \"Tài khoản của tôi\"\n    },\n    \"preference\": {\n      \"default-memo-sort-option\": \"Thời gian hiển thị ghi chú\",\n      \"default-memo-visibility\": \"Quyền xem ghi chú mặc định\",\n      \"theme\": \"Giao diện\",\n      \"label\": \"Tùy chỉnh\"\n    },\n    \"shortcut\": {\n      \"delete-confirm\": \"Bạn có chắc chắn muốn xóa phím tắt `{{title}}`?\",\n      \"delete-success\": \"Đã xóa phím tắt `{{title}}` thành công\"\n    },\n    \"sso\": {\n      \"authorization-endpoint\": \"Điểm cuối xác thực\",\n      \"client-id\": \"ID khách hàng\",\n      \"client-secret\": \"Khóa bí mật\",\n      \"confirm-delete\": \"Bạn có chắc chắn muốn xóa cấu hình SSO \\\"{{name}}\\\"? HÀNH ĐỘNG NÀY KHÔNG THỂ HOÀN TÁC\",\n      \"create-sso\": \"Tạo SSO\",\n      \"custom\": \"Tùy chỉnh\",\n      \"delete-sso\": \"Xác nhận xóa\",\n      \"disabled-password-login-warning\": \"Đăng nhập bằng mật khẩu đã bị vô hiệu hóa, hãy thật cẩn thận khi xóa nhà cung cấp danh tính\",\n      \"display-name\": \"Tên hiển thị\",\n      \"identifier\": \"Định danh\",\n      \"identifier-filter\": \"Bộ lọc định danh\",\n      \"no-sso-found\": \"Không tìm thấy SSO nào.\",\n      \"redirect-url\": \"URL chuyển hướng\",\n      \"scopes\": \"Phạm vi\",\n      \"single-sign-on\": \"Cấu hình Single Sign-On (SSO) cho xác thực\",\n      \"sso-created\": \"SSO {{name}} đã được tạo\",\n      \"sso-list\": \"Danh sách SSO\",\n      \"sso-updated\": \"SSO {{name}} đã được cập nhật\",\n      \"template\": \"Mẫu\",\n      \"token-endpoint\": \"Điểm cuối token\",\n      \"update-sso\": \"Cập nhật SSO\",\n      \"user-endpoint\": \"Điểm cuối người dùng\",\n      \"label\": \"SSO\"\n    },\n    \"storage\": {\n      \"accesskey\": \"Khóa truy cập\",\n      \"accesskey-placeholder\": \"Khóa truy cập / ID truy cập\",\n      \"bucket\": \"Bucket\",\n      \"bucket-placeholder\": \"Tên bucket\",\n      \"create-a-service\": \"Tạo dịch vụ\",\n      \"create-storage\": \"Tạo kho lưu trữ\",\n      \"current-storage\": \"Kho lưu trữ đối tượng hiện tại\",\n      \"delete-storage\": \"Xóa kho lưu trữ\",\n      \"endpoint\": \"Điểm cuối\",\n      \"filepath-template\": \"Mẫu đường dẫn tệp\",\n      \"local-storage-path\": \"Đường dẫn lưu trữ cục bộ\",\n      \"path\": \"Đường dẫn lưu trữ\",\n      \"path-description\": \"Bạn có thể sử dụng các biến động như trong lưu trữ cục bộ, ví dụ {filename}\",\n      \"path-placeholder\": \"đường/dẫn/tùy-chỉnh\",\n      \"presign-placeholder\": \"URL ký trước, tùy chọn\",\n      \"region\": \"Khu vực\",\n      \"region-placeholder\": \"Tên khu vực\",\n      \"s3-compatible-url\": \"URL tương thích S3\",\n      \"secretkey\": \"Khóa bí mật\",\n      \"secretkey-placeholder\": \"Khóa bí mật / Khóa truy cập\",\n      \"storage-services\": \"Dịch vụ lưu trữ\",\n      \"type-database\": \"Cơ sở dữ liệu\",\n      \"type-local\": \"Hệ thống tệp cục bộ\",\n      \"update-a-service\": \"Cập nhật dịch vụ\",\n      \"update-local-path\": \"Cập nhật đường dẫn lưu trữ cục bộ\",\n      \"update-local-path-description\": \"Đường dẫn lưu trữ cục bộ là đường dẫn tương đối đến tệp cơ sở dữ liệu của bạn\",\n      \"update-storage\": \"Cập nhật kho lưu trữ\",\n      \"url-prefix\": \"Tiền tố URL\",\n      \"url-prefix-placeholder\": \"Tiền tố URL tùy chỉnh, tùy chọn\",\n      \"url-suffix\": \"Hậu tố URL\",\n      \"url-suffix-placeholder\": \"Hậu tố URL tùy chỉnh, tùy chọn\",\n      \"warning-text\": \"Bạn có chắc chắn muốn xóa dịch vụ lưu trữ \\\"{{name}}\\\"? HÀNH ĐỘNG NÀY KHÔNG THỂ HOÀN TÁC\",\n      \"label\": \"Lưu trữ\"\n    },\n    \"system\": {\n      \"additional-script\": \"Script bổ sung\",\n      \"additional-script-placeholder\": \"Mã JavaScript bổ sung\",\n      \"additional-style\": \"Kiểu bổ sung\",\n      \"additional-style-placeholder\": \"Mã CSS bổ sung\",\n      \"allow-user-signup\": \"Cho phép đăng ký người dùng\",\n      \"customize-server\": {\n        \"description\": \"Mô tả\",\n        \"icon-url\": \"URL biểu tượng\",\n        \"locale\": \"Ngôn ngữ máy chủ\",\n        \"title\": \"Tùy chỉnh máy chủ\"\n      },\n      \"disable-password-login\": \"Vô hiệu hóa đăng nhập bằng mật khẩu\",\n      \"disable-password-login-final-warning\": \"Vui lòng nhập \\\"CONFIRM\\\" nếu bạn biết mình đang làm gì.\",\n      \"disable-password-login-warning\": \"Điều này sẽ vô hiệu hóa đăng nhập bằng mật khẩu cho tất cả người dùng. Không thể đăng nhập nếu không hoàn tác cài đặt này trong cơ sở dữ liệu khi nhà cung cấp danh tính đã cấu hình gặp sự cố. Bạn cũng cần phải thật cẩn thận khi xóa nhà cung cấp danh tính\",\n      \"display-with-updated-time\": \"Hiển thị với thời gian cập nhật\",\n      \"enable-auto-compact\": \"Bật tự động nén\",\n      \"enable-double-click-to-edit\": \"Bật nhấp đúp để chỉnh sửa\",\n      \"enable-password-login\": \"Bật đăng nhập bằng mật khẩu\",\n      \"enable-password-login-warning\": \"Điều này sẽ bật đăng nhập bằng mật khẩu cho tất cả người dùng. Chỉ tiếp tục nếu bạn muốn người dùng có thể đăng nhập bằng cả SSO và mật khẩu\",\n      \"max-upload-size\": \"Kích thước tải lên tối đa (MiB)\",\n      \"max-upload-size-hint\": \"Giá trị đề xuất là 32 MiB.\",\n      \"removed-completed-task-list-items\": \"Kích hoạt xóa đã hoàn thành\",\n      \"server-name\": \"Tên máy chủ\",\n      \"title\": \"Chung\",\n      \"label\": \"Hệ thống\"\n    },\n    \"version\": \"Phiên bản\",\n    \"access-token\": {\n      \"access-token-copied-to-clipboard\": \"Đã sao chép token truy cập vào bộ nhớ tạm\",\n      \"access-token-deleted\": \"Đã xóa token truy cập `{{description}}`\",\n      \"access-token-deletion\": \"Bạn có chắc chắn muốn xóa token truy cập `{{description}}`?\",\n      \"access-token-deletion-description\": \"Hành động này không thể hoàn tác. Bạn cần cập nhật các dịch vụ sử dụng token này để sử dụng token mới.\",\n      \"create-dialog\": {\n        \"access-token-created\": \"Đã tạo token truy cập `{{description}}`\",\n        \"create-access-token\": \"Tạo token truy cập\",\n        \"created-at\": \"Ngày tạo\",\n        \"description\": \"Mô tả\",\n        \"duration-1m\": \"1 tháng\",\n        \"duration-8h\": \"8 giờ\",\n        \"duration-never\": \"Không bao giờ\",\n        \"expiration\": \"Hết hạn\",\n        \"expires-at\": \"Hết hạn lúc\",\n        \"some-description\": \"Một số mô tả...\"\n      },\n      \"description\": \"Danh sách tất cả token truy cập cho tài khoản của bạn.\",\n      \"title\": \"Token truy cập\",\n      \"token\": \"Token\"\n    },\n    \"account\": {\n      \"change-password\": \"Đổi mật khẩu\",\n      \"email-note\": \"Tùy chọn\",\n      \"export-memos\": \"Xuất ghi chú\",\n      \"nickname-note\": \"Hiển thị trên banner\",\n      \"openapi-reset\": \"Đặt lại khóa OpenAPI\",\n      \"openapi-sample-post\": \"Xin chào #memos từ {{url}}\",\n      \"openapi-title\": \"OpenAPI\",\n      \"reset-api\": \"Đặt lại API\",\n      \"title\": \"Thông tin tài khoản\",\n      \"update-information\": \"Cập nhật thông tin\",\n      \"username-note\": \"Dùng để đăng nhập\"\n    },\n    \"instance\": {\n      \"disallow-change-nickname\": \"Không cho phép đổi biệt danh\",\n      \"disallow-change-username\": \"Không cho phép đổi tên người dùng\",\n      \"disallow-password-auth\": \"Không cho phép xác thực bằng mật khẩu\",\n      \"disallow-user-registration\": \"Không cho phép đăng ký người dùng\",\n      \"monday\": \"Thứ hai\",\n      \"saturday\": \"Thứ bảy\",\n      \"sunday\": \"Chủ nhật\",\n      \"week-start-day\": \"Ngày bắt đầu tuần\"\n    },\n    \"memo\": {\n      \"content-length-limit\": \"Giới hạn độ dài nội dung (Byte)\",\n      \"enable-blur-nsfw-content\": \"Bật làm mờ nội dung nhạy cảm (NSFW)\",\n      \"enable-memo-comments\": \"Bật bình luận ghi chú\",\n      \"enable-memo-location\": \"Bật vị trí ghi chú\",\n      \"reactions\": \"Phản ứng\",\n      \"title\": \"Cài đặt liên quan đến ghi chú\",\n      \"label\": \"Ghi chú\"\n    },\n    \"webhook\": {\n      \"create-dialog\": {\n        \"an-easy-to-remember-name\": \"Tên dễ nhớ\",\n        \"create-webhook\": \"Tạo webhook\",\n        \"create-webhook-success\": \"Đã tạo webhook `{{name}}`\",\n        \"edit-webhook\": \"Chỉnh sửa webhook\",\n        \"payload-url\": \"URL payload\",\n        \"title\": \"Tiêu đề\",\n        \"url-example-post-receive\": \"https://example.com/postreceive\"\n      },\n      \"delete-dialog\": {\n        \"delete-webhook-description\": \"Hành động này không thể hoàn tác.\",\n        \"delete-webhook-success\": \"Đã xóa webhook `{{name}}` thành công\",\n        \"delete-webhook-title\": \"Bạn có chắc chắn muốn xóa webhook `{{name}}`?\"\n      },\n      \"no-webhooks-found\": \"Không tìm thấy webhook nào.\",\n      \"title\": \"Webhook\",\n      \"url\": \"Url\"\n    }\n  },\n  \"tag\": {\n    \"all-tags\": \"Tất cả thẻ\",\n    \"create-tag\": \"Tạo thẻ\",\n    \"create-tags-guide\": \"Bạn có thể tạo thẻ bằng cách nhập `#thẻ`.\",\n    \"delete-confirm\": \"Bạn có chắc chắn muốn xóa thẻ này không? Tất cả các ghi chú liên quan sẽ được lưu trữ.\",\n    \"delete-success\": \"Đã xóa thẻ thành công\",\n    \"delete-tag\": \"Xóa thẻ\",\n    \"new-name\": \"Tên mới\",\n    \"no-tag-found\": \"Không tìm thấy thẻ\",\n    \"old-name\": \"Tên cũ\",\n    \"rename-error-empty\": \"Tên thẻ không được để trống hoặc chứa khoảng trắng\",\n    \"rename-error-repeat\": \"Tên mới không được trùng với tên cũ\",\n    \"rename-success\": \"Đổi tên thẻ thành công\",\n    \"rename-tag\": \"Đổi tên thẻ\",\n    \"rename-tip\": \"Tất cả ghi chú có thẻ này sẽ được cập nhật.\"\n  },\n  \"tooltip\": {\n    \"link-memo\": \"Liên kết ghi chú\",\n    \"markdown-menu\": \"Markdown\",\n    \"select-location\": \"Chọn vị trí\",\n    \"select-visibility\": \"Chọn quyền xem\",\n    \"tags\": \"Thẻ\",\n    \"upload-attachment\": \"Tải lên tệp đính kèm\"\n  }\n}\n"
  },
  {
    "path": "web/src/locales/zh-Hans.json",
    "content": "{\n  \"about\": {\n    \"blogs\": \"博客\",\n    \"description\": \"Memos 是一个隐私优先、轻量级的笔记解决方案，可以轻松捕捉和分享您的想法。\",\n    \"documents\": \"文档\",\n    \"github-repository\": \"GitHub 仓库\",\n    \"official-website\": \"官网\"\n  },\n  \"auth\": {\n    \"create-your-account\": \"创建您的账户\",\n    \"host-tip\": \"您正在注册为站点管理员。\",\n    \"new-password\": \"新密码\",\n    \"repeat-new-password\": \"重复新密码\",\n    \"sign-in-tip\": \"已有账户？\",\n    \"sign-up-tip\": \"还没有账户？\"\n  },\n  \"common\": {\n    \"about\": \"关于\",\n    \"add\": \"增加\",\n    \"admin\": \"管理\",\n    \"all\": \"全部\",\n    \"archive\": \"归档\",\n    \"archived\": \"已归档\",\n    \"attachments\": \"附件\",\n    \"auto-expand\": \"自动展开\",\n    \"avatar\": \"头像\",\n    \"basic\": \"基础\",\n    \"beta\": \"测试\",\n    \"calendar\": \"日历\",\n    \"cancel\": \"取消\",\n    \"change\": \"修改\",\n    \"clear\": \"清除\",\n    \"close\": \"关闭\",\n    \"collapse\": \"收起\",\n    \"confirm\": \"确定\",\n    \"copy\": \"复制\",\n    \"create\": \"创建\",\n    \"created-at\": \"创建时间\",\n    \"database\": \"数据库\",\n    \"day\": \"天\",\n    \"days\": {\n      \"fri\": \"五\",\n      \"mon\": \"一\",\n      \"sat\": \"六\",\n      \"sun\": \"日\",\n      \"thu\": \"四\",\n      \"tue\": \"二\",\n      \"wed\": \"三\"\n    },\n    \"delete\": \"删除\",\n    \"description\": \"说明\",\n    \"edit\": \"编辑\",\n    \"email\": \"邮箱\",\n    \"expand\": \"展开\",\n    \"explore\": \"发现\",\n    \"file\": \"文件\",\n    \"filter\": \"过滤器\",\n    \"home\": \"主页\",\n    \"image\": \"图片\",\n    \"in\": \"包含\",\n    \"inbox\": \"通知\",\n    \"input\": \"输入\",\n    \"language\": \"语言\",\n    \"last-updated-at\": \"最后更新时间\",\n    \"learn-more\": \"了解更多\",\n    \"link\": \"链接\",\n    \"map\": \"地图\",\n    \"mark\": \"引用\",\n    \"memo\": \"备忘录\",\n    \"memos\": \"备忘录\",\n    \"more\": \"更多\",\n    \"name\": \"名称\",\n    \"new\": \"新建\",\n    \"nickname\": \"昵称\",\n    \"null\": \"空\",\n    \"or\": \"或者\",\n    \"password\": \"密码\",\n    \"pin\": \"置顶\",\n    \"pinned\": \"已置顶\",\n    \"preview\": \"预览\",\n    \"profile\": \"个人资料\",\n    \"properties\": \"属性\",\n    \"referenced-by\": \"被引用\",\n    \"referencing\": \"引用\",\n    \"relations\": \"关系图\",\n    \"remember-me\": \"保持登录\",\n    \"rename\": \"重命名\",\n    \"reset\": \"重置\",\n    \"resources\": \"资源库\",\n    \"restore\": \"恢复\",\n    \"role\": \"身份\",\n    \"save\": \"保存\",\n    \"search\": \"搜索\",\n    \"select\": \"选择\",\n    \"settings\": \"设置\",\n    \"share\": \"分享\",\n    \"shortcut-filter\": \"捷径过滤器\",\n    \"shortcuts\": \"捷径\",\n    \"sign-in\": \"登录\",\n    \"sign-in-with\": \"使用 {{provider}} 登录\",\n    \"sign-out\": \"退出登录\",\n    \"sign-up\": \"注册\",\n    \"statistics\": \"统计\",\n    \"tags\": \"标签\",\n    \"title\": \"标题\",\n    \"today\": \"今天\",\n    \"tree-mode\": \"树模式\",\n    \"type\": \"类型\",\n    \"unpin\": \"取消置顶\",\n    \"update\": \"更新\",\n    \"upload\": \"上传\",\n    \"user\": \"用户\",\n    \"username\": \"用户名\",\n    \"version\": \"版本\",\n    \"visibility\": \"可见性\",\n    \"yourself\": \"您自己\"\n  },\n  \"editor\": {\n    \"add-your-comment-here\": \"请输入您的评论...\",\n    \"any-thoughts\": \"此刻的想法...\",\n    \"exit-focus-mode\": \"退出聚焦模式\",\n    \"focus-mode\": \"聚焦模式\",\n    \"no-changes-detected\": \"未检测到更改\",\n    \"save\": \"保存\",\n    \"saving\": \"保存中...\",\n    \"slash-commands\": \"按下 `/` 输入命令\"\n  },\n  \"inbox\": {\n    \"failed-to-load\": \"加载失败\",\n    \"memo-comment\": \"{{user}} 评论了您的“{{memo}}”。\",\n    \"no-archived\": \"无已归档通知\",\n    \"no-unread\": \"无未读通知\",\n    \"unread\": \"未读\"\n  },\n  \"markdown\": {\n    \"checkbox\": \"复选框\",\n    \"code-block\": \"代码块\",\n    \"content-syntax\": \"内容语法\"\n  },\n  \"memo\": {\n    \"archived-at\": \"归档于\",\n    \"click-to-hide-nsfw-content\": \"点击隐藏 NSFW 内容\",\n    \"click-to-show-nsfw-content\": \"点击显示 NSFW 内容\",\n    \"code\": \"代码\",\n    \"comment\": {\n      \"self\": \"评论\",\n      \"write-a-comment\": \"写评论\"\n    },\n    \"copy-content\": \"复制内容\",\n    \"copy-link\": \"复制链接\",\n    \"count-memos-in-date\": \"{{date}} 有 {{count}} 条 {{memos}}\",\n    \"delete-confirm\": \"您确定要删除此条备忘录吗？\",\n    \"delete-confirm-description\": \"此操作是不可逆的，附件、链接和引用也会被删除。\",\n    \"direction\": \"排序方式\",\n    \"direction-asc\": \"正序\",\n    \"direction-desc\": \"倒序\",\n    \"display-time\": \"展示时间\",\n    \"filters\": {\n      \"has-code\": \"有代码\",\n      \"has-link\": \"有链接\",\n      \"has-task-list\": \"有待办\"\n    },\n    \"links\": \"链接\",\n    \"load-more\": \"加载更多\",\n    \"no-archived-memos\": \"没有已归档备忘录。\",\n    \"no-memos\": \"无备忘录\",\n    \"order-by\": \"排序\",\n    \"search-placeholder\": \"搜索备忘录\",\n    \"show-less\": \"显示较少\",\n    \"show-more\": \"查看更多\",\n    \"to-do\": \"待办\",\n    \"view-detail\": \"查看详情\",\n    \"visibility\": {\n      \"disabled\": \"已禁用公开备忘录\",\n      \"private\": \"私有\",\n      \"protected\": \"工作区\",\n      \"public\": \"公开\"\n    }\n  },\n  \"message\": {\n    \"archived-successfully\": \"归档成功\",\n    \"change-memo-created-time\": \"更改备忘录创建时间\",\n    \"copied\": \"已复制\",\n    \"deleted-successfully\": \"成功删除！\",\n    \"description-is-required\": \"请填写说明\",\n    \"failed-to-embed-memo\": \"嵌入备忘录失败\",\n    \"fill-all\": \"请填写所有栏目。\",\n    \"fill-all-required-fields\": \"请填写所有必填项。\",\n    \"maximum-upload-size-is\": \"允许的最大上传大小为 {{size}} MiB\",\n    \"memo-not-found\": \"找不到备忘录\",\n    \"new-password-not-match\": \"新密码不一致。\",\n    \"no-data\": \"未找到任何数据。\",\n    \"password-changed\": \"密码已修改\",\n    \"password-not-match\": \"密码不一致。\",\n    \"restored-successfully\": \"恢复成功\",\n    \"succeed-copy-content\": \"复制内容到剪贴板成功。\",\n    \"succeed-copy-link\": \"复制链接到剪贴板成功。\",\n    \"update-succeed\": \"更新成功\",\n    \"user-not-found\": \"未找到该用户\"\n  },\n  \"reference\": {\n    \"add-references\": \"添加引用\",\n    \"embedded-usage\": \"作为嵌入内容使用\",\n    \"no-memos-found\": \"没有发现备忘录\",\n    \"search-placeholder\": \"搜索内容\"\n  },\n  \"resource\": {\n    \"clear\": \"清除\",\n    \"copy-link\": \"复制链接\",\n    \"create-dialog\": {\n      \"external-link\": {\n        \"file-name\": \"文件名\",\n        \"file-name-placeholder\": \"文件名\",\n        \"link\": \"链接\",\n        \"link-placeholder\": \"https://the.link.to/your/resource\",\n        \"option\": \"外部链接\",\n        \"type\": \"类型\",\n        \"type-placeholder\": \"文件类型\"\n      },\n      \"local-file\": {\n        \"choose\": \"选择文件...\",\n        \"option\": \"本地文件\"\n      },\n      \"title\": \"创建资源\",\n      \"upload-method\": \"上传方式\"\n    },\n    \"delete-all-unused\": \"删除所有未使用资源\",\n    \"delete-all-unused-confirm\": \"您确定要删除所有未使用资源吗？（此操作不可逆）\",\n    \"delete-all-unused-error\": \"删除未使用的资源失败\",\n    \"delete-all-unused-success\": \"删除资源成功\",\n    \"delete-resource\": \"删除资源\",\n    \"delete-selected-resources\": \"删除选中资源\",\n    \"fetching-data\": \"正在获取数据...\",\n    \"file-drag-drop-prompt\": \"将您的文件拖放到此处以上传文件\",\n    \"linked-amount\": \"链接的备忘录数量\",\n    \"no-files-selected\": \"没有文件被选中\",\n    \"no-resources\": \"没有资源。\",\n    \"no-unused-resources\": \"没有可删除的资源\",\n    \"reset-link\": \"重置链接\",\n    \"reset-link-prompt\": \"您确定要重置链接吗？这将导致当前使用的链接失效。（此操作不可逆）\",\n    \"reset-resource-link\": \"重置资源链接\",\n    \"unused-resources\": \"未使用资源\"\n  },\n  \"router\": {\n    \"back-to-top\": \"回到顶部\",\n    \"go-to-home\": \"回到首页\"\n  },\n  \"setting\": {\n    \"member\": {\n      \"admin\": \"管理员\",\n      \"archive-member\": \"归档成员\",\n      \"archive-success\": \"{{username}} 归档成功\",\n      \"archive-warning\": \"您确定要归档 {{username}} 吗？\",\n      \"archive-warning-description\": \"归档会禁用用户。您可以稍后恢复或删除它。\",\n      \"create-a-member\": \"创建成员\",\n      \"delete-member\": \"删除成员\",\n      \"delete-success\": \"{{username}} 删除成功\",\n      \"delete-warning\": \"您确定要删除 {{username}} 吗？\",\n      \"delete-warning-description\": \"此操作不可逆\",\n      \"restore-success\": \"{{username}} 恢复成功\",\n      \"user\": \"普通用户\",\n      \"label\": \"成员\",\n      \"list-title\": \"成员列表\"\n    },\n    \"my-account\": {\n      \"label\": \"我的账号\"\n    },\n    \"preference\": {\n      \"default-memo-sort-option\": \"备忘录显示时间\",\n      \"default-memo-visibility\": \"默认备忘录可见性\",\n      \"theme\": \"主题\",\n      \"label\": \"偏好设置\"\n    },\n    \"shortcut\": {\n      \"delete-confirm\": \"您确定要删除捷径 `{{title}}` 吗？\",\n      \"delete-success\": \"捷径 `{{title}}` 删除成功\"\n    },\n    \"sso\": {\n      \"authorization-endpoint\": \"授权端点（Authorization Endpoint）\",\n      \"client-id\": \"客户端ID（Client ID）\",\n      \"client-secret\": \"客户端密钥（Client Secret）\",\n      \"confirm-delete\": \"您确定要删除“{{name}}”单点登录配置吗？（此操作不可逆）\",\n      \"create-sso\": \"创建单点登录\",\n      \"custom\": \"自定义\",\n      \"delete-sso\": \"确认删除\",\n      \"disabled-password-login-warning\": \"密码登录已被禁用，删除身份提供程序时要格外小心\",\n      \"display-name\": \"显示名称\",\n      \"identifier\": \"标识符（Identifier）\",\n      \"identifier-filter\": \"标识符过滤器（Identifier Filter）\",\n      \"no-sso-found\": \"没有 SSO 配置\",\n      \"redirect-url\": \"重定向链接\",\n      \"scopes\": \"范围\",\n      \"single-sign-on\": \"配置单点登录（SSO）进行身份验证\",\n      \"sso-created\": \"单点登录 {{name}} 已创建\",\n      \"sso-list\": \"单点登录列表\",\n      \"sso-updated\": \"单点登录 {{name}} 已更新\",\n      \"template\": \"模板\",\n      \"token-endpoint\": \"令牌端点（Token Endpoint）\",\n      \"update-sso\": \"更新单点登录\",\n      \"user-endpoint\": \"用户端点（User Endpoint）\",\n      \"label\": \"单点登录\"\n    },\n    \"storage\": {\n      \"accesskey\": \"访问密钥（Access key）\",\n      \"accesskey-placeholder\": \"访问密钥 / 访问 ID\",\n      \"bucket\": \"储存桶（Bucket）\",\n      \"bucket-placeholder\": \"储存桶名\",\n      \"create-a-service\": \"新建服务\",\n      \"create-storage\": \"创建存储\",\n      \"current-storage\": \"当前对象存储\",\n      \"delete-storage\": \"删除存储\",\n      \"endpoint\": \"端点（Endpoint）\",\n      \"filepath-template\": \"文件路径模板\",\n      \"local-storage-path\": \"本地存储路径\",\n      \"path\": \"存储路径\",\n      \"path-description\": \"您可以使用本地存储中的相同动态变量，例如 {filename}\",\n      \"path-placeholder\": \"自定义路径\",\n      \"presign-placeholder\": \"预签名链接（可选）\",\n      \"region\": \"地区\",\n      \"region-placeholder\": \"区域名称\",\n      \"s3-compatible-url\": \"S3 兼容链接\",\n      \"secretkey\": \"私有密钥\",\n      \"secretkey-placeholder\": \"私有密钥 / 访问密钥\",\n      \"storage-services\": \"存储服务列表\",\n      \"type-database\": \"数据库\",\n      \"type-local\": \"本地文件系统\",\n      \"update-a-service\": \"更新服务\",\n      \"update-local-path\": \"更新本地存储路径\",\n      \"update-local-path-description\": \"本地存储路径是数据库文件的相对路径\",\n      \"update-storage\": \"更新存储\",\n      \"url-prefix\": \"链接前缀\",\n      \"url-prefix-placeholder\": \"自定义链接前缀，可选\",\n      \"url-suffix\": \"链接后缀\",\n      \"url-suffix-placeholder\": \"自定义链接后缀，可选\",\n      \"warning-text\": \"您确定要删除存储服务“{{name}}”吗？（此操作不可逆）\",\n      \"label\": \"存储\"\n    },\n    \"system\": {\n      \"additional-script\": \"自定义脚本\",\n      \"additional-script-placeholder\": \"自定义 JavaScript 代码\",\n      \"additional-style\": \"自定义样式\",\n      \"additional-style-placeholder\": \"自定义 CSS 代码\",\n      \"allow-user-signup\": \"允许用户注册\",\n      \"customize-server\": {\n        \"description\": \"描述\",\n        \"icon-url\": \"图标链接\",\n        \"locale\": \"服务器语言环境\",\n        \"title\": \"自定义服务器\"\n      },\n      \"disable-password-login\": \"禁用密码登录\",\n      \"disable-password-login-final-warning\": \"如果您知道自己在做什么，请输入 \\\"CONFIRM\\\"。\",\n      \"disable-password-login-warning\": \"所有用户将无法使用密码登录。如果配置的身份提供程序失效，不在数据库中恢复此设置将无法登录。删除身份提供程序时也要格外小心\",\n      \"display-with-updated-time\": \"根据最后修改时间顺序显示\",\n      \"enable-auto-compact\": \"启用自动超长折叠显示\",\n      \"enable-double-click-to-edit\": \"启用双击编辑\",\n      \"enable-password-login\": \"启用密码登录\",\n      \"enable-password-login-warning\": \"启用所有用户的密码登录。如果希望用户同时使用单点登录和密码登录，请开启密码登录\",\n      \"max-upload-size\": \"最大上传大小 (MiB)\",\n      \"max-upload-size-hint\": \"建议值为 32 MiB。\",\n      \"removed-completed-task-list-items\": \"启用移除已办\",\n      \"server-name\": \"服务器名称\",\n      \"title\": \"一般设置\",\n      \"label\": \"系统\"\n    },\n    \"version\": \"版本\",\n    \"access-token\": {\n      \"access-token-copied-to-clipboard\": \"访问令牌已复制到剪贴板\",\n      \"access-token-deleted\": \"访问令牌 `{{description}}` 已删除\",\n      \"access-token-deletion\": \"您确定要删除访问令牌 `{{description}}` 吗？\",\n      \"access-token-deletion-description\": \"此操作是不可逆的。您需要将所有正在使用该令牌的服务更新为新的访问令牌。\",\n      \"create-dialog\": {\n        \"access-token-created\": \"访问令牌 `{{description}}` 已创建\",\n        \"create-access-token\": \"创建令牌\",\n        \"created-at\": \"创建时间\",\n        \"description\": \"描述\",\n        \"duration-1m\": \"1 个月\",\n        \"duration-8h\": \"8 小时\",\n        \"duration-never\": \"从不\",\n        \"expiration\": \"过期时间\",\n        \"expires-at\": \"过期时间\",\n        \"some-description\": \"请输入描述...\"\n      },\n      \"description\": \"该账号下全部的访问令牌\",\n      \"title\": \"访问令牌\",\n      \"token\": \"令牌\"\n    },\n    \"account\": {\n      \"change-password\": \"修改密码\",\n      \"email-note\": \"可选\",\n      \"export-memos\": \"导出备忘录\",\n      \"nickname-note\": \"显示在横幅中\",\n      \"openapi-reset\": \"重置 OpenAPI 密钥（Key）\",\n      \"openapi-sample-post\": \"您好 #memos 来自 {{url}}\",\n      \"openapi-title\": \"OpenAPI 接口\",\n      \"reset-api\": \"重置 API\",\n      \"title\": \"账号信息\",\n      \"update-information\": \"更新个人信息\",\n      \"username-note\": \"用于登录\"\n    },\n    \"instance\": {\n      \"disallow-change-nickname\": \"禁止修改用户昵称\",\n      \"disallow-change-username\": \"禁止修改用户名\",\n      \"disallow-password-auth\": \"禁用密码登录\",\n      \"disallow-user-registration\": \"禁用用户注册\",\n      \"monday\": \"周一\",\n      \"saturday\": \"周六\",\n      \"sunday\": \"周日\",\n      \"week-start-day\": \"周开始日\"\n    },\n    \"memo\": {\n      \"content-length-limit\": \"内容长度限制（字节）\",\n      \"enable-blur-nsfw-content\": \"启用 NSFW 内容模糊处理（在下方添加 NSFW 标签）\",\n      \"enable-memo-comments\": \"启用备忘录评论\",\n      \"enable-memo-location\": \"启用备忘录定位\",\n      \"reactions\": \"表态\",\n      \"title\": \"备忘录相关设置\",\n      \"label\": \"备忘录\"\n    },\n    \"webhook\": {\n      \"create-dialog\": {\n        \"an-easy-to-remember-name\": \"请输入一个容易记住的标题\",\n        \"create-webhook\": \"创建 Webhook\",\n        \"create-webhook-success\": \"Webhook `{{name}}` 已创建\",\n        \"edit-webhook\": \"编辑 Webhook\",\n        \"payload-url\": \"请输入有效的 URL\",\n        \"title\": \"标题\",\n        \"url-example-post-receive\": \"https://example.com/postreceive\"\n      },\n      \"delete-dialog\": {\n        \"delete-webhook-description\": \"此操作是不可逆的。\",\n        \"delete-webhook-success\": \"Webhook `{{name}}` 删除成功\",\n        \"delete-webhook-title\": \"您确定要删除 Webhook `{{name}}` 吗？\"\n      },\n      \"no-webhooks-found\": \"没有 Webhook。\",\n      \"title\": \"Webhook\",\n      \"url\": \"链接\"\n    }\n  },\n  \"tag\": {\n    \"all-tags\": \"全部标签\",\n    \"create-tag\": \"创建标签\",\n    \"create-tags-guide\": \"您可以通过输入 “#标签” 创建标签。\",\n    \"delete-confirm\": \"您确定要删除此标签吗?所有相关的备忘录将会被归档。\",\n    \"delete-success\": \"标签删除成功\",\n    \"delete-tag\": \"删除标签\",\n    \"new-name\": \"新名称\",\n    \"no-tag-found\": \"没找到此标签\",\n    \"old-name\": \"旧名称\",\n    \"rename-error-empty\": \"新名称不能为空或包含空格\",\n    \"rename-error-repeat\": \"新名称不能与旧名称相同\",\n    \"rename-success\": \"重命名成功\",\n    \"rename-tag\": \"重命名\",\n    \"rename-tip\": \"您的所有带有此标签的备忘录将被更新。\"\n  },\n  \"tooltip\": {\n    \"link-memo\": \"链接备忘录\",\n    \"markdown-menu\": \"Markdown 菜单\",\n    \"select-location\": \"位置\",\n    \"select-visibility\": \"浏览权限\",\n    \"tags\": \"标签\",\n    \"upload-attachment\": \"上传附件\"\n  },\n  \"live-update\": {\n    \"connected\": \"已连接\",\n    \"connecting\": \"正在连接\",\n    \"disconnected\": \"已断开\"\n  }\n}\n"
  },
  {
    "path": "web/src/locales/zh-Hant.json",
    "content": "{\n  \"about\": {\n    \"blogs\": \"部落格\",\n    \"description\": \"以隱私為核心的輕量化筆記服務，輕鬆記錄並分享您的好點子。\",\n    \"documents\": \"文件\",\n    \"github-repository\": \"GitHub 儲存庫\",\n    \"official-website\": \"官方網站\"\n  },\n  \"auth\": {\n    \"create-your-account\": \"建立您的帳號\",\n    \"host-tip\": \"您即將註冊為網站管理員。\",\n    \"new-password\": \"新密碼\",\n    \"repeat-new-password\": \"再次輸入新密碼\",\n    \"sign-in-tip\": \"已經有帳戶了嗎？\",\n    \"sign-up-tip\": \"還沒有帳戶嗎？\"\n  },\n  \"common\": {\n    \"about\": \"關於\",\n    \"add\": \"新增\",\n    \"admin\": \"管理\",\n    \"all\": \"所有\",\n    \"archive\": \"封存\",\n    \"archived\": \"已封存\",\n    \"attachments\": \"附件\",\n    \"auto-expand\": \"自動展開\",\n    \"avatar\": \"頭像\",\n    \"basic\": \"基礎\",\n    \"beta\": \"測試版\",\n    \"calendar\": \"日曆\",\n    \"cancel\": \"取消\",\n    \"change\": \"變更\",\n    \"clear\": \"清除\",\n    \"close\": \"關閉\",\n    \"collapse\": \"摺疊\",\n    \"confirm\": \"確認\",\n    \"copy\": \"複製\",\n    \"create\": \"建立\",\n    \"created-at\": \"建立於\",\n    \"database\": \"資料庫\",\n    \"day\": \"日\",\n    \"days\": {\n      \"fri\": \"五\",\n      \"mon\": \"一\",\n      \"sat\": \"六\",\n      \"sun\": \"日\",\n      \"thu\": \"四\",\n      \"tue\": \"二\",\n      \"wed\": \"三\"\n    },\n    \"delete\": \"刪除\",\n    \"description\": \"說明\",\n    \"edit\": \"編輯\",\n    \"email\": \"信箱\",\n    \"expand\": \"展開\",\n    \"explore\": \"探索\",\n    \"file\": \"檔案\",\n    \"filter\": \"篩選\",\n    \"home\": \"首頁\",\n    \"image\": \"圖片\",\n    \"in\": \"在\",\n    \"inbox\": \"通知\",\n    \"input\": \"輸入\",\n    \"language\": \"語言\",\n    \"last-updated-at\": \"最後更新於\",\n    \"learn-more\": \"了解更多\",\n    \"link\": \"連結\",\n    \"map\": \"地圖\",\n    \"mark\": \"標記\",\n    \"memo\": \"備忘錄\",\n    \"memos\": \"備忘錄\",\n    \"more\": \"更多\",\n    \"name\": \"名稱\",\n    \"new\": \"新增\",\n    \"nickname\": \"暱稱\",\n    \"null\": \"空\",\n    \"or\": \"或\",\n    \"password\": \"密碼\",\n    \"pin\": \"置頂\",\n    \"pinned\": \"已置頂\",\n    \"preview\": \"預覽\",\n    \"profile\": \"個人檔案\",\n    \"properties\": \"屬性\",\n    \"referenced-by\": \"被引用\",\n    \"referencing\": \"引用\",\n    \"relations\": \"關聯\",\n    \"remember-me\": \"保持登入\",\n    \"rename\": \"重新命名\",\n    \"reset\": \"重設\",\n    \"resources\": \"檔案\",\n    \"restore\": \"還原\",\n    \"role\": \"身份\",\n    \"save\": \"儲存\",\n    \"search\": \"搜尋\",\n    \"select\": \"選擇\",\n    \"settings\": \"設定\",\n    \"share\": \"分享\",\n    \"shortcut-filter\": \"篩選條件表達式\",\n    \"shortcuts\": \"快捷篩選\",\n    \"sign-in\": \"登入\",\n    \"sign-in-with\": \"使用 {{provider}} 登入\",\n    \"sign-out\": \"登出\",\n    \"sign-up\": \"註冊\",\n    \"statistics\": \"統計\",\n    \"tags\": \"標籤\",\n    \"title\": \"標題\",\n    \"today\": \"今天\",\n    \"tree-mode\": \"樹狀顯示\",\n    \"type\": \"類型\",\n    \"unpin\": \"取消置頂\",\n    \"update\": \"更新\",\n    \"upload\": \"上傳\",\n    \"user\": \"使用者\",\n    \"username\": \"帳號\",\n    \"version\": \"版本\",\n    \"visibility\": \"瀏覽權限\",\n    \"yourself\": \"您自己\"\n  },\n  \"editor\": {\n    \"add-your-comment-here\": \"在這裡添加您的評論...\",\n    \"any-thoughts\": \"任何想法...\",\n    \"exit-focus-mode\": \"退出專注模式\",\n    \"focus-mode\": \"專注模式\",\n    \"no-changes-detected\": \"未發現變更\",\n    \"save\": \"儲存\",\n    \"saving\": \"儲存中...\",\n    \"slash-commands\": \"輸入 `/` 以使用指令\"\n  },\n  \"inbox\": {\n    \"failed-to-load\": \"載入失敗\",\n    \"memo-comment\": \"{{user}} 對您的 {{memo}} 發表了評論。\",\n    \"no-archived\": \"無已封存通知\",\n    \"no-unread\": \"無未讀通知\",\n    \"unread\": \"未讀\"\n  },\n  \"markdown\": {\n    \"checkbox\": \"核取方塊\",\n    \"code-block\": \"程式碼區塊\",\n    \"content-syntax\": \"內容語法\"\n  },\n  \"memo\": {\n    \"archived-at\": \"封存於\",\n    \"click-to-hide-nsfw-content\": \"點擊隱藏 NSFW 內容\",\n    \"click-to-show-nsfw-content\": \"點擊顯示 NSFW 內容\",\n    \"code\": \"程式碼\",\n    \"comment\": {\n      \"self\": \"評論\",\n      \"write-a-comment\": \"寫下評論\"\n    },\n    \"copy-content\": \"複製內容\",\n    \"copy-link\": \"複製連結\",\n    \"count-memos-in-date\": \"{{count}} 條{{memos}} 於 {{date}}\",\n    \"delete-confirm\": \"您確定要刪除此備忘錄嗎？\",\n    \"delete-confirm-description\": \"此操作無法恢復。附件、連結和引用將一併被移除。\",\n    \"direction\": \"排序\",\n    \"direction-asc\": \"升序\",\n    \"direction-desc\": \"降序\",\n    \"display-time\": \"顯示時間\",\n    \"filters\": {\n      \"has-code\": \"有程式碼\",\n      \"has-link\": \"有連結\",\n      \"has-task-list\": \"有待辦事項\"\n    },\n    \"links\": \"連結\",\n    \"load-more\": \"載入更多\",\n    \"no-archived-memos\": \"無已封存的備忘錄\",\n    \"no-memos\": \"無備忘錄\",\n    \"order-by\": \"排序\",\n    \"search-placeholder\": \"搜尋備忘錄\",\n    \"show-less\": \"顯示較少\",\n    \"show-more\": \"查看更多\",\n    \"to-do\": \"待辦事項\",\n    \"view-detail\": \"查看詳情\",\n    \"visibility\": {\n      \"disabled\": \"已停用公開備忘錄\",\n      \"private\": \"私人\",\n      \"protected\": \"成員\",\n      \"public\": \"公開\"\n    }\n  },\n  \"message\": {\n    \"archived-successfully\": \"封存成功\",\n    \"change-memo-created-time\": \"變更備忘錄建立時間\",\n    \"copied\": \"已複製\",\n    \"deleted-successfully\": \"刪除成功\",\n    \"description-is-required\": \"說明必填\",\n    \"failed-to-embed-memo\": \"嵌入備忘錄失敗\",\n    \"fill-all\": \"請填寫所有欄位。\",\n    \"fill-all-required-fields\": \"請填寫所有必填欄位\",\n    \"maximum-upload-size-is\": \"最大允許上傳大小為 {{size}} MiB\",\n    \"memo-not-found\": \"未找到備忘錄\",\n    \"new-password-not-match\": \"新密碼不一致。\",\n    \"no-data\": \"或許尋覓虛空，或者改換選擇之軌跡。\",\n    \"password-changed\": \"密碼變更完成\",\n    \"password-not-match\": \"密碼不一致。\",\n    \"restored-successfully\": \"還原成功\",\n    \"succeed-copy-content\": \"內容複製到剪貼簿成功。\",\n    \"succeed-copy-link\": \"複製連結到剪貼簿成功。\",\n    \"update-succeed\": \"更新成功\",\n    \"user-not-found\": \"未找到該使用者\"\n  },\n  \"reference\": {\n    \"add-references\": \"引用參考資料\",\n    \"embedded-usage\": \"作為嵌入內容使用\",\n    \"no-memos-found\": \"未找到任何備忘錄\",\n    \"search-placeholder\": \"搜尋內容\"\n  },\n  \"resource\": {\n    \"clear\": \"清除\",\n    \"copy-link\": \"複製連結\",\n    \"create-dialog\": {\n      \"external-link\": {\n        \"file-name\": \"檔案名稱\",\n        \"file-name-placeholder\": \"檔案名稱\",\n        \"link\": \"連結\",\n        \"link-placeholder\": \"您的資源連結\",\n        \"option\": \"外部連結\",\n        \"type\": \"類型\",\n        \"type-placeholder\": \"檔案類型\"\n      },\n      \"local-file\": {\n        \"choose\": \"選擇檔案...\",\n        \"option\": \"本機檔案\"\n      },\n      \"title\": \"新增資源\",\n      \"upload-method\": \"上傳方式\"\n    },\n    \"delete-all-unused\": \"刪除所有未使用的檔案\",\n    \"delete-all-unused-confirm\": \"您確定要刪除所有未使用的檔案嗎？此操作無法恢復。\",\n    \"delete-all-unused-error\": \"刪除未使用的檔案失敗\",\n    \"delete-all-unused-success\": \"檔案刪除成功\",\n    \"delete-resource\": \"刪除檔案\",\n    \"delete-selected-resources\": \"刪除所選的檔案\",\n    \"fetching-data\": \"抓取資料...\",\n    \"file-drag-drop-prompt\": \"將您的檔案拖曳到此處以上傳\",\n    \"linked-amount\": \"連結數量\",\n    \"no-files-selected\": \"未選取檔案\",\n    \"no-resources\": \"無檔案。\",\n    \"no-unused-resources\": \"無未使用的檔案\",\n    \"reset-link\": \"重設連結\",\n    \"reset-link-prompt\": \"您確定要重設連結嗎？這將導致當前使用的連結失效。此操作無法恢復。\",\n    \"reset-resource-link\": \"重設檔案連結\",\n    \"unused-resources\": \"未使用的檔案\"\n  },\n  \"router\": {\n    \"back-to-top\": \"回到頂端\",\n    \"go-to-home\": \"回到首頁\"\n  },\n  \"setting\": {\n    \"member\": {\n      \"admin\": \"管理者\",\n      \"archive-member\": \"封存使用者\",\n      \"archive-success\": \"{{username}} 封存成功\",\n      \"archive-warning\": \"您確定要封存 {{username}} 嗎？\",\n      \"archive-warning-description\": \"封存會停用該帳戶。您可於日後恢復或刪除該帳戶。\",\n      \"create-a-member\": \"新增使用者\",\n      \"delete-member\": \"刪除使用者\",\n      \"delete-success\": \"{{username}} 刪除成功\",\n      \"delete-warning\": \"您確定要刪除 {{username}} 嗎？\",\n      \"delete-warning-description\": \"此操作無法恢復。\",\n      \"restore-success\": \"{{username}} 恢復成功\",\n      \"user\": \"使用者\",\n      \"label\": \"使用者\",\n      \"list-title\": \"使用者列表\"\n    },\n    \"my-account\": {\n      \"label\": \"我的帳號\"\n    },\n    \"preference\": {\n      \"default-memo-sort-option\": \"備忘錄顯示時間\",\n      \"default-memo-visibility\": \"備忘錄預設瀏覽權限\",\n      \"theme\": \"主題\",\n      \"label\": \"偏好設定\"\n    },\n    \"shortcut\": {\n      \"delete-confirm\": \"您確定要刪除快捷篩選 `{{title}}` 嗎？\",\n      \"delete-success\": \"快捷篩選 `{{title}}` 刪除成功\"\n    },\n    \"sso\": {\n      \"authorization-endpoint\": \"驗證端點（Authorization Endpoint）\",\n      \"client-id\": \"客戶端 ID（Client ID）\",\n      \"client-secret\": \"客戶端金鑰（Client Secret）\",\n      \"confirm-delete\": \"您確定要刪除 `{{name}}` 的單點登錄 (SSO) 配置嗎？此操作無法恢復。\",\n      \"create-sso\": \"新增 SSO\",\n      \"custom\": \"自訂\",\n      \"delete-sso\": \"確認刪除\",\n      \"disabled-password-login-warning\": \"已停用密碼登入，删除身份提供程式時，請格外小心\",\n      \"display-name\": \"顯示名稱\",\n      \"identifier\": \"識別碼（Identifier）\",\n      \"identifier-filter\": \"識別碼篩選（Identifier Filter）\",\n      \"no-sso-found\": \"未設定 SSO\",\n      \"redirect-url\": \"重新導向網址\",\n      \"scopes\": \"範圍\",\n      \"single-sign-on\": \"設定單一登入 SSO 進行身份驗證\",\n      \"sso-created\": \"SSO {{name}} 新增成功\",\n      \"sso-list\": \"SSO 列表\",\n      \"sso-updated\": \"SSO {{name}} 更新成功\",\n      \"template\": \"範本\",\n      \"token-endpoint\": \"權杖端點（Token Endpoint）\",\n      \"update-sso\": \"更新 SSO\",\n      \"user-endpoint\": \"使用者端點（User Endpoint）\",\n      \"label\": \"單一登入\"\n    },\n    \"storage\": {\n      \"accesskey\": \"存取金鑰（Access key）\",\n      \"accesskey-placeholder\": \"存取金鑰 / 存取 ID\",\n      \"bucket\": \"儲存桶\",\n      \"bucket-placeholder\": \"Bucket 名稱\",\n      \"create-a-service\": \"建立服務\",\n      \"create-storage\": \"新增儲存空間\",\n      \"current-storage\": \"目前的儲存空間\",\n      \"delete-storage\": \"刪除儲存空間\",\n      \"endpoint\": \"端點（Endpoint）\",\n      \"filepath-template\": \"檔案路徑範本\",\n      \"local-storage-path\": \"本機儲存空間路徑\",\n      \"path\": \"儲存空間路徑\",\n      \"path-description\": \"您可以使用與本機儲存空間相同的動態變數，如 {filename}\",\n      \"path-placeholder\": \"自訂路徑\",\n      \"presign-placeholder\": \"Pre-sign URL（選填）\",\n      \"region\": \"地區\",\n      \"region-placeholder\": \"地區名稱\",\n      \"s3-compatible-url\": \"S3 相容網址\",\n      \"secretkey\": \"金鑰\",\n      \"secretkey-placeholder\": \"金鑰 / 存取金鑰\",\n      \"storage-services\": \"儲存服務列表\",\n      \"type-database\": \"資料庫\",\n      \"type-local\": \"本機\",\n      \"update-a-service\": \"更新服務\",\n      \"update-local-path\": \"更新本機儲存空間路徑\",\n      \"update-local-path-description\": \"本機儲存空間路徑為資料庫檔案的相對路徑\",\n      \"update-storage\": \"更新儲存空間\",\n      \"url-prefix\": \"網址前綴\",\n      \"url-prefix-placeholder\": \"自訂網址前綴（選填）\",\n      \"url-suffix\": \"網址後綴\",\n      \"url-suffix-placeholder\": \"自訂網址後綴（選填）\",\n      \"warning-text\": \"您確定要刪除存儲服務 `{{name}}` 嗎？此操作無法恢復。\",\n      \"label\": \"儲存空間\"\n    },\n    \"system\": {\n      \"additional-script\": \"自訂腳本\",\n      \"additional-script-placeholder\": \"自訂 JavaScript 代碼\",\n      \"additional-style\": \"自訂樣式\",\n      \"additional-style-placeholder\": \"自訂 CSS 代碼\",\n      \"allow-user-signup\": \"允許使用者註冊\",\n      \"customize-server\": {\n        \"description\": \"說明\",\n        \"icon-url\": \"圖示網址\",\n        \"locale\": \"語言\",\n        \"title\": \"自訂伺服器\"\n      },\n      \"disable-password-login\": \"停用密碼登入\",\n      \"disable-password-login-final-warning\": \"如果您知道自己在做什麼，請輸入 `CONFIRM`。\",\n      \"disable-password-login-warning\": \"所有使用者將無法使用密碼登入。如果設定的身份識別提供者失效，不在資料庫中恢復此設定將無法登入。刪除身分識別提供者時也要特別小心❗\",\n      \"display-with-updated-time\": \"顯示更新時間\",\n      \"enable-auto-compact\": \"啟用摺疊顯示\",\n      \"enable-double-click-to-edit\": \"啟用雙擊編輯\",\n      \"enable-password-login\": \"啟用密碼登入\",\n      \"enable-password-login-warning\": \"啟用所有使用者的密碼登入。如果希望使用者同時使用 SSO 和密碼登入，請啟用密碼登入\",\n      \"max-upload-size\": \"最大上傳檔案大小 (MiB)\",\n      \"max-upload-size-hint\": \"建議值為 32 MiB。\",\n      \"removed-completed-task-list-items\": \"啟用移除已完成待辦事項\",\n      \"server-name\": \"伺服器名稱\",\n      \"title\": \"系統設定\",\n      \"label\": \"系統\"\n    },\n    \"version\": \"版本\",\n    \"access-token\": {\n      \"access-token-copied-to-clipboard\": \"存取令牌已複製到剪貼簿\",\n      \"access-token-deleted\": \"存取令牌 `{{description}}` 已刪除\",\n      \"access-token-deletion\": \"您確定要刪除存取令牌 `{{description}}` 嗎？\",\n      \"access-token-deletion-description\": \"此操作無法恢復。您需要更新所有使用此令牌的服務以使用新的令牌。\",\n      \"create-dialog\": {\n        \"access-token-created\": \"存取令牌 `{{description}}` 已建立\",\n        \"create-access-token\": \"建立存取令牌\",\n        \"created-at\": \"建立於\",\n        \"description\": \"說明\",\n        \"duration-1m\": \"1 個月\",\n        \"duration-8h\": \"8 小時\",\n        \"duration-never\": \"永不過期\",\n        \"expiration\": \"過期時間\",\n        \"expires-at\": \"過期於\",\n        \"some-description\": \"請輸入說明...\"\n      },\n      \"description\": \"此處列出您帳號的所有存取令牌。\",\n      \"title\": \"存取令牌\",\n      \"token\": \"令牌\"\n    },\n    \"account\": {\n      \"change-password\": \"變更密碼\",\n      \"email-note\": \"選填\",\n      \"export-memos\": \"導出備忘錄\",\n      \"nickname-note\": \"顯示於橫幅\",\n      \"openapi-reset\": \"重設 OpenAPI 密鑰（Key）\",\n      \"openapi-sample-post\": \"哈囉，來自 {{url}} 的 #memos\",\n      \"openapi-title\": \"OpenAPI 介面\",\n      \"reset-api\": \"重設 API\",\n      \"title\": \"帳號資訊\",\n      \"update-information\": \"更新個人資訊\",\n      \"username-note\": \"用於登入\"\n    },\n    \"instance\": {\n      \"disallow-change-nickname\": \"禁止變更暱稱\",\n      \"disallow-change-username\": \"禁止變更使用者名稱\",\n      \"disallow-password-auth\": \"禁止使用密碼登入\",\n      \"disallow-user-registration\": \"禁止使用者註冊\",\n      \"monday\": \"星期一\",\n      \"saturday\": \"星期六\",\n      \"sunday\": \"星期日\",\n      \"week-start-day\": \"每週起始日\"\n    },\n    \"memo\": {\n      \"content-length-limit\": \"內容長度限制（位元組）\",\n      \"enable-blur-nsfw-content\": \"啟用 NSFW 內容模糊化（在下方添加 NSFW 標籤）\",\n      \"enable-memo-comments\": \"啟用備忘錄評論\",\n      \"enable-memo-location\": \"啟用備忘錄定位\",\n      \"reactions\": \"表情回應\",\n      \"title\": \"備忘錄相關設定\",\n      \"label\": \"備忘錄\"\n    },\n    \"webhook\": {\n      \"create-dialog\": {\n        \"an-easy-to-remember-name\": \"一個容易記住的名稱\",\n        \"create-webhook\": \"建立 Webhook\",\n        \"create-webhook-success\": \"Webhook `{{name}}` 已建立\",\n        \"edit-webhook\": \"編輯 Webhook\",\n        \"payload-url\": \"URL\",\n        \"title\": \"標題\",\n        \"url-example-post-receive\": \"https://範例.com/postreceive\"\n      },\n      \"delete-dialog\": {\n        \"delete-webhook-description\": \"此操作無法恢復。\",\n        \"delete-webhook-success\": \"Webhook `{{name}}` 刪除成功\",\n        \"delete-webhook-title\": \"您確定要刪除 webhook `{{name}}` 嗎？\"\n      },\n      \"no-webhooks-found\": \"尚未建立任何 Webhook。\",\n      \"title\": \"Webhook\",\n      \"url\": \"網址\"\n    }\n  },\n  \"tag\": {\n    \"all-tags\": \"所有標籤\",\n    \"create-tag\": \"建立標籤\",\n    \"create-tags-guide\": \"您可以通過輸入`#標籤`來建立標籤。\",\n    \"delete-confirm\": \"您確定要刪除此標籤嗎？所有關聯的備忘錄將會被封存\",\n    \"delete-success\": \"標籤刪除成功\",\n    \"delete-tag\": \"刪除標籤\",\n    \"new-name\": \"新標籤名稱\",\n    \"no-tag-found\": \"未找到標籤\",\n    \"old-name\": \"舊標籤名稱\",\n    \"rename-error-empty\": \"標籤名稱不能為空\",\n    \"rename-error-repeat\": \"標籤名稱已存在\",\n    \"rename-success\": \"重新命名標籤成功\",\n    \"rename-tag\": \"重新命名標籤\",\n    \"rename-tip\": \"您的標籤名稱將會被更新\"\n  },\n  \"tooltip\": {\n    \"link-memo\": \"連結備忘錄\",\n    \"markdown-menu\": \"Markdown 選單\",\n    \"select-location\": \"位置\",\n    \"select-visibility\": \"瀏覽權限\",\n    \"tags\": \"標籤\",\n    \"upload-attachment\": \"上傳附件\"\n  },\n  \"live-update\": {\n    \"connected\": \"已連接\",\n    \"connecting\": \"正在連接\",\n    \"disconnected\": \"已斷開\"\n  }\n}\n"
  },
  {
    "path": "web/src/main.tsx",
    "content": "import \"@github/relative-time-element\";\nimport { QueryClientProvider } from \"@tanstack/react-query\";\nimport { ReactQueryDevtools } from \"@tanstack/react-query-devtools\";\nimport React, { useEffect, useRef } from \"react\";\nimport { createRoot } from \"react-dom/client\";\nimport { Toaster } from \"react-hot-toast\";\nimport { RouterProvider } from \"react-router-dom\";\nimport \"./i18n\";\nimport \"./index.css\";\nimport { ErrorBoundary } from \"@/components/ErrorBoundary\";\nimport { refreshAccessToken } from \"@/connect\";\nimport { AuthProvider, useAuth } from \"@/contexts/AuthContext\";\nimport { InstanceProvider, useInstance } from \"@/contexts/InstanceContext\";\nimport { ViewProvider } from \"@/contexts/ViewContext\";\nimport { useLiveMemoRefresh } from \"@/hooks/useLiveMemoRefresh\";\nimport { useTokenRefreshOnFocus } from \"@/hooks/useTokenRefreshOnFocus\";\nimport { queryClient } from \"@/lib/query-client\";\nimport router from \"./router\";\nimport { applyLocaleEarly } from \"./utils/i18n\";\nimport { applyThemeEarly } from \"./utils/theme\";\nimport \"leaflet/dist/leaflet.css\";\nimport \"katex/dist/katex.min.css\";\n\n// Apply theme and locale early to prevent flash\napplyThemeEarly();\napplyLocaleEarly();\n\n// Inner component that initializes contexts\nfunction AppInitializer({ children }: { children: React.ReactNode }) {\n  const { isInitialized: authInitialized, initialize: initAuth, currentUser } = useAuth();\n  const { isInitialized: instanceInitialized, initialize: initInstance } = useInstance();\n  const initStartedRef = useRef(false);\n\n  // Initialize on mount - run in parallel for better performance\n  useEffect(() => {\n    if (initStartedRef.current) return;\n    initStartedRef.current = true;\n\n    const init = async () => {\n      await Promise.all([initInstance(), initAuth()]);\n    };\n    init();\n  }, [initAuth, initInstance]);\n\n  // Proactively refresh token on window focus to prevent 401 errors\n  // Only enabled when user is authenticated\n  // Related: https://github.com/usememos/memos/issues/5589\n  useTokenRefreshOnFocus(refreshAccessToken, !!currentUser);\n\n  // Live refresh: listen for memo changes via SSE and invalidate caches.\n  useLiveMemoRefresh();\n\n  if (!authInitialized || !instanceInitialized) {\n    return null;\n  }\n\n  return <>{children}</>;\n}\n\nfunction Main() {\n  return (\n    <ErrorBoundary>\n      <QueryClientProvider client={queryClient}>\n        <InstanceProvider>\n          <AuthProvider>\n            <ViewProvider>\n              <AppInitializer>\n                <RouterProvider router={router} />\n                <Toaster position=\"top-right\" />\n              </AppInitializer>\n            </ViewProvider>\n          </AuthProvider>\n        </InstanceProvider>\n        <ReactQueryDevtools initialIsOpen={false} />\n      </QueryClientProvider>\n    </ErrorBoundary>\n  );\n}\n\nconst container = document.getElementById(\"root\");\nconst root = createRoot(container as HTMLElement);\nroot.render(<Main />);\n"
  },
  {
    "path": "web/src/pages/AdminSignIn.tsx",
    "content": "import AuthFooter from \"@/components/AuthFooter\";\nimport PasswordSignInForm from \"@/components/PasswordSignInForm\";\nimport { useInstance } from \"@/contexts/InstanceContext\";\n\nconst AdminSignIn = () => {\n  const { generalSetting: instanceGeneralSetting } = useInstance();\n\n  return (\n    <div className=\"py-4 sm:py-8 w-80 max-w-full min-h-svh mx-auto flex flex-col justify-start items-center\">\n      <div className=\"w-full py-4 grow flex flex-col justify-center items-center\">\n        <div className=\"w-full flex flex-row justify-center items-center mb-6\">\n          <img className=\"h-14 w-auto rounded-full shadow\" src={instanceGeneralSetting.customProfile?.logoUrl || \"/logo.webp\"} alt=\"\" />\n          <p className=\"ml-2 text-5xl text-foreground opacity-80\">{instanceGeneralSetting.customProfile?.title || \"Memos\"}</p>\n        </div>\n        <p className=\"w-full text-xl font-medium text-muted-foreground\">Sign in with admin accounts</p>\n        <PasswordSignInForm />\n      </div>\n      <AuthFooter />\n    </div>\n  );\n};\n\nexport default AdminSignIn;\n"
  },
  {
    "path": "web/src/pages/Archived.tsx",
    "content": "import MemoView from \"@/components/MemoView\";\nimport PagedMemoList from \"@/components/PagedMemoList\";\nimport { useMemoFilters, useMemoSorting } from \"@/hooks\";\nimport useCurrentUser from \"@/hooks/useCurrentUser\";\nimport { State } from \"@/types/proto/api/v1/common_pb\";\nimport { Memo } from \"@/types/proto/api/v1/memo_service_pb\";\n\nconst Archived = () => {\n  const user = useCurrentUser();\n\n  // Build filter using unified hook (no shortcuts or pinned filter)\n  const memoFilter = useMemoFilters({\n    creatorName: user?.name,\n    includeShortcuts: false,\n    includePinned: false,\n  });\n\n  // Get sorting logic using unified hook (pinned first, archived state)\n  const { listSort, orderBy } = useMemoSorting({\n    pinnedFirst: true,\n    state: State.ARCHIVED,\n  });\n\n  return (\n    <PagedMemoList\n      renderer={(memo: Memo) => <MemoView key={`${memo.name}-${memo.updateTime}`} memo={memo} showVisibility compact />}\n      listSort={listSort}\n      state={State.ARCHIVED}\n      orderBy={orderBy}\n      filter={memoFilter}\n    />\n  );\n};\n\nexport default Archived;\n"
  },
  {
    "path": "web/src/pages/Attachments.tsx",
    "content": "import { timestampDate } from \"@bufbuild/protobuf/wkt\";\nimport dayjs from \"dayjs\";\nimport { ExternalLinkIcon, PaperclipIcon, SearchIcon, Trash } from \"lucide-react\";\nimport { useCallback, useEffect, useMemo, useState } from \"react\";\nimport { toast } from \"react-hot-toast\";\nimport { Link } from \"react-router-dom\";\nimport AttachmentIcon from \"@/components/AttachmentIcon\";\nimport ConfirmDialog from \"@/components/ConfirmDialog\";\nimport Empty from \"@/components/Empty\";\nimport MobileHeader from \"@/components/MobileHeader\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { Separator } from \"@/components/ui/separator\";\nimport { attachmentServiceClient } from \"@/connect\";\nimport { useDeleteAttachment } from \"@/hooks/useAttachmentQueries\";\nimport useDialog from \"@/hooks/useDialog\";\nimport useLoading from \"@/hooks/useLoading\";\nimport useMediaQuery from \"@/hooks/useMediaQuery\";\nimport i18n from \"@/i18n\";\nimport { handleError } from \"@/lib/error\";\nimport type { Attachment } from \"@/types/proto/api/v1/attachment_service_pb\";\nimport { useTranslate } from \"@/utils/i18n\";\n\nconst PAGE_SIZE = 50;\n\nconst groupAttachmentsByDate = (attachments: Attachment[]): Map<string, Attachment[]> => {\n  const grouped = new Map<string, Attachment[]>();\n  const sorted = [...attachments].sort((a, b) => {\n    const aTime = a.createTime ? timestampDate(a.createTime) : undefined;\n    const bTime = b.createTime ? timestampDate(b.createTime) : undefined;\n    return dayjs(bTime).unix() - dayjs(aTime).unix();\n  });\n\n  for (const attachment of sorted) {\n    const createTime = attachment.createTime ? timestampDate(attachment.createTime) : undefined;\n    const monthKey = dayjs(createTime).format(\"YYYY-MM\");\n    const group = grouped.get(monthKey) ?? [];\n    group.push(attachment);\n    grouped.set(monthKey, group);\n  }\n\n  return grouped;\n};\n\nconst filterAttachments = (attachments: Attachment[], searchQuery: string): Attachment[] => {\n  if (!searchQuery.trim()) return attachments;\n  const query = searchQuery.toLowerCase();\n  return attachments.filter((attachment) => attachment.filename.toLowerCase().includes(query));\n};\n\ninterface AttachmentItemProps {\n  attachment: Attachment;\n}\n\nconst AttachmentItem = ({ attachment }: AttachmentItemProps) => (\n  <div className=\"w-24 sm:w-32 h-auto flex flex-col justify-start items-start\">\n    <div className=\"w-24 h-24 flex justify-center items-center sm:w-32 sm:h-32 border border-border overflow-clip rounded-xl cursor-pointer hover:shadow hover:opacity-80\">\n      <AttachmentIcon attachment={attachment} strokeWidth={0.5} />\n    </div>\n    <div className=\"w-full max-w-full flex flex-row justify-between items-center mt-1 px-1\">\n      <p className=\"text-xs shrink text-muted-foreground truncate\">{attachment.filename}</p>\n      {attachment.memo && (\n        <Link to={`/${attachment.memo}`} className=\"text-primary hover:opacity-80 transition-opacity shrink-0 ml-1\" aria-label=\"View memo\">\n          <ExternalLinkIcon className=\"w-3 h-3\" />\n        </Link>\n      )}\n    </div>\n  </div>\n);\n\nconst Attachments = () => {\n  const t = useTranslate();\n  const md = useMediaQuery(\"md\");\n  const loadingState = useLoading();\n  const deleteUnusedAttachmentsDialog = useDialog();\n  const { mutateAsync: deleteAttachment } = useDeleteAttachment();\n\n  const [searchQuery, setSearchQuery] = useState(\"\");\n  const [attachments, setAttachments] = useState<Attachment[]>([]);\n  const [nextPageToken, setNextPageToken] = useState(\"\");\n  const [isLoadingMore, setIsLoadingMore] = useState(false);\n\n  // Memoized computed values\n  const filteredAttachments = useMemo(() => filterAttachments(attachments, searchQuery), [attachments, searchQuery]);\n\n  const usedAttachments = useMemo(() => filteredAttachments.filter((attachment) => attachment.memo), [filteredAttachments]);\n\n  const unusedAttachments = useMemo(() => filteredAttachments.filter((attachment) => !attachment.memo), [filteredAttachments]);\n\n  const groupedAttachments = useMemo(() => groupAttachmentsByDate(usedAttachments), [usedAttachments]);\n\n  // Fetch initial attachments\n  useEffect(() => {\n    const fetchInitialAttachments = async () => {\n      try {\n        const { attachments: fetchedAttachments, nextPageToken } = await attachmentServiceClient.listAttachments({\n          pageSize: PAGE_SIZE,\n        });\n        setAttachments(fetchedAttachments);\n        setNextPageToken(nextPageToken ?? \"\");\n      } catch (error) {\n        handleError(error, toast.error, {\n          context: \"Failed to fetch attachments\",\n          fallbackMessage: \"Failed to load attachments. Please try again.\",\n        });\n      } finally {\n        loadingState.setFinish();\n      }\n    };\n\n    fetchInitialAttachments();\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, []);\n\n  // Load more attachments with pagination\n  const handleLoadMore = useCallback(async () => {\n    if (!nextPageToken || isLoadingMore) return;\n\n    setIsLoadingMore(true);\n    try {\n      const { attachments: fetchedAttachments, nextPageToken: newPageToken } = await attachmentServiceClient.listAttachments({\n        pageSize: PAGE_SIZE,\n        pageToken: nextPageToken,\n      });\n      setAttachments((prev) => [...prev, ...fetchedAttachments]);\n      setNextPageToken(newPageToken ?? \"\");\n    } catch (error) {\n      handleError(error, toast.error, {\n        context: \"Failed to load more attachments\",\n        fallbackMessage: \"Failed to load more attachments. Please try again.\",\n      });\n    } finally {\n      setIsLoadingMore(false);\n    }\n  }, [nextPageToken, isLoadingMore]);\n\n  // Refetch all attachments from the beginning\n  const handleRefetch = useCallback(async () => {\n    try {\n      loadingState.setLoading();\n      const { attachments: fetchedAttachments, nextPageToken } = await attachmentServiceClient.listAttachments({\n        pageSize: PAGE_SIZE,\n      });\n      setAttachments(fetchedAttachments);\n      setNextPageToken(nextPageToken ?? \"\");\n      loadingState.setFinish();\n    } catch (error) {\n      handleError(error, toast.error, {\n        context: \"Failed to refetch attachments\",\n        fallbackMessage: \"Failed to refresh attachments. Please try again.\",\n        onError: () => loadingState.setError(),\n      });\n    }\n  }, [loadingState]);\n\n  // Delete all unused attachments\n  const handleDeleteUnusedAttachments = useCallback(async () => {\n    try {\n      let allUnusedAttachments: Attachment[] = [];\n      let nextPageToken = \"\";\n      do {\n        const response = await attachmentServiceClient.listAttachments({\n          pageSize: 1000,\n          pageToken: nextPageToken,\n          filter: \"memo_id == null\",\n        });\n        allUnusedAttachments = [...allUnusedAttachments, ...response.attachments];\n        nextPageToken = response.nextPageToken;\n      } while (nextPageToken);\n\n      await Promise.all(allUnusedAttachments.map((attachment) => deleteAttachment(attachment.name)));\n      toast.success(t(\"resource.delete-all-unused-success\"));\n    } catch (error) {\n      handleError(error, toast.error, {\n        context: \"Failed to delete unused attachments\",\n        fallbackMessage: t(\"resource.delete-all-unused-error\"),\n      });\n    } finally {\n      await handleRefetch();\n    }\n  }, [t, handleRefetch, deleteAttachment]);\n\n  // Handle search input change\n  const handleSearchChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {\n    setSearchQuery(e.target.value);\n  }, []);\n\n  return (\n    <section className=\"@container w-full max-w-5xl min-h-full flex flex-col justify-start items-center sm:pt-3 md:pt-6 pb-8\">\n      {!md && <MobileHeader />}\n      <div className=\"w-full px-4 sm:px-6\">\n        <div className=\"w-full border border-border flex flex-col justify-start items-start px-4 py-3 rounded-xl bg-background text-foreground\">\n          <div className=\"relative w-full flex flex-row justify-between items-center\">\n            <p className=\"py-1 flex flex-row justify-start items-center select-none opacity-80\">\n              <PaperclipIcon className=\"w-6 h-auto mr-1 opacity-80\" />\n              <span className=\"text-lg\">{t(\"common.attachments\")}</span>\n            </p>\n            <div>\n              <div className=\"relative max-w-32\">\n                <SearchIcon className=\"absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-muted-foreground\" />\n                <Input className=\"pl-9\" placeholder={t(\"common.search\")} value={searchQuery} onChange={handleSearchChange} />\n              </div>\n            </div>\n          </div>\n          <div className=\"w-full flex flex-col justify-start items-start mt-4 mb-6\">\n            {loadingState.isLoading ? (\n              <div className=\"w-full h-32 flex flex-col justify-center items-center\">\n                <p className=\"w-full text-center text-base my-6 mt-8\">{t(\"resource.fetching-data\")}</p>\n              </div>\n            ) : (\n              <>\n                {filteredAttachments.length === 0 ? (\n                  <div className=\"w-full mt-8 mb-8 flex flex-col justify-center items-center italic\">\n                    <Empty />\n                    <p className=\"mt-4 text-muted-foreground\">{t(\"message.no-data\")}</p>\n                  </div>\n                ) : (\n                  <>\n                    <div className={\"w-full h-auto px-2 flex flex-col justify-start items-start gap-y-8\"}>\n                      {Array.from(groupedAttachments.entries()).map(([monthStr, attachments]) => {\n                        return (\n                          <div key={monthStr} className=\"w-full flex flex-row justify-start items-start\">\n                            <div className=\"w-16 sm:w-24 pt-4 sm:pl-4 flex flex-col justify-start items-start\">\n                              <span className=\"text-sm opacity-60\">{dayjs(monthStr).year()}</span>\n                              <span className=\"font-medium text-xl\">\n                                {dayjs(monthStr).toDate().toLocaleString(i18n.language, { month: \"short\" })}\n                              </span>\n                            </div>\n                            <div className=\"w-full max-w-[calc(100%-4rem)] sm:max-w-[calc(100%-6rem)] flex flex-row justify-start items-start gap-4 flex-wrap\">\n                              {attachments.map((attachment) => (\n                                <AttachmentItem key={attachment.name} attachment={attachment} />\n                              ))}\n                            </div>\n                          </div>\n                        );\n                      })}\n\n                      {unusedAttachments.length > 0 && (\n                        <>\n                          <Separator />\n                          <div className=\"w-full flex flex-row justify-start items-start\">\n                            <div className=\"w-16 sm:w-24 sm:pl-4 flex flex-col justify-start items-start\"></div>\n                            <div className=\"w-full max-w-[calc(100%-4rem)] sm:max-w-[calc(100%-6rem)] flex flex-row justify-start items-start gap-4 flex-wrap\">\n                              <div className=\"w-full flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2\">\n                                <div className=\"flex flex-row items-center gap-2\">\n                                  <span className=\"text-muted-foreground\">{t(\"resource.unused-resources\")}</span>\n                                  <span className=\"text-muted-foreground opacity-80\">({unusedAttachments.length})</span>\n                                </div>\n                                <div>\n                                  <Button variant=\"destructive\" onClick={() => deleteUnusedAttachmentsDialog.open()} size=\"sm\">\n                                    <Trash />\n                                    {t(\"resource.delete-all-unused\")}\n                                  </Button>\n                                </div>\n                              </div>\n                              {unusedAttachments.map((attachment) => (\n                                <AttachmentItem key={attachment.name} attachment={attachment} />\n                              ))}\n                            </div>\n                          </div>\n                        </>\n                      )}\n                    </div>\n                    {nextPageToken && (\n                      <div className=\"w-full flex flex-row justify-center items-center mt-4\">\n                        <Button variant=\"outline\" size=\"sm\" onClick={handleLoadMore} disabled={isLoadingMore}>\n                          {isLoadingMore ? t(\"resource.fetching-data\") : t(\"memo.load-more\")}\n                        </Button>\n                      </div>\n                    )}\n                  </>\n                )}\n              </>\n            )}\n          </div>\n        </div>\n      </div>\n\n      <ConfirmDialog\n        open={deleteUnusedAttachmentsDialog.isOpen}\n        onOpenChange={deleteUnusedAttachmentsDialog.setOpen}\n        title={t(\"resource.delete-all-unused-confirm\")}\n        confirmLabel={t(\"common.delete\")}\n        cancelLabel={t(\"common.cancel\")}\n        onConfirm={handleDeleteUnusedAttachments}\n        confirmVariant=\"destructive\"\n      />\n    </section>\n  );\n};\n\nexport default Attachments;\n"
  },
  {
    "path": "web/src/pages/AuthCallback.tsx",
    "content": "import { timestampDate } from \"@bufbuild/protobuf/wkt\";\nimport { useEffect, useRef, useState } from \"react\";\nimport { useSearchParams } from \"react-router-dom\";\nimport { setAccessToken } from \"@/auth-state\";\nimport { authServiceClient } from \"@/connect\";\nimport { useAuth } from \"@/contexts/AuthContext\";\nimport { absolutifyLink } from \"@/helpers/utils\";\nimport useNavigateTo from \"@/hooks/useNavigateTo\";\nimport { handleError } from \"@/lib/error\";\nimport { validateOAuthState } from \"@/utils/oauth\";\n\ninterface State {\n  loading: boolean;\n  errorMessage: string;\n}\n\nconst AuthCallback = () => {\n  const navigateTo = useNavigateTo();\n  const { initialize } = useAuth();\n  const [searchParams] = useSearchParams();\n  const handledRef = useRef(false);\n  const [state, setState] = useState<State>({\n    loading: true,\n    errorMessage: \"\",\n  });\n\n  useEffect(() => {\n    if (handledRef.current) {\n      return;\n    }\n    handledRef.current = true;\n    // Check for OAuth error response first (e.g., user denied access)\n    const error = searchParams.get(\"error\");\n    const errorDescription = searchParams.get(\"error_description\");\n    const errorUri = searchParams.get(\"error_uri\");\n\n    if (error) {\n      // OAuth provider returned an error\n      let errorMessage = `OAuth error: ${error}`;\n      if (errorDescription) {\n        errorMessage += `\\n${decodeURIComponent(errorDescription)}`;\n      }\n      if (errorUri) {\n        errorMessage += `\\nMore info: ${errorUri}`;\n      }\n\n      setState({\n        loading: false,\n        errorMessage,\n      });\n      return;\n    }\n\n    const code = searchParams.get(\"code\");\n    const state = searchParams.get(\"state\");\n\n    if (!code || !state) {\n      setState({\n        loading: false,\n        errorMessage: \"Failed to authorize. Missing authorization code or state parameter.\",\n      });\n      return;\n    }\n\n    // Validate OAuth state (CSRF protection) and retrieve PKCE code_verifier\n    const validatedState = validateOAuthState(state);\n    if (!validatedState) {\n      setState({\n        loading: false,\n        errorMessage: \"Failed to authorize. Invalid or expired state parameter. This may indicate a CSRF attack attempt.\",\n      });\n      return;\n    }\n\n    const { identityProviderName, returnUrl, codeVerifier } = validatedState;\n    const redirectUri = absolutifyLink(\"/auth/callback\");\n\n    (async () => {\n      try {\n        const response = await authServiceClient.signIn({\n          credentials: {\n            case: \"ssoCredentials\",\n            value: {\n              idpName: identityProviderName,\n              code,\n              redirectUri,\n              codeVerifier: codeVerifier || \"\", // Pass PKCE code_verifier for token exchange\n            },\n          },\n        });\n        // Store access token from login response\n        if (response.accessToken) {\n          setAccessToken(response.accessToken, response.accessTokenExpiresAt ? timestampDate(response.accessTokenExpiresAt) : undefined);\n        }\n        setState({\n          loading: false,\n          errorMessage: \"\",\n        });\n        await initialize();\n        // Redirect to return URL if specified, otherwise home\n        navigateTo(returnUrl || \"/\");\n      } catch (error: unknown) {\n        handleError(error, () => {}, {\n          fallbackMessage: \"Failed to authenticate.\",\n          onError: (err) => {\n            const message = err instanceof Error ? err.message : \"Failed to authenticate.\";\n            setState({\n              loading: false,\n              errorMessage: message,\n            });\n          },\n        });\n      }\n    })();\n  }, [searchParams, navigateTo]);\n\n  if (state.loading) return null;\n\n  return (\n    <div className=\"p-4 py-24 w-full h-full flex justify-center items-center\">\n      <div className=\"max-w-lg font-mono whitespace-pre-wrap opacity-80\">{state.errorMessage}</div>\n    </div>\n  );\n};\n\nexport default AuthCallback;\n"
  },
  {
    "path": "web/src/pages/Explore.tsx",
    "content": "import MemoView from \"@/components/MemoView\";\nimport PagedMemoList from \"@/components/PagedMemoList\";\nimport { useMemoFilters, useMemoSorting } from \"@/hooks\";\nimport useCurrentUser from \"@/hooks/useCurrentUser\";\nimport { State } from \"@/types/proto/api/v1/common_pb\";\nimport { Memo, Visibility } from \"@/types/proto/api/v1/memo_service_pb\";\n\nconst Explore = () => {\n  const currentUser = useCurrentUser();\n\n  // Determine visibility filter based on authentication status\n  // - Logged-in users: Can see PUBLIC and PROTECTED memos\n  // - Visitors: Can only see PUBLIC memos\n  // Note: The backend is responsible for filtering stats based on visibility permissions.\n  const visibilities = currentUser ? [Visibility.PUBLIC, Visibility.PROTECTED] : [Visibility.PUBLIC];\n\n  // Build filter using unified hook (no creator scoping for Explore)\n  const memoFilter = useMemoFilters({\n    includeShortcuts: false,\n    includePinned: false,\n    visibilities,\n  });\n\n  // Get sorting logic using unified hook (no pinned sorting)\n  const { listSort, orderBy } = useMemoSorting({\n    pinnedFirst: false,\n    state: State.NORMAL,\n  });\n\n  return (\n    <PagedMemoList\n      renderer={(memo: Memo) => <MemoView key={`${memo.name}-${memo.updateTime}`} memo={memo} showCreator showVisibility compact />}\n      listSort={listSort}\n      orderBy={orderBy}\n      filter={memoFilter}\n      showCreator\n    />\n  );\n};\n\nexport default Explore;\n"
  },
  {
    "path": "web/src/pages/Home.tsx",
    "content": "import MemoView from \"@/components/MemoView\";\nimport PagedMemoList from \"@/components/PagedMemoList\";\nimport { useInstance } from \"@/contexts/InstanceContext\";\nimport { useMemoFilters, useMemoSorting } from \"@/hooks\";\nimport useCurrentUser from \"@/hooks/useCurrentUser\";\nimport { State } from \"@/types/proto/api/v1/common_pb\";\nimport { Memo } from \"@/types/proto/api/v1/memo_service_pb\";\n\nconst Home = () => {\n  const user = useCurrentUser();\n  const { isInitialized } = useInstance();\n\n  const memoFilter = useMemoFilters({\n    creatorName: user?.name,\n    includeShortcuts: true,\n    includePinned: true,\n  });\n\n  const { listSort, orderBy } = useMemoSorting({\n    pinnedFirst: true,\n    state: State.NORMAL,\n  });\n\n  return (\n    <div className=\"w-full min-h-full bg-background text-foreground\">\n      <PagedMemoList\n        renderer={(memo: Memo) => <MemoView key={`${memo.name}-${memo.displayTime}`} memo={memo} showVisibility showPinned compact />}\n        listSort={listSort}\n        orderBy={orderBy}\n        filter={memoFilter}\n        enabled={isInitialized}\n      />\n    </div>\n  );\n};\n\nexport default Home;\n"
  },
  {
    "path": "web/src/pages/Inboxes.tsx",
    "content": "import { timestampDate } from \"@bufbuild/protobuf/wkt\";\nimport { sortBy } from \"lodash-es\";\nimport { ArchiveIcon, BellIcon, InboxIcon } from \"lucide-react\";\nimport { useState } from \"react\";\nimport Empty from \"@/components/Empty\";\nimport MemoCommentMessage from \"@/components/Inbox/MemoCommentMessage\";\nimport MobileHeader from \"@/components/MobileHeader\";\nimport useMediaQuery from \"@/hooks/useMediaQuery\";\nimport { useNotifications } from \"@/hooks/useUserQueries\";\nimport { cn } from \"@/lib/utils\";\nimport { UserNotification, UserNotification_Status, UserNotification_Type } from \"@/types/proto/api/v1/user_service_pb\";\nimport { useTranslate } from \"@/utils/i18n\";\n\nconst Inboxes = () => {\n  const t = useTranslate();\n  const md = useMediaQuery(\"md\");\n  const [filter, setFilter] = useState<\"all\" | \"unread\" | \"archived\">(\"all\");\n\n  // Fetch notifications with React Query\n  const { data: fetchedNotifications = [] } = useNotifications();\n\n  const allNotifications = sortBy(fetchedNotifications, (notification: UserNotification) => {\n    return -((notification.createTime ? timestampDate(notification.createTime) : undefined)?.getTime() || 0);\n  });\n\n  const notifications = allNotifications.filter((notification) => {\n    if (filter === \"unread\") return notification.status === UserNotification_Status.UNREAD;\n    if (filter === \"archived\") return notification.status === UserNotification_Status.ARCHIVED;\n    return true;\n  });\n\n  const unreadCount = allNotifications.filter((n) => n.status === UserNotification_Status.UNREAD).length;\n  const archivedCount = allNotifications.filter((n) => n.status === UserNotification_Status.ARCHIVED).length;\n\n  return (\n    <section className=\"@container w-full max-w-5xl min-h-full flex flex-col justify-start items-center sm:pt-3 md:pt-6 pb-8\">\n      {!md && <MobileHeader />}\n      <div className=\"w-full px-4 sm:px-6\">\n        <div className=\"w-full border border-border flex flex-col justify-start items-start rounded-xl bg-background text-foreground overflow-hidden\">\n          {/* Header */}\n          <div className=\"w-full px-4 py-4 border-b border-border\">\n            <div className=\"flex flex-row justify-between items-center\">\n              <div className=\"flex flex-row items-center gap-2\">\n                <BellIcon className=\"w-5 h-auto text-muted-foreground\" />\n                <h1 className=\"text-xl font-semibold\">{t(\"common.inbox\")}</h1>\n                {unreadCount > 0 && (\n                  <span className=\"ml-1 px-2 py-0.5 text-xs font-medium rounded-full bg-primary text-primary-foreground\">\n                    {unreadCount}\n                  </span>\n                )}\n              </div>\n            </div>\n          </div>\n\n          {/* Filter Tabs */}\n          <div className=\"w-full px-4 py-2 border-b border-border bg-muted/30\">\n            <div className=\"flex flex-row gap-1\">\n              <button\n                onClick={() => setFilter(\"all\")}\n                className={cn(\n                  \"px-3 py-1.5 text-sm font-medium rounded-md transition-colors\",\n                  filter === \"all\"\n                    ? \"bg-background text-foreground shadow-sm\"\n                    : \"text-muted-foreground hover:text-foreground hover:bg-background/50\",\n                )}\n              >\n                {t(\"common.all\")} ({allNotifications.length})\n              </button>\n              <button\n                onClick={() => setFilter(\"unread\")}\n                className={cn(\n                  \"px-3 py-1.5 text-sm font-medium rounded-md transition-colors flex items-center gap-1.5\",\n                  filter === \"unread\"\n                    ? \"bg-background text-foreground shadow-sm\"\n                    : \"text-muted-foreground hover:text-foreground hover:bg-background/50\",\n                )}\n              >\n                <InboxIcon className=\"w-3.5 h-auto\" />\n                {t(\"inbox.unread\")} ({unreadCount})\n              </button>\n              <button\n                onClick={() => setFilter(\"archived\")}\n                className={cn(\n                  \"px-3 py-1.5 text-sm font-medium rounded-md transition-colors flex items-center gap-1.5\",\n                  filter === \"archived\"\n                    ? \"bg-background text-foreground shadow-sm\"\n                    : \"text-muted-foreground hover:text-foreground hover:bg-background/50\",\n                )}\n              >\n                <ArchiveIcon className=\"w-3.5 h-auto\" />\n                {t(\"common.archived\")} ({archivedCount})\n              </button>\n            </div>\n          </div>\n\n          {/* Notifications List */}\n          <div className=\"w-full\">\n            {notifications.length === 0 ? (\n              <div className=\"w-full py-16 flex flex-col justify-center items-center\">\n                <Empty />\n                <p className=\"mt-4 text-sm text-muted-foreground\">\n                  {filter === \"unread\" ? t(\"inbox.no-unread\") : filter === \"archived\" ? t(\"inbox.no-archived\") : t(\"message.no-data\")}\n                </p>\n              </div>\n            ) : (\n              <div className=\"flex flex-col\">\n                {notifications.map((notification: UserNotification) => {\n                  if (notification.type === UserNotification_Type.MEMO_COMMENT) {\n                    return <MemoCommentMessage key={notification.name} notification={notification} />;\n                  }\n                  return null;\n                })}\n              </div>\n            )}\n          </div>\n        </div>\n      </div>\n    </section>\n  );\n};\n\nexport default Inboxes;\n"
  },
  {
    "path": "web/src/pages/MemoDetail.tsx",
    "content": "import { ConnectError } from \"@connectrpc/connect\";\nimport { ArrowUpLeftFromCircleIcon, MessageCircleIcon } from \"lucide-react\";\nimport { useEffect, useState } from \"react\";\nimport { toast } from \"react-hot-toast\";\nimport { Link, useLocation, useParams } from \"react-router-dom\";\nimport { MemoDetailSidebar, MemoDetailSidebarDrawer } from \"@/components/MemoDetailSidebar\";\nimport MemoEditor from \"@/components/MemoEditor\";\nimport MemoView from \"@/components/MemoView\";\nimport MobileHeader from \"@/components/MobileHeader\";\nimport { Button } from \"@/components/ui/button\";\nimport { extractMemoIdFromName, memoNamePrefix } from \"@/helpers/resource-names\";\nimport useCurrentUser from \"@/hooks/useCurrentUser\";\nimport useMediaQuery from \"@/hooks/useMediaQuery\";\nimport { useMemo, useMemoComments } from \"@/hooks/useMemoQueries\";\nimport useNavigateTo from \"@/hooks/useNavigateTo\";\nimport { cn } from \"@/lib/utils\";\nimport { useTranslate } from \"@/utils/i18n\";\n\nconst MemoDetail = () => {\n  const t = useTranslate();\n  const md = useMediaQuery(\"md\");\n  const params = useParams();\n  const navigateTo = useNavigateTo();\n  const { state: locationState } = useLocation();\n  const currentUser = useCurrentUser();\n  const uid = params.uid;\n  const memoName = `${memoNamePrefix}${uid}`;\n  const [showCommentEditor, setShowCommentEditor] = useState(false);\n\n  // Fetch main memo with React Query\n  const { data: memo, error, isLoading } = useMemo(memoName, { enabled: !!memoName });\n\n  // Handle errors\n  if (error) {\n    toast.error((error as ConnectError).message);\n    navigateTo(\"/403\");\n  }\n\n  // Fetch parent memo if exists\n  const { data: parentMemo } = useMemo(memo?.parent || \"\", {\n    enabled: !!memo?.parent,\n  });\n\n  // Fetch all comments for this memo in a single query\n  const { data: commentsResponse } = useMemoComments(memoName, {\n    enabled: !!memo,\n  });\n  const comments = commentsResponse?.memos || [];\n\n  const { hash } = useLocation();\n  useEffect(() => {\n    if (!hash || comments.length === 0) return;\n    const el = document.getElementById(hash.slice(1));\n    el?.scrollIntoView({ behavior: \"smooth\", block: \"center\" });\n  }, [hash, comments]);\n\n  const showCreateCommentButton = currentUser && !showCommentEditor;\n\n  if (isLoading || !memo) {\n    return null;\n  }\n\n  const handleShowCommentEditor = () => {\n    setShowCommentEditor(true);\n  };\n\n  const handleCommentCreated = async (_memoCommentName: string) => {\n    // React Query will auto-refetch due to invalidation in the mutation\n    setShowCommentEditor(false);\n  };\n\n  return (\n    <section className=\"@container w-full max-w-5xl min-h-full flex flex-col justify-start items-center sm:pt-3 md:pt-6 pb-8\">\n      {!md && (\n        <MobileHeader>\n          <MemoDetailSidebarDrawer memo={memo} parentPage={locationState?.from} />\n        </MobileHeader>\n      )}\n      <div className={cn(\"w-full flex flex-row justify-start items-start px-4 sm:px-6 gap-4\")}>\n        <div className={cn(\"w-full md:w-[calc(100%-15rem)]\")}>\n          {parentMemo && (\n            <div className=\"w-auto inline-block mb-2\">\n              <Link\n                className=\"px-3 py-1 border border-border rounded-lg max-w-xs w-auto text-sm flex flex-row justify-start items-center flex-nowrap text-muted-foreground hover:shadow hover:opacity-80\"\n                to={`/${parentMemo.name}`}\n                state={locationState}\n                viewTransition\n              >\n                <ArrowUpLeftFromCircleIcon className=\"w-4 h-auto shrink-0 opacity-60 mr-2\" />\n                <span className=\"truncate\">{parentMemo.content}</span>\n              </Link>\n            </div>\n          )}\n          <MemoView\n            key={`${memo.name}-${memo.displayTime}`}\n            className=\"shadow hover:shadow-md transition-all\"\n            memo={memo}\n            compact={false}\n            parentPage={locationState?.from}\n            showCreator\n            showVisibility\n            showPinned\n          />\n          <div className=\"pt-8 pb-16 w-full\">\n            <h2 id=\"comments\" className=\"sr-only\">\n              {t(\"memo.comment.self\")}\n            </h2>\n            <div className=\"relative mx-auto grow w-full min-h-full flex flex-col justify-start items-start gap-y-1\">\n              {comments.length === 0 ? (\n                showCreateCommentButton && (\n                  <div className=\"w-full flex flex-row justify-center items-center py-6\">\n                    <Button variant=\"ghost\" onClick={handleShowCommentEditor}>\n                      <span className=\"text-muted-foreground\">{t(\"memo.comment.write-a-comment\")}</span>\n                      <MessageCircleIcon className=\"ml-2 w-5 h-auto text-muted-foreground\" />\n                    </Button>\n                  </div>\n                )\n              ) : (\n                <div className=\"w-full flex flex-row justify-between items-center h-8 pl-3 mb-2\">\n                  <div className=\"flex flex-row justify-start items-center\">\n                    <MessageCircleIcon className=\"w-5 h-auto text-muted-foreground mr-1\" />\n                    <span className=\"text-muted-foreground text-sm\">{t(\"memo.comment.self\")}</span>\n                    <span className=\"text-muted-foreground text-sm ml-1\">({comments.length})</span>\n                  </div>\n                  {showCreateCommentButton && (\n                    <Button variant=\"ghost\" className=\"text-muted-foreground\" onClick={handleShowCommentEditor}>\n                      {t(\"memo.comment.write-a-comment\")}\n                    </Button>\n                  )}\n                </div>\n              )}\n              {showCommentEditor && (\n                <div className=\"w-full mb-2\">\n                  <MemoEditor\n                    cacheKey={`${memo.name}-${memo.updateTime}-comment`}\n                    placeholder={t(\"editor.add-your-comment-here\")}\n                    parentMemoName={memo.name}\n                    autoFocus\n                    onConfirm={handleCommentCreated}\n                    onCancel={() => setShowCommentEditor(false)}\n                  />\n                </div>\n              )}\n              {comments.map((comment) => (\n                <div className=\"w-full\" key={`${comment.name}-${comment.displayTime}`} id={extractMemoIdFromName(comment.name)}>\n                  <MemoView memo={comment} parentPage={locationState?.from} showCreator compact />\n                </div>\n              ))}\n            </div>\n          </div>\n        </div>\n        {md && (\n          <div className=\"sticky top-0 left-0 shrink-0 -mt-6 w-56 h-full\">\n            <MemoDetailSidebar className=\"py-6\" memo={memo} parentPage={locationState?.from} />\n          </div>\n        )}\n      </div>\n    </section>\n  );\n};\n\nexport default MemoDetail;\n"
  },
  {
    "path": "web/src/pages/NotFound.tsx",
    "content": "import MobileHeader from \"@/components/MobileHeader\";\n\nconst NotFound = () => {\n  return (\n    <section className=\"@container w-full max-w-5xl min-h-svh flex flex-col justify-start items-center sm:pt-3 md:pt-6 pb-8\">\n      <MobileHeader />\n      <div className=\"w-full px-4 grow flex flex-col justify-center items-center sm:px-6\">\n        <p className=\"font-medium\">{\"The page you are looking for can't be found.\"}</p>\n        <p className=\"mt-4 text-[8rem] font-mono text-foreground\">404</p>\n      </div>\n    </section>\n  );\n};\n\nexport default NotFound;\n"
  },
  {
    "path": "web/src/pages/PermissionDenied.tsx",
    "content": "import MobileHeader from \"@/components/MobileHeader\";\n\nconst PermissionDenied = () => {\n  return (\n    <section className=\"@container w-full max-w-5xl min-h-svh flex flex-col justify-start items-center sm:pt-3 md:pt-6 pb-8\">\n      <MobileHeader />\n      <div className=\"w-full px-4 grow flex flex-col justify-center items-center sm:px-6\">\n        <p className=\"font-medium\">Permission denied</p>\n        <p className=\"mt-4 text-[8rem] font-mono text-foreground\">403</p>\n      </div>\n    </section>\n  );\n};\n\nexport default PermissionDenied;\n"
  },
  {
    "path": "web/src/pages/Setting.tsx",
    "content": "import { CogIcon, DatabaseIcon, KeyIcon, LibraryIcon, LucideIcon, Settings2Icon, UserIcon, UsersIcon } from \"lucide-react\";\nimport { useCallback, useEffect, useMemo, useState } from \"react\";\nimport { useLocation } from \"react-router-dom\";\nimport MobileHeader from \"@/components/MobileHeader\";\nimport InstanceSection from \"@/components/Settings/InstanceSection\";\nimport MemberSection from \"@/components/Settings/MemberSection\";\nimport MemoRelatedSettings from \"@/components/Settings/MemoRelatedSettings\";\nimport MyAccountSection from \"@/components/Settings/MyAccountSection\";\nimport PreferencesSection from \"@/components/Settings/PreferencesSection\";\nimport SectionMenuItem from \"@/components/Settings/SectionMenuItem\";\nimport SSOSection from \"@/components/Settings/SSOSection\";\nimport StorageSection from \"@/components/Settings/StorageSection\";\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from \"@/components/ui/select\";\nimport { useInstance } from \"@/contexts/InstanceContext\";\nimport useCurrentUser from \"@/hooks/useCurrentUser\";\nimport useMediaQuery from \"@/hooks/useMediaQuery\";\nimport { InstanceSetting_Key } from \"@/types/proto/api/v1/instance_service_pb\";\nimport { User_Role } from \"@/types/proto/api/v1/user_service_pb\";\nimport { useTranslate } from \"@/utils/i18n\";\n\ntype SettingSection = \"my-account\" | \"preference\" | \"member\" | \"system\" | \"memo\" | \"storage\" | \"sso\";\n\ninterface State {\n  selectedSection: SettingSection;\n}\n\nconst BASIC_SECTIONS: SettingSection[] = [\"my-account\", \"preference\"];\nconst ADMIN_SECTIONS: SettingSection[] = [\"member\", \"system\", \"memo\", \"storage\", \"sso\"];\nconst SECTION_ICON_MAP: Record<SettingSection, LucideIcon> = {\n  \"my-account\": UserIcon,\n  preference: CogIcon,\n  member: UsersIcon,\n  system: Settings2Icon,\n  memo: LibraryIcon,\n  storage: DatabaseIcon,\n  sso: KeyIcon,\n};\n\nconst Setting = () => {\n  const t = useTranslate();\n  const sm = useMediaQuery(\"sm\");\n  const location = useLocation();\n  const user = useCurrentUser();\n  const { profile, fetchSetting } = useInstance();\n  const [state, setState] = useState<State>({\n    selectedSection: \"my-account\",\n  });\n  const isHost = user?.role === User_Role.ADMIN;\n\n  const settingsSectionList = useMemo(() => {\n    let settingList = [...BASIC_SECTIONS];\n    if (isHost) {\n      settingList = settingList.concat(ADMIN_SECTIONS);\n    }\n    return settingList;\n  }, [isHost]);\n\n  useEffect(() => {\n    let hash = location.hash.slice(1) as SettingSection;\n    // If the hash is not a valid section, redirect to the default section.\n    if (![...BASIC_SECTIONS, ...ADMIN_SECTIONS].includes(hash)) {\n      hash = \"my-account\";\n    }\n    setState({\n      selectedSection: hash,\n    });\n  }, [location.hash]);\n\n  useEffect(() => {\n    if (!isHost) {\n      return;\n    }\n\n    // Initial fetch for instance settings.\n    (async () => {\n      [InstanceSetting_Key.MEMO_RELATED, InstanceSetting_Key.STORAGE].forEach(async (key) => {\n        await fetchSetting(key);\n      });\n    })();\n  }, [isHost, fetchSetting]);\n\n  const handleSectionSelectorItemClick = useCallback((settingSection: SettingSection) => {\n    window.location.hash = settingSection;\n  }, []);\n\n  return (\n    <section className=\"@container w-full max-w-5xl min-h-full flex flex-col justify-start items-start sm:pt-3 md:pt-6 pb-8\">\n      {!sm && <MobileHeader />}\n      <div className=\"w-full px-4 sm:px-6\">\n        <div className=\"w-full border border-border flex flex-row justify-start items-start px-4 py-3 rounded-xl bg-background text-muted-foreground\">\n          {sm && (\n            <div className=\"flex flex-col justify-start items-start w-40 h-auto shrink-0 py-2\">\n              <span className=\"text-sm mt-0.5 pl-3 font-mono select-none text-muted-foreground\">{t(\"common.basic\")}</span>\n              <div className=\"w-full flex flex-col justify-start items-start mt-1\">\n                {BASIC_SECTIONS.map((item) => (\n                  <SectionMenuItem\n                    key={item}\n                    text={t(`setting.${item}.label`)}\n                    icon={SECTION_ICON_MAP[item]}\n                    isSelected={state.selectedSection === item}\n                    onClick={() => handleSectionSelectorItemClick(item)}\n                  />\n                ))}\n              </div>\n              {isHost ? (\n                <>\n                  <span className=\"text-sm mt-4 pl-3 font-mono select-none text-muted-foreground\">{t(\"common.admin\")}</span>\n                  <div className=\"w-full flex flex-col justify-start items-start mt-1\">\n                    {ADMIN_SECTIONS.map((item) => (\n                      <SectionMenuItem\n                        key={item}\n                        text={t(`setting.${item}.label`)}\n                        icon={SECTION_ICON_MAP[item]}\n                        isSelected={state.selectedSection === item}\n                        onClick={() => handleSectionSelectorItemClick(item)}\n                      />\n                    ))}\n                    <span className=\"px-3 mt-2 opacity-70 text-sm\">\n                      {t(\"setting.version\")}: v{profile.version}\n                    </span>\n                  </div>\n                </>\n              ) : null}\n            </div>\n          )}\n          <div className=\"w-full grow sm:pl-4 overflow-x-auto\">\n            {!sm && (\n              <div className=\"w-auto inline-block my-2\">\n                <Select value={state.selectedSection} onValueChange={(value) => handleSectionSelectorItemClick(value as SettingSection)}>\n                  <SelectTrigger className=\"w-[180px]\">\n                    <SelectValue placeholder=\"Select section\" />\n                  </SelectTrigger>\n                  <SelectContent>\n                    {settingsSectionList.map((settingSection) => (\n                      <SelectItem key={settingSection} value={settingSection}>\n                        {t(`setting.${settingSection}.label`)}\n                      </SelectItem>\n                    ))}\n                  </SelectContent>\n                </Select>\n              </div>\n            )}\n            {state.selectedSection === \"my-account\" ? (\n              <MyAccountSection />\n            ) : state.selectedSection === \"preference\" ? (\n              <PreferencesSection />\n            ) : state.selectedSection === \"member\" ? (\n              <MemberSection />\n            ) : state.selectedSection === \"system\" ? (\n              <InstanceSection />\n            ) : state.selectedSection === \"memo\" ? (\n              <MemoRelatedSettings />\n            ) : state.selectedSection === \"storage\" ? (\n              <StorageSection />\n            ) : state.selectedSection === \"sso\" ? (\n              <SSOSection />\n            ) : null}\n          </div>\n        </div>\n      </div>\n    </section>\n  );\n};\n\nexport default Setting;\n"
  },
  {
    "path": "web/src/pages/SharedMemo.tsx",
    "content": "import type { Timestamp } from \"@bufbuild/protobuf/wkt\";\nimport { timestampDate } from \"@bufbuild/protobuf/wkt\";\nimport { Code, ConnectError } from \"@connectrpc/connect\";\nimport { AlertCircleIcon } from \"lucide-react\";\nimport { useParams } from \"react-router-dom\";\nimport MemoContent from \"@/components/MemoContent\";\nimport AttachmentList from \"@/components/MemoView/components/metadata/AttachmentList\";\nimport UserAvatar from \"@/components/UserAvatar\";\nimport { useSharedMemo } from \"@/hooks/useMemoShareQueries\";\nimport { useUser } from \"@/hooks/useUserQueries\";\nimport i18n from \"@/i18n\";\nimport type { Attachment } from \"@/types/proto/api/v1/attachment_service_pb\";\nimport { useTranslate } from \"@/utils/i18n\";\n\nfunction withShareAttachmentLinks(attachments: Attachment[], token: string): Attachment[] {\n  return attachments.map((a) => {\n    if (a.externalLink) return a;\n    return { ...a, externalLink: `${window.location.origin}/file/${a.name}/${a.filename}?share_token=${encodeURIComponent(token)}` };\n  });\n}\n\nconst SharedMemo = () => {\n  const t = useTranslate();\n  const { token = \"\" } = useParams<{ token: string }>();\n\n  const { data: memo, error, isLoading } = useSharedMemo(token, { enabled: !!token });\n  const { data: creator } = useUser(memo?.creator ?? \"\", { enabled: !!memo?.creator });\n\n  const isNotFound = error instanceof ConnectError && (error.code === Code.NotFound || error.code === Code.Unauthenticated);\n\n  if (isLoading) {\n    return (\n      <div className=\"flex h-screen items-center justify-center\">\n        <div className=\"h-5 w-5 animate-spin rounded-full border-2 border-primary border-t-transparent\" />\n      </div>\n    );\n  }\n\n  if (isNotFound || (!isLoading && !memo)) {\n    return (\n      <div className=\"flex h-screen flex-col items-center justify-center gap-3 text-center\">\n        <AlertCircleIcon className=\"h-8 w-8 text-muted-foreground\" />\n        <p className=\"text-sm text-muted-foreground\">{t(\"memo.share.invalid-link\")}</p>\n      </div>\n    );\n  }\n\n  if (error || !memo) return null;\n\n  const displayDate = (memo.displayTime as Timestamp | undefined)\n    ? timestampDate(memo.displayTime as Timestamp)?.toLocaleString(i18n.language)\n    : null;\n\n  return (\n    <div className=\"mx-auto w-full min-w-80 max-w-2xl px-4 py-8\">\n      {/* Creator + date above the card */}\n      <div className=\"mb-3 flex flex-row items-center justify-between\">\n        <div className=\"flex flex-row items-center gap-2\">\n          <UserAvatar className=\"shrink-0\" avatarUrl={creator?.avatarUrl} />\n          <span className=\"text-sm text-muted-foreground\">{creator?.displayName || creator?.username || memo.creator}</span>\n        </div>\n        {displayDate && <span className=\"text-xs text-muted-foreground\">{displayDate}</span>}\n      </div>\n\n      <div className=\"relative flex flex-col items-start gap-2 rounded-lg border border-border bg-card px-4 py-3 text-card-foreground\">\n        <MemoContent content={memo.content} />\n        {memo.attachments.length > 0 && <AttachmentList attachments={withShareAttachmentLinks(memo.attachments, token)} />}\n      </div>\n    </div>\n  );\n};\n\nexport default SharedMemo;\n"
  },
  {
    "path": "web/src/pages/SignIn.tsx",
    "content": "import { useEffect, useState } from \"react\";\nimport { toast } from \"react-hot-toast\";\nimport { Link } from \"react-router-dom\";\nimport AuthFooter from \"@/components/AuthFooter\";\nimport PasswordSignInForm from \"@/components/PasswordSignInForm\";\nimport { Button } from \"@/components/ui/button\";\nimport { Separator } from \"@/components/ui/separator\";\nimport { identityProviderServiceClient } from \"@/connect\";\nimport { useInstance } from \"@/contexts/InstanceContext\";\nimport { absolutifyLink } from \"@/helpers/utils\";\nimport useCurrentUser from \"@/hooks/useCurrentUser\";\nimport { handleError } from \"@/lib/error\";\nimport { Routes } from \"@/router\";\nimport { IdentityProvider, IdentityProvider_Type } from \"@/types/proto/api/v1/idp_service_pb\";\nimport { useTranslate } from \"@/utils/i18n\";\nimport { storeOAuthState } from \"@/utils/oauth\";\n\nconst SignIn = () => {\n  const t = useTranslate();\n  const currentUser = useCurrentUser();\n  const [identityProviderList, setIdentityProviderList] = useState<IdentityProvider[]>([]);\n  const { generalSetting: instanceGeneralSetting } = useInstance();\n\n  // Redirect to root page if already signed in.\n  useEffect(() => {\n    if (currentUser?.name) {\n      window.location.href = Routes.ROOT;\n    }\n  }, [currentUser]);\n\n  // Prepare identity provider list.\n  useEffect(() => {\n    const fetchIdentityProviderList = async () => {\n      const { identityProviders } = await identityProviderServiceClient.listIdentityProviders({});\n      setIdentityProviderList(identityProviders);\n    };\n    fetchIdentityProviderList();\n  }, []);\n\n  const handleSignInWithIdentityProvider = async (identityProvider: IdentityProvider) => {\n    if (identityProvider.type === IdentityProvider_Type.OAUTH2) {\n      const redirectUri = absolutifyLink(\"/auth/callback\");\n      const oauth2Config = identityProvider.config?.config?.case === \"oauth2Config\" ? identityProvider.config.config.value : undefined;\n      if (!oauth2Config) {\n        toast.error(\"Identity provider configuration is invalid.\");\n        return;\n      }\n\n      try {\n        // Generate and store secure state parameter with CSRF protection\n        // Also generate PKCE parameters (code_challenge) for enhanced security if available\n        const { state, codeChallenge } = await storeOAuthState(identityProvider.name);\n\n        // Build OAuth authorization URL with secure state\n        // Include PKCE if available (requires HTTPS/localhost for crypto.subtle)\n        // Using S256 (SHA-256) as the code_challenge_method per RFC 7636\n        let authUrl = `${oauth2Config.authUrl}?client_id=${\n          oauth2Config.clientId\n        }&redirect_uri=${encodeURIComponent(redirectUri)}&state=${state}&response_type=code&scope=${encodeURIComponent(\n          oauth2Config.scopes.join(\" \"),\n        )}`;\n\n        // Add PKCE parameters if available\n        if (codeChallenge) {\n          authUrl += `&code_challenge=${codeChallenge}&code_challenge_method=S256`;\n        }\n\n        window.location.href = authUrl;\n      } catch (error) {\n        handleError(error, toast.error, {\n          context: \"Failed to initiate OAuth flow\",\n          fallbackMessage: \"Failed to initiate sign-in. Please try again.\",\n        });\n      }\n    }\n  };\n\n  return (\n    <div className=\"py-4 sm:py-8 w-80 max-w-full min-h-svh mx-auto flex flex-col justify-start items-center\">\n      <div className=\"w-full py-4 grow flex flex-col justify-center items-center\">\n        <div className=\"w-full flex flex-row justify-center items-center mb-6\">\n          <img className=\"h-14 w-auto rounded-full shadow\" src={instanceGeneralSetting.customProfile?.logoUrl || \"/logo.webp\"} alt=\"\" />\n          <p className=\"ml-2 text-5xl text-foreground opacity-80\">{instanceGeneralSetting.customProfile?.title || \"Memos\"}</p>\n        </div>\n        {!instanceGeneralSetting.disallowPasswordAuth ? (\n          <PasswordSignInForm />\n        ) : (\n          identityProviderList.length === 0 && <p className=\"w-full text-2xl mt-2 text-muted-foreground\">Password auth is not allowed.</p>\n        )}\n        {!instanceGeneralSetting.disallowUserRegistration && !instanceGeneralSetting.disallowPasswordAuth && (\n          <p className=\"w-full mt-4 text-sm\">\n            <span className=\"text-muted-foreground\">{t(\"auth.sign-up-tip\")}</span>\n            <Link to=\"/auth/signup\" className=\"cursor-pointer ml-2 text-primary hover:underline\" viewTransition>\n              {t(\"common.sign-up\")}\n            </Link>\n          </p>\n        )}\n        {identityProviderList.length > 0 && (\n          <>\n            {!instanceGeneralSetting.disallowPasswordAuth && (\n              <div className=\"relative my-4 w-full\">\n                <Separator />\n                <div className=\"absolute inset-0 flex items-center justify-center\">\n                  <span className=\"bg-background px-2 text-xs text-muted-foreground\">{t(\"common.or\")}</span>\n                </div>\n              </div>\n            )}\n            <div className=\"w-full flex flex-col space-y-2\">\n              {identityProviderList.map((identityProvider) => (\n                <Button\n                  className=\"bg-background w-full\"\n                  key={identityProvider.name}\n                  variant=\"outline\"\n                  onClick={() => handleSignInWithIdentityProvider(identityProvider)}\n                >\n                  {t(\"common.sign-in-with\", { provider: identityProvider.title })}\n                </Button>\n              ))}\n            </div>\n          </>\n        )}\n      </div>\n      <AuthFooter />\n    </div>\n  );\n};\n\nexport default SignIn;\n"
  },
  {
    "path": "web/src/pages/SignUp.tsx",
    "content": "import { create } from \"@bufbuild/protobuf\";\nimport { timestampDate } from \"@bufbuild/protobuf/wkt\";\nimport { LoaderIcon } from \"lucide-react\";\nimport { useState } from \"react\";\nimport { toast } from \"react-hot-toast\";\nimport { Link } from \"react-router-dom\";\nimport { setAccessToken } from \"@/auth-state\";\nimport AuthFooter from \"@/components/AuthFooter\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { authServiceClient, userServiceClient } from \"@/connect\";\nimport { useAuth } from \"@/contexts/AuthContext\";\nimport { useInstance } from \"@/contexts/InstanceContext\";\nimport useLoading from \"@/hooks/useLoading\";\nimport useNavigateTo from \"@/hooks/useNavigateTo\";\nimport { handleError } from \"@/lib/error\";\nimport { User_Role, UserSchema } from \"@/types/proto/api/v1/user_service_pb\";\nimport { useTranslate } from \"@/utils/i18n\";\n\nconst SignUp = () => {\n  const t = useTranslate();\n  const navigateTo = useNavigateTo();\n  const actionBtnLoadingState = useLoading(false);\n  const [username, setUsername] = useState(\"\");\n  const [password, setPassword] = useState(\"\");\n  const { initialize: initAuth } = useAuth();\n  const { generalSetting: instanceGeneralSetting, profile, initialize: initInstance } = useInstance();\n\n  const handleUsernameInputChanged = (e: React.ChangeEvent<HTMLInputElement>) => {\n    const text = e.target.value as string;\n    setUsername(text);\n  };\n\n  const handlePasswordInputChanged = (e: React.ChangeEvent<HTMLInputElement>) => {\n    const text = e.target.value as string;\n    setPassword(text);\n  };\n\n  const handleFormSubmit = (e: React.FormEvent<HTMLFormElement>) => {\n    e.preventDefault();\n    handleSignUpButtonClick();\n  };\n\n  const handleSignUpButtonClick = async () => {\n    if (username === \"\" || password === \"\") {\n      return;\n    }\n\n    if (actionBtnLoadingState.isLoading) {\n      return;\n    }\n\n    try {\n      actionBtnLoadingState.setLoading();\n      const user = create(UserSchema, {\n        username,\n        password,\n        role: User_Role.USER,\n      });\n      await userServiceClient.createUser({ user });\n      const response = await authServiceClient.signIn({\n        credentials: {\n          case: \"passwordCredentials\",\n          value: { username, password },\n        },\n      });\n      // Store access token from login response\n      if (response.accessToken) {\n        setAccessToken(response.accessToken, response.accessTokenExpiresAt ? timestampDate(response.accessTokenExpiresAt) : undefined);\n      }\n      // Refresh auth context to load the current user\n      await initAuth();\n      // Refetch instance profile to update the initialized status\n      await initInstance();\n      navigateTo(\"/\");\n    } catch (error: unknown) {\n      handleError(error, toast.error, {\n        fallbackMessage: \"Sign up failed\",\n      });\n    }\n    actionBtnLoadingState.setFinish();\n  };\n\n  return (\n    <div className=\"py-4 sm:py-8 w-80 max-w-full min-h-svh mx-auto flex flex-col justify-start items-center\">\n      <div className=\"w-full py-4 grow flex flex-col justify-center items-center\">\n        <div className=\"w-full flex flex-row justify-center items-center mb-6\">\n          <img className=\"h-14 w-auto rounded-full shadow\" src={instanceGeneralSetting.customProfile?.logoUrl || \"/logo.webp\"} alt=\"\" />\n          <p className=\"ml-2 text-5xl text-foreground opacity-80\">{instanceGeneralSetting.customProfile?.title || \"Memos\"}</p>\n        </div>\n        {!instanceGeneralSetting.disallowUserRegistration ? (\n          <>\n            <p className=\"w-full text-2xl mt-2 text-muted-foreground\">{t(\"auth.create-your-account\")}</p>\n            <form className=\"w-full mt-2\" onSubmit={handleFormSubmit}>\n              <div className=\"flex flex-col justify-start items-start w-full gap-4\">\n                <div className=\"w-full flex flex-col justify-start items-start\">\n                  <span className=\"leading-8 text-muted-foreground\">{t(\"common.username\")}</span>\n                  <Input\n                    className=\"w-full bg-background h-10\"\n                    type=\"text\"\n                    readOnly={actionBtnLoadingState.isLoading}\n                    placeholder={t(\"common.username\")}\n                    value={username}\n                    autoComplete=\"username\"\n                    autoCapitalize=\"off\"\n                    spellCheck={false}\n                    onChange={handleUsernameInputChanged}\n                    required\n                  />\n                </div>\n                <div className=\"w-full flex flex-col justify-start items-start\">\n                  <span className=\"leading-8 text-muted-foreground\">{t(\"common.password\")}</span>\n                  <Input\n                    className=\"w-full bg-background h-10\"\n                    type=\"password\"\n                    readOnly={actionBtnLoadingState.isLoading}\n                    placeholder={t(\"common.password\")}\n                    value={password}\n                    autoComplete=\"new-password\"\n                    autoCapitalize=\"off\"\n                    spellCheck={false}\n                    onChange={handlePasswordInputChanged}\n                    required\n                  />\n                </div>\n              </div>\n              <div className=\"flex flex-row justify-end items-center w-full mt-6\">\n                <Button type=\"submit\" className=\"w-full h-10\" disabled={actionBtnLoadingState.isLoading} onClick={handleSignUpButtonClick}>\n                  {t(\"common.sign-up\")}\n                  {actionBtnLoadingState.isLoading && <LoaderIcon className=\"w-5 h-auto ml-2 animate-spin opacity-60\" />}\n                </Button>\n              </div>\n            </form>\n          </>\n        ) : (\n          <p className=\"w-full text-2xl mt-2 text-muted-foreground\">Sign up is not allowed.</p>\n        )}\n        {!profile.admin ? (\n          <p className=\"w-full mt-4 text-sm font-medium text-muted-foreground\">{t(\"auth.host-tip\")}</p>\n        ) : (\n          <p className=\"w-full mt-4 text-sm\">\n            <span className=\"text-muted-foreground\">{t(\"auth.sign-in-tip\")}</span>\n            <Link to=\"/auth\" className=\"cursor-pointer ml-2 text-primary hover:underline\" viewTransition>\n              {t(\"common.sign-in\")}\n            </Link>\n          </p>\n        )}\n      </div>\n      <AuthFooter />\n    </div>\n  );\n};\n\nexport default SignUp;\n"
  },
  {
    "path": "web/src/pages/UserProfile.tsx",
    "content": "import copy from \"copy-to-clipboard\";\nimport { ExternalLinkIcon, LayoutListIcon, type LucideIcon, MapIcon } from \"lucide-react\";\nimport { toast } from \"react-hot-toast\";\nimport { useParams, useSearchParams } from \"react-router-dom\";\nimport MemoView from \"@/components/MemoView\";\nimport PagedMemoList from \"@/components/PagedMemoList\";\nimport UserAvatar from \"@/components/UserAvatar\";\nimport UserMemoMap from \"@/components/UserMemoMap\";\nimport { Button } from \"@/components/ui/button\";\nimport { useMemoFilters, useMemoSorting } from \"@/hooks\";\nimport { useUser } from \"@/hooks/useUserQueries\";\nimport { cn } from \"@/lib/utils\";\nimport { State } from \"@/types/proto/api/v1/common_pb\";\nimport { Memo } from \"@/types/proto/api/v1/memo_service_pb\";\nimport { useTranslate } from \"@/utils/i18n\";\n\ntype TabView = \"memos\" | \"map\";\n\nconst TabButton = ({\n  icon: Icon,\n  label,\n  isActive,\n  onClick,\n}: {\n  icon: LucideIcon;\n  label: string;\n  isActive: boolean;\n  onClick: () => void;\n}) => (\n  <button\n    type=\"button\"\n    onClick={onClick}\n    className={cn(\n      \"flex items-center gap-2 px-3 py-2 text-sm font-medium transition-all duration-200 border-b-2 rounded-t-lg\",\n      isActive\n        ? \"border-primary text-primary bg-primary/5\"\n        : \"border-transparent text-muted-foreground hover:text-foreground hover:bg-muted/50\",\n    )}\n  >\n    <Icon className=\"h-4 w-4\" />\n    {label}\n  </button>\n);\n\ninterface User {\n  name: string;\n  username: string;\n  displayName: string;\n  avatarUrl?: string;\n  description?: string;\n}\n\nconst ProfileHeader = ({ user, onCopyProfileLink, shareLabel }: { user: User; onCopyProfileLink: () => void; shareLabel: string }) => (\n  <div className=\"border-b border-border/10 px-4 py-8 sm:px-6\">\n    <div className=\"mx-auto flex max-w-2xl gap-4 sm:gap-6\">\n      <UserAvatar className=\"h-20 w-20 shrink-0 rounded-2xl shadow-sm sm:h-24 sm:w-24\" avatarUrl={user.avatarUrl} />\n      <div className=\"flex flex-1 flex-col gap-3\">\n        <div>\n          <h1 className=\"text-2xl font-bold text-foreground sm:text-3xl\">{user.displayName || user.username}</h1>\n          {user.displayName && <p className=\"text-sm text-muted-foreground\">@{user.username}</p>}\n        </div>\n        {user.description && <p className=\"text-sm text-foreground/70\">{user.description}</p>}\n        <Button variant=\"outline\" size=\"sm\" onClick={onCopyProfileLink} className=\"w-fit gap-2\">\n          <ExternalLinkIcon className=\"h-4 w-4\" />\n          {shareLabel}\n        </Button>\n      </div>\n    </div>\n  </div>\n);\n\nconst UserProfile = () => {\n  const t = useTranslate();\n  const username = useParams().username;\n  const [searchParams, setSearchParams] = useSearchParams();\n  const activeTab = (searchParams.get(\"view\") === \"map\" ? \"map\" : \"memos\") as TabView;\n\n  const { data: user, isLoading, error } = useUser(`users/${username}`, { enabled: !!username });\n\n  if (error && !isLoading) {\n    toast.error(t(\"message.user-not-found\"));\n  }\n\n  const memoFilter = useMemoFilters({\n    creatorName: user?.name,\n    includeShortcuts: false,\n    includePinned: true,\n  });\n\n  const { listSort, orderBy } = useMemoSorting({\n    pinnedFirst: true,\n    state: State.NORMAL,\n  });\n\n  const handleCopyProfileLink = () => {\n    if (!user) return;\n    copy(`${window.location.origin}/u/${encodeURIComponent(user.username)}`);\n    toast.success(t(\"message.copied\"));\n  };\n\n  const toggleTab = (view: TabView) => {\n    setSearchParams((prev) => {\n      view === \"map\" ? prev.set(\"view\", \"map\") : prev.delete(\"view\");\n      return prev;\n    });\n  };\n\n  if (isLoading) return null;\n\n  return (\n    <section className=\"flex min-h-screen w-full flex-col bg-background\">\n      {user ? (\n        <>\n          <ProfileHeader user={user} onCopyProfileLink={handleCopyProfileLink} shareLabel={t(\"common.share\")} />\n\n          <div className=\"border-b border-border/10 mb-4\">\n            <div className=\"mx-auto flex max-w-2xl\">\n              <TabButton\n                icon={LayoutListIcon}\n                label={t(\"common.memos\")}\n                isActive={activeTab === \"memos\"}\n                onClick={() => toggleTab(\"memos\")}\n              />\n              <TabButton icon={MapIcon} label={t(\"common.map\")} isActive={activeTab === \"map\"} onClick={() => toggleTab(\"map\")} />\n            </div>\n          </div>\n\n          <div className=\"flex-1\">\n            <div className=\"mx-auto w-full max-w-2xl\">\n              {activeTab === \"memos\" ? (\n                <PagedMemoList\n                  renderer={(memo: Memo) => (\n                    <MemoView key={`${memo.name}-${memo.displayTime}`} memo={memo} showVisibility showPinned compact />\n                  )}\n                  listSort={listSort}\n                  orderBy={orderBy}\n                  filter={memoFilter}\n                />\n              ) : (\n                <div className=\"\">\n                  <UserMemoMap creator={user.name} className=\"h-[60dvh] sm:h-[500px] rounded-xl\" />\n                </div>\n              )}\n            </div>\n          </div>\n        </>\n      ) : (\n        <div className=\"flex flex-1 items-center justify-center\">\n          <p className=\"text-muted-foreground\">{t(\"message.user-not-found\")}</p>\n        </div>\n      )}\n    </section>\n  );\n};\n\nexport default UserProfile;\n"
  },
  {
    "path": "web/src/router/index.tsx",
    "content": "import { lazy } from \"react\";\nimport { createBrowserRouter } from \"react-router-dom\";\nimport App from \"@/App\";\nimport { ChunkLoadErrorFallback } from \"@/components/ErrorBoundary\";\nimport MainLayout from \"@/layouts/MainLayout\";\nimport RootLayout from \"@/layouts/RootLayout\";\nimport Home from \"@/pages/Home\";\n\n// Wrap lazy imports to auto-reload on chunk load failure (e.g., after redeployment).\nfunction lazyWithReload<T extends React.ComponentType>(factory: () => Promise<{ default: T }>) {\n  return lazy(() =>\n    factory().catch((error) => {\n      const isChunkError =\n        error?.message?.includes(\"Failed to fetch dynamically imported module\") ||\n        error?.message?.includes(\"Importing a module script failed\");\n      const reloadKey = \"chunk-reload\";\n      if (isChunkError && !sessionStorage.getItem(reloadKey)) {\n        sessionStorage.setItem(reloadKey, \"1\");\n        window.location.reload();\n      }\n      throw error;\n    }),\n  );\n}\n\nconst AdminSignIn = lazyWithReload(() => import(\"@/pages/AdminSignIn\"));\nconst Archived = lazyWithReload(() => import(\"@/pages/Archived\"));\nconst AuthCallback = lazyWithReload(() => import(\"@/pages/AuthCallback\"));\nconst Explore = lazyWithReload(() => import(\"@/pages/Explore\"));\nconst Inboxes = lazyWithReload(() => import(\"@/pages/Inboxes\"));\nconst MemoDetail = lazyWithReload(() => import(\"@/pages/MemoDetail\"));\nconst NotFound = lazyWithReload(() => import(\"@/pages/NotFound\"));\nconst PermissionDenied = lazyWithReload(() => import(\"@/pages/PermissionDenied\"));\nconst Attachments = lazyWithReload(() => import(\"@/pages/Attachments\"));\nconst Setting = lazyWithReload(() => import(\"@/pages/Setting\"));\nconst SharedMemo = lazyWithReload(() => import(\"@/pages/SharedMemo\"));\nconst SignIn = lazyWithReload(() => import(\"@/pages/SignIn\"));\nconst SignUp = lazyWithReload(() => import(\"@/pages/SignUp\"));\nconst UserProfile = lazyWithReload(() => import(\"@/pages/UserProfile\"));\n\nimport { ROUTES } from \"./routes\";\n\n// Backward compatibility alias\nexport const Routes = ROUTES;\nexport { ROUTES };\n\nconst router = createBrowserRouter([\n  {\n    path: \"/\",\n    element: <App />,\n    errorElement: <ChunkLoadErrorFallback />,\n    children: [\n      {\n        path: Routes.AUTH,\n        children: [\n          { path: \"\", element: <SignIn /> },\n          { path: \"admin\", element: <AdminSignIn /> },\n          { path: \"signup\", element: <SignUp /> },\n          { path: \"callback\", element: <AuthCallback /> },\n        ],\n      },\n      {\n        path: Routes.ROOT,\n        element: <RootLayout />,\n        children: [\n          {\n            element: <MainLayout />,\n            children: [\n              { path: \"\", element: <Home /> },\n              { path: Routes.EXPLORE, element: <Explore /> },\n              { path: Routes.ARCHIVED, element: <Archived /> },\n              { path: \"u/:username\", element: <UserProfile /> },\n            ],\n          },\n          { path: Routes.ATTACHMENTS, element: <Attachments /> },\n          { path: Routes.INBOX, element: <Inboxes /> },\n          { path: Routes.SETTING, element: <Setting /> },\n          { path: \"memos/:uid\", element: <MemoDetail /> },\n          { path: \"403\", element: <PermissionDenied /> },\n          { path: \"404\", element: <NotFound /> },\n          { path: \"*\", element: <NotFound /> },\n        ],\n      },\n      // Public share-link viewer — outside RootLayout to bypass auth-gating\n      { path: \"memos/shares/:token\", element: <SharedMemo /> },\n    ],\n  },\n]);\n\nexport default router;\n"
  },
  {
    "path": "web/src/router/routes.ts",
    "content": "export const ROUTES = {\n  ROOT: \"/\",\n  ATTACHMENTS: \"/attachments\",\n  INBOX: \"/inbox\",\n  ARCHIVED: \"/archived\",\n  SETTING: \"/setting\",\n  EXPLORE: \"/explore\",\n  AUTH: \"/auth\",\n  SHARED_MEMO: \"/memos/shares\",\n} as const;\n\nexport type RouteKey = keyof typeof ROUTES;\nexport type RoutePath = (typeof ROUTES)[RouteKey];\n"
  },
  {
    "path": "web/src/themes/COLOR_GUIDE.md",
    "content": "# Color System Guide\n\nThis document explains the color system used in the Memos application, built with OKLCH color space for better perceptual uniformity and accessibility.\n\n## Overview\n\nThe color system supports both light and dark themes automatically through CSS custom properties. All colors are defined using OKLCH (Oklab LCH) color space, which provides better perceptual uniformity than traditional RGB/HSL.\n\n## Color Categories\n\n### 🎨 Primary Brand Colors\n\n| Variable               | Light Theme   | Dark Theme      | Usage                          |\n| ---------------------- | ------------- | --------------- | ------------------------------ |\n| `--primary`            | Golden yellow | Brighter golden | Main brand color, primary CTAs |\n| `--primary-foreground` | White         | White           | Text on primary backgrounds    |\n\n**When to use:**\n\n- Call-to-action buttons\n- Active navigation items\n- Important links and highlights\n- Brand elements\n\n```css\n/* Example usage */\n.cta-button {\n  background: var(--primary);\n  color: var(--primary-foreground);\n}\n```\n\n### 🔘 Secondary Colors\n\n| Variable                 | Light Theme | Dark Theme      | Usage                         |\n| ------------------------ | ----------- | --------------- | ----------------------------- |\n| `--secondary`            | Light gray  | Very light gray | Supporting actions            |\n| `--secondary-foreground` | Dark gray   | Dark gray       | Text on secondary backgrounds |\n\n**When to use:**\n\n- Secondary buttons\n- Less important actions\n- Alternative navigation items\n- Subtle highlights\n\n### 📄 Background & Surface Colors\n\n| Variable               | Light Theme | Dark Theme  | Usage                       |\n| ---------------------- | ----------- | ----------- | --------------------------- |\n| `--background`         | Near white  | Dark gray   | Main page background        |\n| `--card`               | Near white  | Dark gray   | Card/container backgrounds  |\n| `--card-foreground`    | Very dark   | Near white  | Text on card backgrounds    |\n| `--popover`            | Pure white  | Darker gray | Overlay backgrounds         |\n| `--popover-foreground` | Dark gray   | Light gray  | Text on overlay backgrounds |\n\n**When to use:**\n\n- Page backgrounds (`--background`)\n- Content cards and panels (`--card`)\n- Tooltips, dropdowns, modals (`--popover`)\n\n### ✏️ Text & Content Colors\n\n| Variable             | Light Theme | Dark Theme   | Usage                    |\n| -------------------- | ----------- | ------------ | ------------------------ |\n| `--foreground`       | Dark gray   | Light gray   | Primary text color       |\n| `--muted`            | Light gray  | Very dark    | Subtle background areas  |\n| `--muted-foreground` | Medium gray | Medium light | Secondary text, captions |\n\n**When to use:**\n\n- Main body text (`--foreground`)\n- Helper text, placeholders (`--muted-foreground`)\n- Disabled text states\n- Subtle background sections (`--muted`)\n\n### 🎯 Interactive Elements\n\n| Variable              | Light Theme  | Dark Theme  | Usage                        |\n| --------------------- | ------------ | ----------- | ---------------------------- |\n| `--accent`            | Light gray   | Very dark   | Hover states, selected items |\n| `--accent-foreground` | Dark gray    | Light gray  | Text on accent backgrounds   |\n| `--border`            | Medium light | Medium dark | Dividers, input borders      |\n| `--input`             | Medium light | Medium dark | Form input backgrounds       |\n\n**When to use:**\n\n- Hover states (`--accent`)\n- Form field borders (`--border`)\n- Input field backgrounds (`--input`)\n\n### ⚠️ Feedback Colors\n\n| Variable                   | Light Theme | Dark Theme | Usage                           |\n| -------------------------- | ----------- | ---------- | ------------------------------- |\n| `--destructive`            | Very dark   | Red        | Error states, dangerous actions |\n| `--destructive-foreground` | White       | White      | Text on destructive backgrounds |\n\n**When to use:**\n\n- Error messages\n- Delete buttons\n- Warning alerts\n- Validation failures\n\n### 📊 Data Visualization\n\n| Variable    | Purpose                                 |\n| ----------- | --------------------------------------- |\n| `--chart-1` | Primary data series (golden)            |\n| `--chart-2` | Secondary data series (purple)          |\n| `--chart-3` | Tertiary data series (light)            |\n| `--chart-4` | Quaternary data series (purple variant) |\n| `--chart-5` | Quinary data series (golden variant)    |\n\n**When to use:**\n\n- Charts and graphs\n- Data visualization\n- Progress indicators\n- Statistical displays\n\n### 🔧 Sidebar System\n\n| Variable                       | Usage                        |\n| ------------------------------ | ---------------------------- |\n| `--sidebar`                    | Sidebar background           |\n| `--sidebar-foreground`         | Sidebar text                 |\n| `--sidebar-primary`            | Active sidebar items         |\n| `--sidebar-primary-foreground` | Text on active sidebar items |\n| `--sidebar-accent`             | Sidebar hover states         |\n| `--sidebar-accent-foreground`  | Text on sidebar hover states |\n| `--sidebar-border`             | Sidebar dividers             |\n\n## Best Practices\n\n### ✅ Do's\n\n1. **Always pair colors correctly:**\n\n   ```css\n   /* Correct */\n   background: var(--primary);\n   color: var(--primary-foreground);\n   ```\n\n2. **Use semantic meaning:**\n   - Primary = main actions\n   - Secondary = supporting actions\n   - Destructive = dangerous/delete actions\n   - Muted = less important content\n\n3. **Respect the design system:**\n   - Use existing color tokens instead of custom colors\n   - Maintain consistency across components\n\n### ❌ Don'ts\n\n1. **Don't mix incompatible pairs:**\n\n   ```css\n   /* Incorrect - poor contrast */\n   background: var(--primary);\n   color: var(--foreground);\n   ```\n\n2. **Don't use colors outside their intended purpose:**\n   - Don't use destructive colors for positive actions\n   - Don't use primary colors for secondary elements\n\n3. **Don't hardcode color values:**\n\n   ```css\n   /* Bad */\n   color: #333333;\n\n   /* Good */\n   color: var(--foreground);\n   ```\n\n## Theme Switching\n\nThe color system automatically adapts between light and dark themes when the `.dark` class is applied to a parent element (typically `<html>` or `<body>`):\n\n```javascript\n// Toggle dark mode\ndocument.documentElement.classList.toggle(\"dark\");\n```\n\n## Accessibility\n\n- All color pairs meet WCAG contrast requirements\n- Color is never the only means of conveying information\n\n## Implementation Examples\n\n### Button Variants\n\n```css\n/* Primary button */\n.btn-primary {\n  background: var(--primary);\n  color: var(--primary-foreground);\n  border: 1px solid var(--primary);\n}\n\n/* Secondary button */\n.btn-secondary {\n  background: var(--secondary);\n  color: var(--secondary-foreground);\n  border: 1px solid var(--border);\n}\n\n/* Destructive button */\n.btn-destructive {\n  background: var(--destructive);\n  color: var(--destructive-foreground);\n  border: 1px solid var(--destructive);\n}\n```\n\n### Form Elements\n\n```css\n/* Input field */\n.input {\n  background: var(--input);\n  color: var(--foreground);\n  border: 1px solid var(--border);\n}\n```\n\n### Cards and Containers\n\n```css\n/* Content card */\n.card {\n  background: var(--card);\n  color: var(--card-foreground);\n  border: 1px solid var(--border);\n}\n\n/* Popover/Modal */\n.popover {\n  background: var(--popover);\n  color: var(--popover-foreground);\n  box-shadow: var(--shadow-lg);\n}\n```\n\n## Color Testing\n\nTo ensure proper contrast and accessibility:\n\n1. Test both light and dark themes\n2. Verify readability at different zoom levels\n3. Check with colorblind simulation tools\n4. Validate WCAG contrast ratios\n\n## Z-Index Hierarchy\n\nThe application uses a structured z-index hierarchy to ensure proper layering of UI components:\n\n| Component Type    | Z-Index  | Usage                                 |\n| ----------------- | -------- | ------------------------------------- |\n| **Base Content**  | `z-0`    | Normal page content                   |\n| **Overlays**      | `z-50`   | Dialog/Sheet backgrounds              |\n| **Modal Content** | `z-50`   | Dialog/Sheet content                  |\n| **Dropdowns**     | `z-[60]` | Select, DropdownMenu, Popover content |\n| **Tooltips**      | `z-[70]` | Tooltip content (highest priority)    |\n\n### Rules\n\n1. **Dialog/Sheet**: Use `z-50` for both overlay and content\n2. **Interactive Elements**: Use `z-[60]` for dropdowns inside dialogs\n3. **Tooltips**: Use `z-[70]` to appear above all other elements\n4. **Always test**: Ensure Select/DropdownMenu works inside Dialog/Sheet\n\n### Example\n\n```tsx\n// ✅ Correct: Select inside Dialog will appear above dialog content\n<Dialog>\n  <DialogContent>\n    <Select>\n      <SelectContent className=\"z-[60]\">\n        {\" \"}\n        {/* Higher than dialog */}\n        <SelectItem>Option 1</SelectItem>\n      </SelectContent>\n    </Select>\n  </DialogContent>\n</Dialog>\n```\n\n---\n\n_This color system is designed to provide a consistent, accessible, and beautiful user experience across all themes and components._\n"
  },
  {
    "path": "web/src/themes/default-dark.css",
    "content": ":root {\n  /* Surfaces — layered from darkest to lightest, consistent cool-slate tint */\n  --background: oklch(0.09 0.006 265);\n  --foreground: oklch(0.82 0.005 265);\n\n  --card: oklch(0.13 0.006 265);\n  --card-foreground: oklch(0.82 0.005 265);\n\n  /* Popovers float above cards — slightly lighter to appear elevated */\n  --popover: oklch(0.17 0.006 265);\n  --popover-foreground: oklch(0.82 0.005 265);\n\n  /* Primary — bright blue, clearly interactive in dark context */\n  --primary: oklch(0.65 0.15 250);\n  --primary-foreground: oklch(0.98 0.003 265);\n\n  /* Secondary — subtle elevated surface for secondary buttons */\n  --secondary: oklch(0.19 0.007 265);\n  --secondary-foreground: oklch(0.82 0.005 265);\n\n  /* Muted — inline backgrounds (code, skeletons, error panels) */\n  --muted: oklch(0.21 0.008 265);\n  --muted-foreground: oklch(0.56 0.005 265);\n\n  /* Accent — hover states, slightly more chromatic than muted to feel interactive */\n  --accent: oklch(0.22 0.012 265);\n  --accent-foreground: oklch(0.88 0.005 265);\n\n  /* Destructive — vivid red, clearly visible on dark surfaces */\n  --destructive: oklch(0.62 0.2 22);\n  --destructive-foreground: oklch(0.98 0.003 265);\n\n  /* Borders — --border for layout dividers, --input for form field borders */\n  --border: oklch(0.21 0.007 265);\n  --input: oklch(0.25 0.007 265);\n  --ring: oklch(0.65 0.15 250);\n\n  /* Sidebar — darkest surface, distinct from background */\n  --sidebar: oklch(0.07 0.005 265);\n  --sidebar-foreground: oklch(0.68 0.005 265);\n  --sidebar-accent: oklch(0.19 0.01 265);\n  --sidebar-accent-foreground: oklch(0.84 0.005 265);\n}\n"
  },
  {
    "path": "web/src/themes/default.css",
    "content": ":root {\n  --background: oklch(0.9818 0.0054 95.0986);\n  --foreground: oklch(0.2438 0.0269 95.7226);\n  --card: oklch(1 0 0);\n  --card-foreground: oklch(0.1908 0.002 106.5859);\n  --popover: oklch(1 0 0);\n  --popover-foreground: oklch(0.2671 0.0196 98.939);\n  --primary: oklch(0.45 0.08 250);\n  --primary-foreground: oklch(0.9818 0.0054 95.0986);\n  --secondary: oklch(0.9245 0.0138 92.9892);\n  --secondary-foreground: oklch(0.4334 0.0177 98.6048);\n  --muted: oklch(0.9341 0.0153 90.239);\n  --muted-foreground: oklch(0.5559 0.0075 97.4233);\n  --accent: oklch(0.9245 0.0138 92.9892);\n  --accent-foreground: oklch(0.2671 0.0196 98.939);\n  --destructive: oklch(0.5 0.12 25);\n  --destructive-foreground: oklch(1 0 0);\n  --border: oklch(0.8847 0.0069 97.3627);\n  --input: oklch(0.7621 0.0156 98.3528);\n  --ring: oklch(0.45 0.08 250);\n  --sidebar: oklch(0.9663 0.008 98.8792);\n  --sidebar-foreground: oklch(0.359 0.0051 106.6524);\n  --sidebar-accent: oklch(0.9245 0.0138 92.9892);\n  --sidebar-accent-foreground: oklch(0.325 0 0);\n  --font-sans:\n    ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, \"Noto Sans\", sans-serif,\n    \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\";\n  --font-serif: ui-serif, Georgia, Cambria, \"Times New Roman\", Times, serif;\n  --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace;\n  --radius: 0.5rem;\n  --shadow-xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);\n  --shadow-sm: 0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 1px 2px -1px hsl(0 0% 0% / 0.1);\n  --shadow: 0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 1px 2px -1px hsl(0 0% 0% / 0.1);\n  --shadow-md: 0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 2px 4px -1px hsl(0 0% 0% / 0.1);\n  --shadow-lg: 0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 4px 6px -1px hsl(0 0% 0% / 0.1);\n  --shadow-xl: 0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 8px 10px -1px hsl(0 0% 0% / 0.1);\n  --shadow-2xl: 0 1px 3px 0px hsl(0 0% 0% / 0.25);\n}\n\n/* Tailwind v4 token mappings — compiled once at build time, shared by all themes.\n   Dynamic theme files only need to override :root variables; this block covers all of them. */\n@theme inline {\n  --color-background: var(--background);\n  --color-foreground: var(--foreground);\n  --color-card: var(--card);\n  --color-card-foreground: var(--card-foreground);\n  --color-popover: var(--popover);\n  --color-popover-foreground: var(--popover-foreground);\n  --color-primary: var(--primary);\n  --color-primary-foreground: var(--primary-foreground);\n  --color-secondary: var(--secondary);\n  --color-secondary-foreground: var(--secondary-foreground);\n  --color-muted: var(--muted);\n  --color-muted-foreground: var(--muted-foreground);\n  --color-accent: var(--accent);\n  --color-accent-foreground: var(--accent-foreground);\n  --color-destructive: var(--destructive);\n  --color-destructive-foreground: var(--destructive-foreground);\n  --color-border: var(--border);\n  --color-input: var(--input);\n  --color-ring: var(--ring);\n  --color-sidebar: var(--sidebar);\n  --color-sidebar-foreground: var(--sidebar-foreground);\n  --color-sidebar-accent: var(--sidebar-accent);\n  --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);\n\n  --font-sans: var(--font-sans);\n  --font-mono: var(--font-mono);\n  --font-serif: var(--font-serif);\n\n  --radius-sm: calc(var(--radius) - 4px);\n  --radius-md: calc(var(--radius) - 2px);\n  --radius-lg: var(--radius);\n  --radius-xl: calc(var(--radius) + 4px);\n\n  --shadow-xs: var(--shadow-xs);\n  --shadow-sm: var(--shadow-sm);\n  --shadow: var(--shadow);\n  --shadow-md: var(--shadow-md);\n  --shadow-lg: var(--shadow-lg);\n  --shadow-xl: var(--shadow-xl);\n  --shadow-2xl: var(--shadow-2xl);\n}\n"
  },
  {
    "path": "web/src/themes/paper.css",
    "content": ":root {\n  --background: oklch(0.95 0.015 75);\n  --foreground: oklch(0.25 0.02 65);\n  --card: oklch(0.98 0.008 80);\n  --card-foreground: oklch(0.22 0.015 68);\n  --popover: oklch(0.98 0.008 80);\n  --popover-foreground: oklch(0.25 0.02 65);\n  --primary: oklch(0.45 0.08 45);\n  --primary-foreground: oklch(0.98 0.008 80);\n  --secondary: oklch(0.92 0.025 70);\n  --secondary-foreground: oklch(0.35 0.03 60);\n  --muted: oklch(0.9 0.025 75);\n  --muted-foreground: oklch(0.5 0.02 68);\n  --accent: oklch(0.88 0.035 55);\n  --accent-foreground: oklch(0.25 0.02 65);\n  --destructive: oklch(0.48 0.15 25);\n  --destructive-foreground: oklch(0.98 0.008 80);\n  --border: oklch(0.85 0.025 72);\n  --input: oklch(0.8 0.03 75);\n  --ring: oklch(0.45 0.08 45);\n  --sidebar: oklch(0.9647 0.0118 83.7892);\n  --sidebar-foreground: oklch(0.2941 0.0196 67.6524);\n  --sidebar-accent: oklch(0.9412 0.0196 78.9456);\n  --sidebar-accent-foreground: oklch(0.2647 0.0157 71.2341);\n  --shadow-xs: 0 1px 3px 0px hsl(34 12% 15% / 0.04);\n  --shadow-sm: 0 1px 3px 0px hsl(34 12% 15% / 0.08), 0 1px 2px -1px hsl(34 12% 15% / 0.08);\n  --shadow: 0 1px 3px 0px hsl(34 12% 15% / 0.08), 0 1px 2px -1px hsl(34 12% 15% / 0.08);\n  --shadow-md: 0 1px 3px 0px hsl(34 12% 15% / 0.08), 0 2px 4px -1px hsl(34 12% 15% / 0.08);\n  --shadow-lg: 0 1px 3px 0px hsl(34 12% 15% / 0.08), 0 4px 6px -1px hsl(34 12% 15% / 0.08);\n  --shadow-xl: 0 1px 3px 0px hsl(34 12% 15% / 0.08), 0 8px 10px -1px hsl(34 12% 15% / 0.08);\n  --shadow-2xl: 0 1px 3px 0px hsl(34 12% 15% / 0.18);\n}\n"
  },
  {
    "path": "web/src/types/common.d.ts",
    "content": "type FunctionType = (...args: unknown[]) => unknown;\n"
  },
  {
    "path": "web/src/types/common.ts",
    "content": "export type TableData = Record<string, unknown>;\n\nexport interface ApiError {\n  message: string;\n  code?: string;\n  details?: unknown;\n}\n\nexport function isApiError(error: unknown): error is ApiError {\n  return typeof error === \"object\" && error !== null && \"message\" in error && typeof (error as ApiError).message === \"string\";\n}\n\nexport type ToastFunction = (message: string) => void | Promise<void>;\n"
  },
  {
    "path": "web/src/types/i18n.d.ts",
    "content": "type Locale = string;\n"
  },
  {
    "path": "web/src/types/markdown.ts",
    "content": "import type { Data, Element as HastElement } from \"hast\";\n\nexport interface TagNode {\n  type: \"tagNode\";\n  value: string;\n  data: TagNodeData;\n}\n\nexport interface TagNodeData {\n  hName: \"span\";\n  hProperties: TagNodeProperties;\n  hChildren: Array<{ type: \"text\"; value: string }>;\n}\n\nexport interface TagNodeProperties {\n  className: string;\n  \"data-tag\": string;\n}\n\nexport interface ExtendedData extends Data {\n  mdastType?: string;\n}\n\nexport function hasExtendedData(node: unknown): node is { data: ExtendedData } {\n  return typeof node === \"object\" && node !== null && \"data\" in node && typeof (node as { data: unknown }).data === \"object\";\n}\n\nexport function isTagElement(node: HastElement): boolean {\n  if (hasExtendedData(node) && node.data.mdastType === \"tagNode\") {\n    return true;\n  }\n\n  const className = node.properties?.className;\n  if (Array.isArray(className) && className.includes(\"tag\")) {\n    return true;\n  }\n\n  return false;\n}\n\nexport function isTaskListItemElement(node: HastElement): boolean {\n  const type = node.properties?.type;\n  return typeof type === \"string\" && type === \"checkbox\";\n}\n"
  },
  {
    "path": "web/src/types/modules/setting.d.ts",
    "content": "type Theme = \"system\" | \"default\" | \"default-dark\" | \"paper\";\n"
  },
  {
    "path": "web/src/types/proto/api/v1/attachment_service_pb.ts",
    "content": "// @generated by protoc-gen-es v2.11.0 with parameter \"target=ts\"\n// @generated from file api/v1/attachment_service.proto (package memos.api.v1, syntax proto3)\n/* eslint-disable */\n\nimport type { GenFile, GenMessage, GenService } from \"@bufbuild/protobuf/codegenv2\";\nimport { fileDesc, messageDesc, serviceDesc } from \"@bufbuild/protobuf/codegenv2\";\nimport { file_google_api_annotations } from \"../../google/api/annotations_pb\";\nimport { file_google_api_client } from \"../../google/api/client_pb\";\nimport { file_google_api_field_behavior } from \"../../google/api/field_behavior_pb\";\nimport { file_google_api_resource } from \"../../google/api/resource_pb\";\nimport type { EmptySchema, FieldMask, Timestamp } from \"@bufbuild/protobuf/wkt\";\nimport { file_google_protobuf_empty, file_google_protobuf_field_mask, file_google_protobuf_timestamp } from \"@bufbuild/protobuf/wkt\";\nimport type { Message } from \"@bufbuild/protobuf\";\n\n/**\n * Describes the file api/v1/attachment_service.proto.\n */\nexport const file_api_v1_attachment_service: GenFile = /*@__PURE__*/\n  fileDesc(\"Ch9hcGkvdjEvYXR0YWNobWVudF9zZXJ2aWNlLnByb3RvEgxtZW1vcy5hcGkudjEitgIKCkF0dGFjaG1lbnQSEQoEbmFtZRgBIAEoCUID4EEIEjQKC2NyZWF0ZV90aW1lGAIgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcEID4EEDEhUKCGZpbGVuYW1lGAMgASgJQgPgQQISFAoHY29udGVudBgEIAEoDEID4EEEEhoKDWV4dGVybmFsX2xpbmsYBSABKAlCA+BBARIRCgR0eXBlGAYgASgJQgPgQQISEQoEc2l6ZRgHIAEoA0ID4EEDEhYKBG1lbW8YCCABKAlCA+BBAUgAiAEBOk/qQUwKF21lbW9zLmFwaS52MS9BdHRhY2htZW50EhhhdHRhY2htZW50cy97YXR0YWNobWVudH0qC2F0dGFjaG1lbnRzMgphdHRhY2htZW50QgcKBV9tZW1vImgKF0NyZWF0ZUF0dGFjaG1lbnRSZXF1ZXN0EjEKCmF0dGFjaG1lbnQYASABKAsyGC5tZW1vcy5hcGkudjEuQXR0YWNobWVudEID4EECEhoKDWF0dGFjaG1lbnRfaWQYAiABKAlCA+BBASJ1ChZMaXN0QXR0YWNobWVudHNSZXF1ZXN0EhYKCXBhZ2Vfc2l6ZRgBIAEoBUID4EEBEhcKCnBhZ2VfdG9rZW4YAiABKAlCA+BBARITCgZmaWx0ZXIYAyABKAlCA+BBARIVCghvcmRlcl9ieRgEIAEoCUID4EEBInUKF0xpc3RBdHRhY2htZW50c1Jlc3BvbnNlEi0KC2F0dGFjaG1lbnRzGAEgAygLMhgubWVtb3MuYXBpLnYxLkF0dGFjaG1lbnQSFwoPbmV4dF9wYWdlX3Rva2VuGAIgASgJEhIKCnRvdGFsX3NpemUYAyABKAUiRQoUR2V0QXR0YWNobWVudFJlcXVlc3QSLQoEbmFtZRgBIAEoCUIf4EEC+kEZChdtZW1vcy5hcGkudjEvQXR0YWNobWVudCKCAQoXVXBkYXRlQXR0YWNobWVudFJlcXVlc3QSMQoKYXR0YWNobWVudBgBIAEoCzIYLm1lbW9zLmFwaS52MS5BdHRhY2htZW50QgPgQQISNAoLdXBkYXRlX21hc2sYAiABKAsyGi5nb29nbGUucHJvdG9idWYuRmllbGRNYXNrQgPgQQIiSAoXRGVsZXRlQXR0YWNobWVudFJlcXVlc3QSLQoEbmFtZRgBIAEoCUIf4EEC+kEZChdtZW1vcy5hcGkudjEvQXR0YWNobWVudDLEBQoRQXR0YWNobWVudFNlcnZpY2USiQEKEENyZWF0ZUF0dGFjaG1lbnQSJS5tZW1vcy5hcGkudjEuQ3JlYXRlQXR0YWNobWVudFJlcXVlc3QaGC5tZW1vcy5hcGkudjEuQXR0YWNobWVudCI02kEKYXR0YWNobWVudILT5JMCIToKYXR0YWNobWVudCITL2FwaS92MS9hdHRhY2htZW50cxJ7Cg9MaXN0QXR0YWNobWVudHMSJC5tZW1vcy5hcGkudjEuTGlzdEF0dGFjaG1lbnRzUmVxdWVzdBolLm1lbW9zLmFwaS52MS5MaXN0QXR0YWNobWVudHNSZXNwb25zZSIbgtPkkwIVEhMvYXBpL3YxL2F0dGFjaG1lbnRzEnoKDUdldEF0dGFjaG1lbnQSIi5tZW1vcy5hcGkudjEuR2V0QXR0YWNobWVudFJlcXVlc3QaGC5tZW1vcy5hcGkudjEuQXR0YWNobWVudCIr2kEEbmFtZYLT5JMCHhIcL2FwaS92MS97bmFtZT1hdHRhY2htZW50cy8qfRKpAQoQVXBkYXRlQXR0YWNobWVudBIlLm1lbW9zLmFwaS52MS5VcGRhdGVBdHRhY2htZW50UmVxdWVzdBoYLm1lbW9zLmFwaS52MS5BdHRhY2htZW50IlTaQRZhdHRhY2htZW50LHVwZGF0ZV9tYXNrgtPkkwI1OgphdHRhY2htZW50MicvYXBpL3YxL3thdHRhY2htZW50Lm5hbWU9YXR0YWNobWVudHMvKn0SfgoQRGVsZXRlQXR0YWNobWVudBIlLm1lbW9zLmFwaS52MS5EZWxldGVBdHRhY2htZW50UmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSIr2kEEbmFtZYLT5JMCHiocL2FwaS92MS97bmFtZT1hdHRhY2htZW50cy8qfUKuAQoQY29tLm1lbW9zLmFwaS52MUIWQXR0YWNobWVudFNlcnZpY2VQcm90b1ABWjBnaXRodWIuY29tL3VzZW1lbW9zL21lbW9zL3Byb3RvL2dlbi9hcGkvdjE7YXBpdjGiAgNNQViqAgxNZW1vcy5BcGkuVjHKAgxNZW1vc1xBcGlcVjHiAhhNZW1vc1xBcGlcVjFcR1BCTWV0YWRhdGHqAg5NZW1vczo6QXBpOjpWMWIGcHJvdG8z\", [file_google_api_annotations, file_google_api_client, file_google_api_field_behavior, file_google_api_resource, file_google_protobuf_empty, file_google_protobuf_field_mask, file_google_protobuf_timestamp]);\n\n/**\n * @generated from message memos.api.v1.Attachment\n */\nexport type Attachment = Message<\"memos.api.v1.Attachment\"> & {\n  /**\n   * The name of the attachment.\n   * Format: attachments/{attachment}\n   *\n   * @generated from field: string name = 1;\n   */\n  name: string;\n\n  /**\n   * Output only. The creation timestamp.\n   *\n   * @generated from field: google.protobuf.Timestamp create_time = 2;\n   */\n  createTime?: Timestamp;\n\n  /**\n   * The filename of the attachment.\n   *\n   * @generated from field: string filename = 3;\n   */\n  filename: string;\n\n  /**\n   * Input only. The content of the attachment.\n   *\n   * @generated from field: bytes content = 4;\n   */\n  content: Uint8Array;\n\n  /**\n   * Optional. The external link of the attachment.\n   *\n   * @generated from field: string external_link = 5;\n   */\n  externalLink: string;\n\n  /**\n   * The MIME type of the attachment.\n   *\n   * @generated from field: string type = 6;\n   */\n  type: string;\n\n  /**\n   * Output only. The size of the attachment in bytes.\n   *\n   * @generated from field: int64 size = 7;\n   */\n  size: bigint;\n\n  /**\n   * Optional. The related memo. Refer to `Memo.name`.\n   * Format: memos/{memo}\n   *\n   * @generated from field: optional string memo = 8;\n   */\n  memo?: string;\n};\n\n/**\n * Describes the message memos.api.v1.Attachment.\n * Use `create(AttachmentSchema)` to create a new message.\n */\nexport const AttachmentSchema: GenMessage<Attachment> = /*@__PURE__*/\n  messageDesc(file_api_v1_attachment_service, 0);\n\n/**\n * @generated from message memos.api.v1.CreateAttachmentRequest\n */\nexport type CreateAttachmentRequest = Message<\"memos.api.v1.CreateAttachmentRequest\"> & {\n  /**\n   * Required. The attachment to create.\n   *\n   * @generated from field: memos.api.v1.Attachment attachment = 1;\n   */\n  attachment?: Attachment;\n\n  /**\n   * Optional. The attachment ID to use for this attachment.\n   * If empty, a unique ID will be generated.\n   *\n   * @generated from field: string attachment_id = 2;\n   */\n  attachmentId: string;\n};\n\n/**\n * Describes the message memos.api.v1.CreateAttachmentRequest.\n * Use `create(CreateAttachmentRequestSchema)` to create a new message.\n */\nexport const CreateAttachmentRequestSchema: GenMessage<CreateAttachmentRequest> = /*@__PURE__*/\n  messageDesc(file_api_v1_attachment_service, 1);\n\n/**\n * @generated from message memos.api.v1.ListAttachmentsRequest\n */\nexport type ListAttachmentsRequest = Message<\"memos.api.v1.ListAttachmentsRequest\"> & {\n  /**\n   * Optional. The maximum number of attachments to return.\n   * The service may return fewer than this value.\n   * If unspecified, at most 50 attachments will be returned.\n   * The maximum value is 1000; values above 1000 will be coerced to 1000.\n   *\n   * @generated from field: int32 page_size = 1;\n   */\n  pageSize: number;\n\n  /**\n   * Optional. A page token, received from a previous `ListAttachments` call.\n   * Provide this to retrieve the subsequent page.\n   *\n   * @generated from field: string page_token = 2;\n   */\n  pageToken: string;\n\n  /**\n   * Optional. Filter to apply to the list results.\n   * Example: \"mime_type==\\\"image/png\\\"\" or \"filename.contains(\\\"test\\\")\"\n   * Supported operators: =, !=, <, <=, >, >=, : (contains), in\n   * Supported fields: filename, mime_type, create_time, memo\n   *\n   * @generated from field: string filter = 3;\n   */\n  filter: string;\n\n  /**\n   * Optional. The order to sort results by.\n   * Example: \"create_time desc\" or \"filename asc\"\n   *\n   * @generated from field: string order_by = 4;\n   */\n  orderBy: string;\n};\n\n/**\n * Describes the message memos.api.v1.ListAttachmentsRequest.\n * Use `create(ListAttachmentsRequestSchema)` to create a new message.\n */\nexport const ListAttachmentsRequestSchema: GenMessage<ListAttachmentsRequest> = /*@__PURE__*/\n  messageDesc(file_api_v1_attachment_service, 2);\n\n/**\n * @generated from message memos.api.v1.ListAttachmentsResponse\n */\nexport type ListAttachmentsResponse = Message<\"memos.api.v1.ListAttachmentsResponse\"> & {\n  /**\n   * The list of attachments.\n   *\n   * @generated from field: repeated memos.api.v1.Attachment attachments = 1;\n   */\n  attachments: Attachment[];\n\n  /**\n   * A token that can be sent as `page_token` to retrieve the next page.\n   * If this field is omitted, there are no subsequent pages.\n   *\n   * @generated from field: string next_page_token = 2;\n   */\n  nextPageToken: string;\n\n  /**\n   * The total count of attachments (may be approximate).\n   *\n   * @generated from field: int32 total_size = 3;\n   */\n  totalSize: number;\n};\n\n/**\n * Describes the message memos.api.v1.ListAttachmentsResponse.\n * Use `create(ListAttachmentsResponseSchema)` to create a new message.\n */\nexport const ListAttachmentsResponseSchema: GenMessage<ListAttachmentsResponse> = /*@__PURE__*/\n  messageDesc(file_api_v1_attachment_service, 3);\n\n/**\n * @generated from message memos.api.v1.GetAttachmentRequest\n */\nexport type GetAttachmentRequest = Message<\"memos.api.v1.GetAttachmentRequest\"> & {\n  /**\n   * Required. The attachment name of the attachment to retrieve.\n   * Format: attachments/{attachment}\n   *\n   * @generated from field: string name = 1;\n   */\n  name: string;\n};\n\n/**\n * Describes the message memos.api.v1.GetAttachmentRequest.\n * Use `create(GetAttachmentRequestSchema)` to create a new message.\n */\nexport const GetAttachmentRequestSchema: GenMessage<GetAttachmentRequest> = /*@__PURE__*/\n  messageDesc(file_api_v1_attachment_service, 4);\n\n/**\n * @generated from message memos.api.v1.UpdateAttachmentRequest\n */\nexport type UpdateAttachmentRequest = Message<\"memos.api.v1.UpdateAttachmentRequest\"> & {\n  /**\n   * Required. The attachment which replaces the attachment on the server.\n   *\n   * @generated from field: memos.api.v1.Attachment attachment = 1;\n   */\n  attachment?: Attachment;\n\n  /**\n   * Required. The list of fields to update.\n   *\n   * @generated from field: google.protobuf.FieldMask update_mask = 2;\n   */\n  updateMask?: FieldMask;\n};\n\n/**\n * Describes the message memos.api.v1.UpdateAttachmentRequest.\n * Use `create(UpdateAttachmentRequestSchema)` to create a new message.\n */\nexport const UpdateAttachmentRequestSchema: GenMessage<UpdateAttachmentRequest> = /*@__PURE__*/\n  messageDesc(file_api_v1_attachment_service, 5);\n\n/**\n * @generated from message memos.api.v1.DeleteAttachmentRequest\n */\nexport type DeleteAttachmentRequest = Message<\"memos.api.v1.DeleteAttachmentRequest\"> & {\n  /**\n   * Required. The attachment name of the attachment to delete.\n   * Format: attachments/{attachment}\n   *\n   * @generated from field: string name = 1;\n   */\n  name: string;\n};\n\n/**\n * Describes the message memos.api.v1.DeleteAttachmentRequest.\n * Use `create(DeleteAttachmentRequestSchema)` to create a new message.\n */\nexport const DeleteAttachmentRequestSchema: GenMessage<DeleteAttachmentRequest> = /*@__PURE__*/\n  messageDesc(file_api_v1_attachment_service, 6);\n\n/**\n * @generated from service memos.api.v1.AttachmentService\n */\nexport const AttachmentService: GenService<{\n  /**\n   * CreateAttachment creates a new attachment.\n   *\n   * @generated from rpc memos.api.v1.AttachmentService.CreateAttachment\n   */\n  createAttachment: {\n    methodKind: \"unary\";\n    input: typeof CreateAttachmentRequestSchema;\n    output: typeof AttachmentSchema;\n  },\n  /**\n   * ListAttachments lists all attachments.\n   *\n   * @generated from rpc memos.api.v1.AttachmentService.ListAttachments\n   */\n  listAttachments: {\n    methodKind: \"unary\";\n    input: typeof ListAttachmentsRequestSchema;\n    output: typeof ListAttachmentsResponseSchema;\n  },\n  /**\n   * GetAttachment returns an attachment by name.\n   *\n   * @generated from rpc memos.api.v1.AttachmentService.GetAttachment\n   */\n  getAttachment: {\n    methodKind: \"unary\";\n    input: typeof GetAttachmentRequestSchema;\n    output: typeof AttachmentSchema;\n  },\n  /**\n   * UpdateAttachment updates an attachment.\n   *\n   * @generated from rpc memos.api.v1.AttachmentService.UpdateAttachment\n   */\n  updateAttachment: {\n    methodKind: \"unary\";\n    input: typeof UpdateAttachmentRequestSchema;\n    output: typeof AttachmentSchema;\n  },\n  /**\n   * DeleteAttachment deletes an attachment by name.\n   *\n   * @generated from rpc memos.api.v1.AttachmentService.DeleteAttachment\n   */\n  deleteAttachment: {\n    methodKind: \"unary\";\n    input: typeof DeleteAttachmentRequestSchema;\n    output: typeof EmptySchema;\n  },\n}> = /*@__PURE__*/\n  serviceDesc(file_api_v1_attachment_service, 0);\n\n"
  },
  {
    "path": "web/src/types/proto/api/v1/auth_service_pb.ts",
    "content": "// @generated by protoc-gen-es v2.11.0 with parameter \"target=ts\"\n// @generated from file api/v1/auth_service.proto (package memos.api.v1, syntax proto3)\n/* eslint-disable */\n\nimport type { GenFile, GenMessage, GenService } from \"@bufbuild/protobuf/codegenv2\";\nimport { fileDesc, messageDesc, serviceDesc } from \"@bufbuild/protobuf/codegenv2\";\nimport type { User } from \"./user_service_pb\";\nimport { file_api_v1_user_service } from \"./user_service_pb\";\nimport { file_google_api_annotations } from \"../../google/api/annotations_pb\";\nimport { file_google_api_field_behavior } from \"../../google/api/field_behavior_pb\";\nimport type { EmptySchema, Timestamp } from \"@bufbuild/protobuf/wkt\";\nimport { file_google_protobuf_empty, file_google_protobuf_timestamp } from \"@bufbuild/protobuf/wkt\";\nimport type { Message } from \"@bufbuild/protobuf\";\n\n/**\n * Describes the file api/v1/auth_service.proto.\n */\nexport const file_api_v1_auth_service: GenFile = /*@__PURE__*/\n  fileDesc(\"ChlhcGkvdjEvYXV0aF9zZXJ2aWNlLnByb3RvEgxtZW1vcy5hcGkudjEiFwoVR2V0Q3VycmVudFVzZXJSZXF1ZXN0IjoKFkdldEN1cnJlbnRVc2VyUmVzcG9uc2USIAoEdXNlchgBIAEoCzISLm1lbW9zLmFwaS52MS5Vc2VyIu4CCg1TaWduSW5SZXF1ZXN0Ek8KFHBhc3N3b3JkX2NyZWRlbnRpYWxzGAEgASgLMi8ubWVtb3MuYXBpLnYxLlNpZ25JblJlcXVlc3QuUGFzc3dvcmRDcmVkZW50aWFsc0gAEkUKD3Nzb19jcmVkZW50aWFscxgCIAEoCzIqLm1lbW9zLmFwaS52MS5TaWduSW5SZXF1ZXN0LlNTT0NyZWRlbnRpYWxzSAAaQwoTUGFzc3dvcmRDcmVkZW50aWFscxIVCgh1c2VybmFtZRgBIAEoCUID4EECEhUKCHBhc3N3b3JkGAIgASgJQgPgQQIacQoOU1NPQ3JlZGVudGlhbHMSFQoIaWRwX25hbWUYASABKAlCA+BBAhIRCgRjb2RlGAIgASgJQgPgQQISGQoMcmVkaXJlY3RfdXJpGAMgASgJQgPgQQISGgoNY29kZV92ZXJpZmllchgEIAEoCUID4EEBQg0KC2NyZWRlbnRpYWxzIoUBCg5TaWduSW5SZXNwb25zZRIgCgR1c2VyGAEgASgLMhIubWVtb3MuYXBpLnYxLlVzZXISFAoMYWNjZXNzX3Rva2VuGAIgASgJEjsKF2FjY2Vzc190b2tlbl9leHBpcmVzX2F0GAMgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcCIQCg5TaWduT3V0UmVxdWVzdCIVChNSZWZyZXNoVG9rZW5SZXF1ZXN0IlwKFFJlZnJlc2hUb2tlblJlc3BvbnNlEhQKDGFjY2Vzc190b2tlbhgBIAEoCRIuCgpleHBpcmVzX2F0GAIgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcDK/AwoLQXV0aFNlcnZpY2USdAoOR2V0Q3VycmVudFVzZXISIy5tZW1vcy5hcGkudjEuR2V0Q3VycmVudFVzZXJSZXF1ZXN0GiQubWVtb3MuYXBpLnYxLkdldEN1cnJlbnRVc2VyUmVzcG9uc2UiF4LT5JMCERIPL2FwaS92MS9hdXRoL21lEmMKBlNpZ25JbhIbLm1lbW9zLmFwaS52MS5TaWduSW5SZXF1ZXN0GhwubWVtb3MuYXBpLnYxLlNpZ25JblJlc3BvbnNlIh6C0+STAhg6ASoiEy9hcGkvdjEvYXV0aC9zaWduaW4SXQoHU2lnbk91dBIcLm1lbW9zLmFwaS52MS5TaWduT3V0UmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSIcgtPkkwIWIhQvYXBpL3YxL2F1dGgvc2lnbm91dBJ2CgxSZWZyZXNoVG9rZW4SIS5tZW1vcy5hcGkudjEuUmVmcmVzaFRva2VuUmVxdWVzdBoiLm1lbW9zLmFwaS52MS5SZWZyZXNoVG9rZW5SZXNwb25zZSIfgtPkkwIZOgEqIhQvYXBpL3YxL2F1dGgvcmVmcmVzaEKoAQoQY29tLm1lbW9zLmFwaS52MUIQQXV0aFNlcnZpY2VQcm90b1ABWjBnaXRodWIuY29tL3VzZW1lbW9zL21lbW9zL3Byb3RvL2dlbi9hcGkvdjE7YXBpdjGiAgNNQViqAgxNZW1vcy5BcGkuVjHKAgxNZW1vc1xBcGlcVjHiAhhNZW1vc1xBcGlcVjFcR1BCTWV0YWRhdGHqAg5NZW1vczo6QXBpOjpWMWIGcHJvdG8z\", [file_api_v1_user_service, file_google_api_annotations, file_google_api_field_behavior, file_google_protobuf_empty, file_google_protobuf_timestamp]);\n\n/**\n * @generated from message memos.api.v1.GetCurrentUserRequest\n */\nexport type GetCurrentUserRequest = Message<\"memos.api.v1.GetCurrentUserRequest\"> & {\n};\n\n/**\n * Describes the message memos.api.v1.GetCurrentUserRequest.\n * Use `create(GetCurrentUserRequestSchema)` to create a new message.\n */\nexport const GetCurrentUserRequestSchema: GenMessage<GetCurrentUserRequest> = /*@__PURE__*/\n  messageDesc(file_api_v1_auth_service, 0);\n\n/**\n * @generated from message memos.api.v1.GetCurrentUserResponse\n */\nexport type GetCurrentUserResponse = Message<\"memos.api.v1.GetCurrentUserResponse\"> & {\n  /**\n   * The authenticated user's information.\n   *\n   * @generated from field: memos.api.v1.User user = 1;\n   */\n  user?: User;\n};\n\n/**\n * Describes the message memos.api.v1.GetCurrentUserResponse.\n * Use `create(GetCurrentUserResponseSchema)` to create a new message.\n */\nexport const GetCurrentUserResponseSchema: GenMessage<GetCurrentUserResponse> = /*@__PURE__*/\n  messageDesc(file_api_v1_auth_service, 1);\n\n/**\n * @generated from message memos.api.v1.SignInRequest\n */\nexport type SignInRequest = Message<\"memos.api.v1.SignInRequest\"> & {\n  /**\n   * Authentication credentials. Provide one method.\n   *\n   * @generated from oneof memos.api.v1.SignInRequest.credentials\n   */\n  credentials: {\n    /**\n     * Username and password authentication.\n     *\n     * @generated from field: memos.api.v1.SignInRequest.PasswordCredentials password_credentials = 1;\n     */\n    value: SignInRequest_PasswordCredentials;\n    case: \"passwordCredentials\";\n  } | {\n    /**\n     * SSO provider authentication.\n     *\n     * @generated from field: memos.api.v1.SignInRequest.SSOCredentials sso_credentials = 2;\n     */\n    value: SignInRequest_SSOCredentials;\n    case: \"ssoCredentials\";\n  } | { case: undefined; value?: undefined };\n};\n\n/**\n * Describes the message memos.api.v1.SignInRequest.\n * Use `create(SignInRequestSchema)` to create a new message.\n */\nexport const SignInRequestSchema: GenMessage<SignInRequest> = /*@__PURE__*/\n  messageDesc(file_api_v1_auth_service, 2);\n\n/**\n * Nested message for password-based authentication credentials.\n *\n * @generated from message memos.api.v1.SignInRequest.PasswordCredentials\n */\nexport type SignInRequest_PasswordCredentials = Message<\"memos.api.v1.SignInRequest.PasswordCredentials\"> & {\n  /**\n   * The username to sign in with.\n   *\n   * @generated from field: string username = 1;\n   */\n  username: string;\n\n  /**\n   * The password to sign in with.\n   *\n   * @generated from field: string password = 2;\n   */\n  password: string;\n};\n\n/**\n * Describes the message memos.api.v1.SignInRequest.PasswordCredentials.\n * Use `create(SignInRequest_PasswordCredentialsSchema)` to create a new message.\n */\nexport const SignInRequest_PasswordCredentialsSchema: GenMessage<SignInRequest_PasswordCredentials> = /*@__PURE__*/\n  messageDesc(file_api_v1_auth_service, 2, 0);\n\n/**\n * Nested message for SSO authentication credentials.\n *\n * @generated from message memos.api.v1.SignInRequest.SSOCredentials\n */\nexport type SignInRequest_SSOCredentials = Message<\"memos.api.v1.SignInRequest.SSOCredentials\"> & {\n  /**\n   * The resource name of the SSO provider.\n   * Format: identity-providers/{uid}\n   *\n   * @generated from field: string idp_name = 1;\n   */\n  idpName: string;\n\n  /**\n   * The authorization code from the SSO provider.\n   *\n   * @generated from field: string code = 2;\n   */\n  code: string;\n\n  /**\n   * The redirect URI used in the SSO flow.\n   *\n   * @generated from field: string redirect_uri = 3;\n   */\n  redirectUri: string;\n\n  /**\n   * The PKCE code verifier for enhanced security (RFC 7636).\n   * Optional - enables PKCE flow protection against authorization code interception.\n   *\n   * @generated from field: string code_verifier = 4;\n   */\n  codeVerifier: string;\n};\n\n/**\n * Describes the message memos.api.v1.SignInRequest.SSOCredentials.\n * Use `create(SignInRequest_SSOCredentialsSchema)` to create a new message.\n */\nexport const SignInRequest_SSOCredentialsSchema: GenMessage<SignInRequest_SSOCredentials> = /*@__PURE__*/\n  messageDesc(file_api_v1_auth_service, 2, 1);\n\n/**\n * @generated from message memos.api.v1.SignInResponse\n */\nexport type SignInResponse = Message<\"memos.api.v1.SignInResponse\"> & {\n  /**\n   * The authenticated user's information.\n   *\n   * @generated from field: memos.api.v1.User user = 1;\n   */\n  user?: User;\n\n  /**\n   * The short-lived access token for API requests.\n   * Store in memory only, not in localStorage.\n   *\n   * @generated from field: string access_token = 2;\n   */\n  accessToken: string;\n\n  /**\n   * When the access token expires.\n   * Client should call RefreshToken before this time.\n   *\n   * @generated from field: google.protobuf.Timestamp access_token_expires_at = 3;\n   */\n  accessTokenExpiresAt?: Timestamp;\n};\n\n/**\n * Describes the message memos.api.v1.SignInResponse.\n * Use `create(SignInResponseSchema)` to create a new message.\n */\nexport const SignInResponseSchema: GenMessage<SignInResponse> = /*@__PURE__*/\n  messageDesc(file_api_v1_auth_service, 3);\n\n/**\n * @generated from message memos.api.v1.SignOutRequest\n */\nexport type SignOutRequest = Message<\"memos.api.v1.SignOutRequest\"> & {\n};\n\n/**\n * Describes the message memos.api.v1.SignOutRequest.\n * Use `create(SignOutRequestSchema)` to create a new message.\n */\nexport const SignOutRequestSchema: GenMessage<SignOutRequest> = /*@__PURE__*/\n  messageDesc(file_api_v1_auth_service, 4);\n\n/**\n * @generated from message memos.api.v1.RefreshTokenRequest\n */\nexport type RefreshTokenRequest = Message<\"memos.api.v1.RefreshTokenRequest\"> & {\n};\n\n/**\n * Describes the message memos.api.v1.RefreshTokenRequest.\n * Use `create(RefreshTokenRequestSchema)` to create a new message.\n */\nexport const RefreshTokenRequestSchema: GenMessage<RefreshTokenRequest> = /*@__PURE__*/\n  messageDesc(file_api_v1_auth_service, 5);\n\n/**\n * @generated from message memos.api.v1.RefreshTokenResponse\n */\nexport type RefreshTokenResponse = Message<\"memos.api.v1.RefreshTokenResponse\"> & {\n  /**\n   * The new short-lived access token.\n   *\n   * @generated from field: string access_token = 1;\n   */\n  accessToken: string;\n\n  /**\n   * When the access token expires.\n   *\n   * @generated from field: google.protobuf.Timestamp expires_at = 2;\n   */\n  expiresAt?: Timestamp;\n};\n\n/**\n * Describes the message memos.api.v1.RefreshTokenResponse.\n * Use `create(RefreshTokenResponseSchema)` to create a new message.\n */\nexport const RefreshTokenResponseSchema: GenMessage<RefreshTokenResponse> = /*@__PURE__*/\n  messageDesc(file_api_v1_auth_service, 6);\n\n/**\n * @generated from service memos.api.v1.AuthService\n */\nexport const AuthService: GenService<{\n  /**\n   * GetCurrentUser returns the authenticated user's information.\n   * Validates the access token and returns user details.\n   * Similar to OIDC's /userinfo endpoint.\n   *\n   * @generated from rpc memos.api.v1.AuthService.GetCurrentUser\n   */\n  getCurrentUser: {\n    methodKind: \"unary\";\n    input: typeof GetCurrentUserRequestSchema;\n    output: typeof GetCurrentUserResponseSchema;\n  },\n  /**\n   * SignIn authenticates a user with credentials and returns tokens.\n   * On success, returns an access token and sets a refresh token cookie.\n   * Supports password-based and SSO authentication methods.\n   *\n   * @generated from rpc memos.api.v1.AuthService.SignIn\n   */\n  signIn: {\n    methodKind: \"unary\";\n    input: typeof SignInRequestSchema;\n    output: typeof SignInResponseSchema;\n  },\n  /**\n   * SignOut terminates the user's authentication.\n   * Revokes the refresh token and clears the authentication cookie.\n   *\n   * @generated from rpc memos.api.v1.AuthService.SignOut\n   */\n  signOut: {\n    methodKind: \"unary\";\n    input: typeof SignOutRequestSchema;\n    output: typeof EmptySchema;\n  },\n  /**\n   * RefreshToken exchanges a valid refresh token for a new access token.\n   * The refresh token is read from the HttpOnly cookie.\n   * Returns a new short-lived access token.\n   *\n   * @generated from rpc memos.api.v1.AuthService.RefreshToken\n   */\n  refreshToken: {\n    methodKind: \"unary\";\n    input: typeof RefreshTokenRequestSchema;\n    output: typeof RefreshTokenResponseSchema;\n  },\n}> = /*@__PURE__*/\n  serviceDesc(file_api_v1_auth_service, 0);\n\n"
  },
  {
    "path": "web/src/types/proto/api/v1/common_pb.ts",
    "content": "// @generated by protoc-gen-es v2.11.0 with parameter \"target=ts\"\n// @generated from file api/v1/common.proto (package memos.api.v1, syntax proto3)\n/* eslint-disable */\n\nimport type { GenEnum, GenFile, GenMessage } from \"@bufbuild/protobuf/codegenv2\";\nimport { enumDesc, fileDesc, messageDesc } from \"@bufbuild/protobuf/codegenv2\";\nimport type { Message } from \"@bufbuild/protobuf\";\n\n/**\n * Describes the file api/v1/common.proto.\n */\nexport const file_api_v1_common: GenFile = /*@__PURE__*/\n  fileDesc(\"ChNhcGkvdjEvY29tbW9uLnByb3RvEgxtZW1vcy5hcGkudjEiKgoJUGFnZVRva2VuEg0KBWxpbWl0GAEgASgFEg4KBm9mZnNldBgCIAEoBSo4CgVTdGF0ZRIVChFTVEFURV9VTlNQRUNJRklFRBAAEgoKBk5PUk1BTBABEgwKCEFSQ0hJVkVEEAIqOQoJRGlyZWN0aW9uEhkKFURJUkVDVElPTl9VTlNQRUNJRklFRBAAEgcKA0FTQxABEggKBERFU0MQAkKjAQoQY29tLm1lbW9zLmFwaS52MUILQ29tbW9uUHJvdG9QAVowZ2l0aHViLmNvbS91c2VtZW1vcy9tZW1vcy9wcm90by9nZW4vYXBpL3YxO2FwaXYxogIDTUFYqgIMTWVtb3MuQXBpLlYxygIMTWVtb3NcQXBpXFYx4gIYTWVtb3NcQXBpXFYxXEdQQk1ldGFkYXRh6gIOTWVtb3M6OkFwaTo6VjFiBnByb3RvMw\");\n\n/**\n * Used internally for obfuscating the page token.\n *\n * @generated from message memos.api.v1.PageToken\n */\nexport type PageToken = Message<\"memos.api.v1.PageToken\"> & {\n  /**\n   * @generated from field: int32 limit = 1;\n   */\n  limit: number;\n\n  /**\n   * @generated from field: int32 offset = 2;\n   */\n  offset: number;\n};\n\n/**\n * Describes the message memos.api.v1.PageToken.\n * Use `create(PageTokenSchema)` to create a new message.\n */\nexport const PageTokenSchema: GenMessage<PageToken> = /*@__PURE__*/\n  messageDesc(file_api_v1_common, 0);\n\n/**\n * @generated from enum memos.api.v1.State\n */\nexport enum State {\n  /**\n   * @generated from enum value: STATE_UNSPECIFIED = 0;\n   */\n  STATE_UNSPECIFIED = 0,\n\n  /**\n   * @generated from enum value: NORMAL = 1;\n   */\n  NORMAL = 1,\n\n  /**\n   * @generated from enum value: ARCHIVED = 2;\n   */\n  ARCHIVED = 2,\n}\n\n/**\n * Describes the enum memos.api.v1.State.\n */\nexport const StateSchema: GenEnum<State> = /*@__PURE__*/\n  enumDesc(file_api_v1_common, 0);\n\n/**\n * @generated from enum memos.api.v1.Direction\n */\nexport enum Direction {\n  /**\n   * @generated from enum value: DIRECTION_UNSPECIFIED = 0;\n   */\n  DIRECTION_UNSPECIFIED = 0,\n\n  /**\n   * @generated from enum value: ASC = 1;\n   */\n  ASC = 1,\n\n  /**\n   * @generated from enum value: DESC = 2;\n   */\n  DESC = 2,\n}\n\n/**\n * Describes the enum memos.api.v1.Direction.\n */\nexport const DirectionSchema: GenEnum<Direction> = /*@__PURE__*/\n  enumDesc(file_api_v1_common, 1);\n\n"
  },
  {
    "path": "web/src/types/proto/api/v1/idp_service_pb.ts",
    "content": "// @generated by protoc-gen-es v2.11.0 with parameter \"target=ts\"\n// @generated from file api/v1/idp_service.proto (package memos.api.v1, syntax proto3)\n/* eslint-disable */\n\nimport type { GenEnum, GenFile, GenMessage, GenService } from \"@bufbuild/protobuf/codegenv2\";\nimport { enumDesc, fileDesc, messageDesc, serviceDesc } from \"@bufbuild/protobuf/codegenv2\";\nimport { file_google_api_annotations } from \"../../google/api/annotations_pb\";\nimport { file_google_api_client } from \"../../google/api/client_pb\";\nimport { file_google_api_field_behavior } from \"../../google/api/field_behavior_pb\";\nimport { file_google_api_resource } from \"../../google/api/resource_pb\";\nimport type { EmptySchema, FieldMask } from \"@bufbuild/protobuf/wkt\";\nimport { file_google_protobuf_empty, file_google_protobuf_field_mask } from \"@bufbuild/protobuf/wkt\";\nimport type { Message } from \"@bufbuild/protobuf\";\n\n/**\n * Describes the file api/v1/idp_service.proto.\n */\nexport const file_api_v1_idp_service: GenFile = /*@__PURE__*/\n  fileDesc(\"ChhhcGkvdjEvaWRwX3NlcnZpY2UucHJvdG8SDG1lbW9zLmFwaS52MSLfAgoQSWRlbnRpdHlQcm92aWRlchIRCgRuYW1lGAEgASgJQgPgQQgSNgoEdHlwZRgCIAEoDjIjLm1lbW9zLmFwaS52MS5JZGVudGl0eVByb3ZpZGVyLlR5cGVCA+BBAhISCgV0aXRsZRgDIAEoCUID4EECEh4KEWlkZW50aWZpZXJfZmlsdGVyGAQgASgJQgPgQQESOQoGY29uZmlnGAUgASgLMiQubWVtb3MuYXBpLnYxLklkZW50aXR5UHJvdmlkZXJDb25maWdCA+BBAiIoCgRUeXBlEhQKEFRZUEVfVU5TUEVDSUZJRUQQABIKCgZPQVVUSDIQATpn6kFkCh1tZW1vcy5hcGkudjEvSWRlbnRpdHlQcm92aWRlchIYaWRlbnRpdHktcHJvdmlkZXJzL3tpZHB9GgRuYW1lKhFpZGVudGl0eVByb3ZpZGVyczIQaWRlbnRpdHlQcm92aWRlciJXChZJZGVudGl0eVByb3ZpZGVyQ29uZmlnEjMKDW9hdXRoMl9jb25maWcYASABKAsyGi5tZW1vcy5hcGkudjEuT0F1dGgyQ29uZmlnSABCCAoGY29uZmlnIlsKDEZpZWxkTWFwcGluZxISCgppZGVudGlmaWVyGAEgASgJEhQKDGRpc3BsYXlfbmFtZRgCIAEoCRINCgVlbWFpbBgDIAEoCRISCgphdmF0YXJfdXJsGAQgASgJIrcBCgxPQXV0aDJDb25maWcSEQoJY2xpZW50X2lkGAEgASgJEhUKDWNsaWVudF9zZWNyZXQYAiABKAkSEAoIYXV0aF91cmwYAyABKAkSEQoJdG9rZW5fdXJsGAQgASgJEhUKDXVzZXJfaW5mb191cmwYBSABKAkSDgoGc2NvcGVzGAYgAygJEjEKDWZpZWxkX21hcHBpbmcYByABKAsyGi5tZW1vcy5hcGkudjEuRmllbGRNYXBwaW5nIh4KHExpc3RJZGVudGl0eVByb3ZpZGVyc1JlcXVlc3QiWwodTGlzdElkZW50aXR5UHJvdmlkZXJzUmVzcG9uc2USOgoSaWRlbnRpdHlfcHJvdmlkZXJzGAEgAygLMh4ubWVtb3MuYXBpLnYxLklkZW50aXR5UHJvdmlkZXIiUQoaR2V0SWRlbnRpdHlQcm92aWRlclJlcXVlc3QSMwoEbmFtZRgBIAEoCUIl4EEC+kEfCh1tZW1vcy5hcGkudjEvSWRlbnRpdHlQcm92aWRlciKCAQodQ3JlYXRlSWRlbnRpdHlQcm92aWRlclJlcXVlc3QSPgoRaWRlbnRpdHlfcHJvdmlkZXIYASABKAsyHi5tZW1vcy5hcGkudjEuSWRlbnRpdHlQcm92aWRlckID4EECEiEKFGlkZW50aXR5X3Byb3ZpZGVyX2lkGAIgASgJQgPgQQEilQEKHVVwZGF0ZUlkZW50aXR5UHJvdmlkZXJSZXF1ZXN0Ej4KEWlkZW50aXR5X3Byb3ZpZGVyGAEgASgLMh4ubWVtb3MuYXBpLnYxLklkZW50aXR5UHJvdmlkZXJCA+BBAhI0Cgt1cGRhdGVfbWFzaxgCIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5GaWVsZE1hc2tCA+BBAiJUCh1EZWxldGVJZGVudGl0eVByb3ZpZGVyUmVxdWVzdBIzCgRuYW1lGAEgASgJQiXgQQL6QR8KHW1lbW9zLmFwaS52MS9JZGVudGl0eVByb3ZpZGVyMucGChdJZGVudGl0eVByb3ZpZGVyU2VydmljZRKUAQoVTGlzdElkZW50aXR5UHJvdmlkZXJzEioubWVtb3MuYXBpLnYxLkxpc3RJZGVudGl0eVByb3ZpZGVyc1JlcXVlc3QaKy5tZW1vcy5hcGkudjEuTGlzdElkZW50aXR5UHJvdmlkZXJzUmVzcG9uc2UiIoLT5JMCHBIaL2FwaS92MS9pZGVudGl0eS1wcm92aWRlcnMSkwEKE0dldElkZW50aXR5UHJvdmlkZXISKC5tZW1vcy5hcGkudjEuR2V0SWRlbnRpdHlQcm92aWRlclJlcXVlc3QaHi5tZW1vcy5hcGkudjEuSWRlbnRpdHlQcm92aWRlciIy2kEEbmFtZYLT5JMCJRIjL2FwaS92MS97bmFtZT1pZGVudGl0eS1wcm92aWRlcnMvKn0SsAEKFkNyZWF0ZUlkZW50aXR5UHJvdmlkZXISKy5tZW1vcy5hcGkudjEuQ3JlYXRlSWRlbnRpdHlQcm92aWRlclJlcXVlc3QaHi5tZW1vcy5hcGkudjEuSWRlbnRpdHlQcm92aWRlciJJ2kERaWRlbnRpdHlfcHJvdmlkZXKC0+STAi86EWlkZW50aXR5X3Byb3ZpZGVyIhovYXBpL3YxL2lkZW50aXR5LXByb3ZpZGVycxLXAQoWVXBkYXRlSWRlbnRpdHlQcm92aWRlchIrLm1lbW9zLmFwaS52MS5VcGRhdGVJZGVudGl0eVByb3ZpZGVyUmVxdWVzdBoeLm1lbW9zLmFwaS52MS5JZGVudGl0eVByb3ZpZGVyInDaQR1pZGVudGl0eV9wcm92aWRlcix1cGRhdGVfbWFza4LT5JMCSjoRaWRlbnRpdHlfcHJvdmlkZXIyNS9hcGkvdjEve2lkZW50aXR5X3Byb3ZpZGVyLm5hbWU9aWRlbnRpdHktcHJvdmlkZXJzLyp9EpEBChZEZWxldGVJZGVudGl0eVByb3ZpZGVyEisubWVtb3MuYXBpLnYxLkRlbGV0ZUlkZW50aXR5UHJvdmlkZXJSZXF1ZXN0GhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5IjLaQQRuYW1lgtPkkwIlKiMvYXBpL3YxL3tuYW1lPWlkZW50aXR5LXByb3ZpZGVycy8qfUKnAQoQY29tLm1lbW9zLmFwaS52MUIPSWRwU2VydmljZVByb3RvUAFaMGdpdGh1Yi5jb20vdXNlbWVtb3MvbWVtb3MvcHJvdG8vZ2VuL2FwaS92MTthcGl2MaICA01BWKoCDE1lbW9zLkFwaS5WMcoCDE1lbW9zXEFwaVxWMeICGE1lbW9zXEFwaVxWMVxHUEJNZXRhZGF0YeoCDk1lbW9zOjpBcGk6OlYxYgZwcm90bzM\", [file_google_api_annotations, file_google_api_client, file_google_api_field_behavior, file_google_api_resource, file_google_protobuf_empty, file_google_protobuf_field_mask]);\n\n/**\n * @generated from message memos.api.v1.IdentityProvider\n */\nexport type IdentityProvider = Message<\"memos.api.v1.IdentityProvider\"> & {\n  /**\n   * The resource name of the identity provider.\n   * Format: identity-providers/{idp}\n   *\n   * @generated from field: string name = 1;\n   */\n  name: string;\n\n  /**\n   * Required. The type of the identity provider.\n   *\n   * @generated from field: memos.api.v1.IdentityProvider.Type type = 2;\n   */\n  type: IdentityProvider_Type;\n\n  /**\n   * Required. The display title of the identity provider.\n   *\n   * @generated from field: string title = 3;\n   */\n  title: string;\n\n  /**\n   * Optional. Filter applied to user identifiers.\n   *\n   * @generated from field: string identifier_filter = 4;\n   */\n  identifierFilter: string;\n\n  /**\n   * Required. Configuration for the identity provider.\n   *\n   * @generated from field: memos.api.v1.IdentityProviderConfig config = 5;\n   */\n  config?: IdentityProviderConfig;\n};\n\n/**\n * Describes the message memos.api.v1.IdentityProvider.\n * Use `create(IdentityProviderSchema)` to create a new message.\n */\nexport const IdentityProviderSchema: GenMessage<IdentityProvider> = /*@__PURE__*/\n  messageDesc(file_api_v1_idp_service, 0);\n\n/**\n * @generated from enum memos.api.v1.IdentityProvider.Type\n */\nexport enum IdentityProvider_Type {\n  /**\n   * @generated from enum value: TYPE_UNSPECIFIED = 0;\n   */\n  TYPE_UNSPECIFIED = 0,\n\n  /**\n   * OAuth2 identity provider.\n   *\n   * @generated from enum value: OAUTH2 = 1;\n   */\n  OAUTH2 = 1,\n}\n\n/**\n * Describes the enum memos.api.v1.IdentityProvider.Type.\n */\nexport const IdentityProvider_TypeSchema: GenEnum<IdentityProvider_Type> = /*@__PURE__*/\n  enumDesc(file_api_v1_idp_service, 0, 0);\n\n/**\n * @generated from message memos.api.v1.IdentityProviderConfig\n */\nexport type IdentityProviderConfig = Message<\"memos.api.v1.IdentityProviderConfig\"> & {\n  /**\n   * @generated from oneof memos.api.v1.IdentityProviderConfig.config\n   */\n  config: {\n    /**\n     * @generated from field: memos.api.v1.OAuth2Config oauth2_config = 1;\n     */\n    value: OAuth2Config;\n    case: \"oauth2Config\";\n  } | { case: undefined; value?: undefined };\n};\n\n/**\n * Describes the message memos.api.v1.IdentityProviderConfig.\n * Use `create(IdentityProviderConfigSchema)` to create a new message.\n */\nexport const IdentityProviderConfigSchema: GenMessage<IdentityProviderConfig> = /*@__PURE__*/\n  messageDesc(file_api_v1_idp_service, 1);\n\n/**\n * @generated from message memos.api.v1.FieldMapping\n */\nexport type FieldMapping = Message<\"memos.api.v1.FieldMapping\"> & {\n  /**\n   * @generated from field: string identifier = 1;\n   */\n  identifier: string;\n\n  /**\n   * @generated from field: string display_name = 2;\n   */\n  displayName: string;\n\n  /**\n   * @generated from field: string email = 3;\n   */\n  email: string;\n\n  /**\n   * @generated from field: string avatar_url = 4;\n   */\n  avatarUrl: string;\n};\n\n/**\n * Describes the message memos.api.v1.FieldMapping.\n * Use `create(FieldMappingSchema)` to create a new message.\n */\nexport const FieldMappingSchema: GenMessage<FieldMapping> = /*@__PURE__*/\n  messageDesc(file_api_v1_idp_service, 2);\n\n/**\n * @generated from message memos.api.v1.OAuth2Config\n */\nexport type OAuth2Config = Message<\"memos.api.v1.OAuth2Config\"> & {\n  /**\n   * @generated from field: string client_id = 1;\n   */\n  clientId: string;\n\n  /**\n   * @generated from field: string client_secret = 2;\n   */\n  clientSecret: string;\n\n  /**\n   * @generated from field: string auth_url = 3;\n   */\n  authUrl: string;\n\n  /**\n   * @generated from field: string token_url = 4;\n   */\n  tokenUrl: string;\n\n  /**\n   * @generated from field: string user_info_url = 5;\n   */\n  userInfoUrl: string;\n\n  /**\n   * @generated from field: repeated string scopes = 6;\n   */\n  scopes: string[];\n\n  /**\n   * @generated from field: memos.api.v1.FieldMapping field_mapping = 7;\n   */\n  fieldMapping?: FieldMapping;\n};\n\n/**\n * Describes the message memos.api.v1.OAuth2Config.\n * Use `create(OAuth2ConfigSchema)` to create a new message.\n */\nexport const OAuth2ConfigSchema: GenMessage<OAuth2Config> = /*@__PURE__*/\n  messageDesc(file_api_v1_idp_service, 3);\n\n/**\n * @generated from message memos.api.v1.ListIdentityProvidersRequest\n */\nexport type ListIdentityProvidersRequest = Message<\"memos.api.v1.ListIdentityProvidersRequest\"> & {\n};\n\n/**\n * Describes the message memos.api.v1.ListIdentityProvidersRequest.\n * Use `create(ListIdentityProvidersRequestSchema)` to create a new message.\n */\nexport const ListIdentityProvidersRequestSchema: GenMessage<ListIdentityProvidersRequest> = /*@__PURE__*/\n  messageDesc(file_api_v1_idp_service, 4);\n\n/**\n * @generated from message memos.api.v1.ListIdentityProvidersResponse\n */\nexport type ListIdentityProvidersResponse = Message<\"memos.api.v1.ListIdentityProvidersResponse\"> & {\n  /**\n   * The list of identity providers.\n   *\n   * @generated from field: repeated memos.api.v1.IdentityProvider identity_providers = 1;\n   */\n  identityProviders: IdentityProvider[];\n};\n\n/**\n * Describes the message memos.api.v1.ListIdentityProvidersResponse.\n * Use `create(ListIdentityProvidersResponseSchema)` to create a new message.\n */\nexport const ListIdentityProvidersResponseSchema: GenMessage<ListIdentityProvidersResponse> = /*@__PURE__*/\n  messageDesc(file_api_v1_idp_service, 5);\n\n/**\n * @generated from message memos.api.v1.GetIdentityProviderRequest\n */\nexport type GetIdentityProviderRequest = Message<\"memos.api.v1.GetIdentityProviderRequest\"> & {\n  /**\n   * Required. The resource name of the identity provider to get.\n   * Format: identity-providers/{idp}\n   *\n   * @generated from field: string name = 1;\n   */\n  name: string;\n};\n\n/**\n * Describes the message memos.api.v1.GetIdentityProviderRequest.\n * Use `create(GetIdentityProviderRequestSchema)` to create a new message.\n */\nexport const GetIdentityProviderRequestSchema: GenMessage<GetIdentityProviderRequest> = /*@__PURE__*/\n  messageDesc(file_api_v1_idp_service, 6);\n\n/**\n * @generated from message memos.api.v1.CreateIdentityProviderRequest\n */\nexport type CreateIdentityProviderRequest = Message<\"memos.api.v1.CreateIdentityProviderRequest\"> & {\n  /**\n   * Required. The identity provider to create.\n   *\n   * @generated from field: memos.api.v1.IdentityProvider identity_provider = 1;\n   */\n  identityProvider?: IdentityProvider;\n\n  /**\n   * Optional. The ID to use for the identity provider, which will become the final component of the resource name.\n   * If not provided, the system will generate one.\n   *\n   * @generated from field: string identity_provider_id = 2;\n   */\n  identityProviderId: string;\n};\n\n/**\n * Describes the message memos.api.v1.CreateIdentityProviderRequest.\n * Use `create(CreateIdentityProviderRequestSchema)` to create a new message.\n */\nexport const CreateIdentityProviderRequestSchema: GenMessage<CreateIdentityProviderRequest> = /*@__PURE__*/\n  messageDesc(file_api_v1_idp_service, 7);\n\n/**\n * @generated from message memos.api.v1.UpdateIdentityProviderRequest\n */\nexport type UpdateIdentityProviderRequest = Message<\"memos.api.v1.UpdateIdentityProviderRequest\"> & {\n  /**\n   * Required. The identity provider to update.\n   *\n   * @generated from field: memos.api.v1.IdentityProvider identity_provider = 1;\n   */\n  identityProvider?: IdentityProvider;\n\n  /**\n   * Required. The update mask applies to the resource. Only the top level fields of\n   * IdentityProvider are supported.\n   *\n   * @generated from field: google.protobuf.FieldMask update_mask = 2;\n   */\n  updateMask?: FieldMask;\n};\n\n/**\n * Describes the message memos.api.v1.UpdateIdentityProviderRequest.\n * Use `create(UpdateIdentityProviderRequestSchema)` to create a new message.\n */\nexport const UpdateIdentityProviderRequestSchema: GenMessage<UpdateIdentityProviderRequest> = /*@__PURE__*/\n  messageDesc(file_api_v1_idp_service, 8);\n\n/**\n * @generated from message memos.api.v1.DeleteIdentityProviderRequest\n */\nexport type DeleteIdentityProviderRequest = Message<\"memos.api.v1.DeleteIdentityProviderRequest\"> & {\n  /**\n   * Required. The resource name of the identity provider to delete.\n   * Format: identity-providers/{idp}\n   *\n   * @generated from field: string name = 1;\n   */\n  name: string;\n};\n\n/**\n * Describes the message memos.api.v1.DeleteIdentityProviderRequest.\n * Use `create(DeleteIdentityProviderRequestSchema)` to create a new message.\n */\nexport const DeleteIdentityProviderRequestSchema: GenMessage<DeleteIdentityProviderRequest> = /*@__PURE__*/\n  messageDesc(file_api_v1_idp_service, 9);\n\n/**\n * @generated from service memos.api.v1.IdentityProviderService\n */\nexport const IdentityProviderService: GenService<{\n  /**\n   * ListIdentityProviders lists identity providers.\n   *\n   * @generated from rpc memos.api.v1.IdentityProviderService.ListIdentityProviders\n   */\n  listIdentityProviders: {\n    methodKind: \"unary\";\n    input: typeof ListIdentityProvidersRequestSchema;\n    output: typeof ListIdentityProvidersResponseSchema;\n  },\n  /**\n   * GetIdentityProvider gets an identity provider.\n   *\n   * @generated from rpc memos.api.v1.IdentityProviderService.GetIdentityProvider\n   */\n  getIdentityProvider: {\n    methodKind: \"unary\";\n    input: typeof GetIdentityProviderRequestSchema;\n    output: typeof IdentityProviderSchema;\n  },\n  /**\n   * CreateIdentityProvider creates an identity provider.\n   *\n   * @generated from rpc memos.api.v1.IdentityProviderService.CreateIdentityProvider\n   */\n  createIdentityProvider: {\n    methodKind: \"unary\";\n    input: typeof CreateIdentityProviderRequestSchema;\n    output: typeof IdentityProviderSchema;\n  },\n  /**\n   * UpdateIdentityProvider updates an identity provider.\n   *\n   * @generated from rpc memos.api.v1.IdentityProviderService.UpdateIdentityProvider\n   */\n  updateIdentityProvider: {\n    methodKind: \"unary\";\n    input: typeof UpdateIdentityProviderRequestSchema;\n    output: typeof IdentityProviderSchema;\n  },\n  /**\n   * DeleteIdentityProvider deletes an identity provider.\n   *\n   * @generated from rpc memos.api.v1.IdentityProviderService.DeleteIdentityProvider\n   */\n  deleteIdentityProvider: {\n    methodKind: \"unary\";\n    input: typeof DeleteIdentityProviderRequestSchema;\n    output: typeof EmptySchema;\n  },\n}> = /*@__PURE__*/\n  serviceDesc(file_api_v1_idp_service, 0);\n\n"
  },
  {
    "path": "web/src/types/proto/api/v1/instance_service_pb.ts",
    "content": "// @generated by protoc-gen-es v2.11.0 with parameter \"target=ts\"\n// @generated from file api/v1/instance_service.proto (package memos.api.v1, syntax proto3)\n/* eslint-disable */\n\nimport type { GenEnum, GenFile, GenMessage, GenService } from \"@bufbuild/protobuf/codegenv2\";\nimport { enumDesc, fileDesc, messageDesc, serviceDesc } from \"@bufbuild/protobuf/codegenv2\";\nimport type { User } from \"./user_service_pb\";\nimport { file_api_v1_user_service } from \"./user_service_pb\";\nimport { file_google_api_annotations } from \"../../google/api/annotations_pb\";\nimport { file_google_api_client } from \"../../google/api/client_pb\";\nimport { file_google_api_field_behavior } from \"../../google/api/field_behavior_pb\";\nimport { file_google_api_resource } from \"../../google/api/resource_pb\";\nimport type { FieldMask } from \"@bufbuild/protobuf/wkt\";\nimport { file_google_protobuf_field_mask } from \"@bufbuild/protobuf/wkt\";\nimport type { Color } from \"../../google/type/color_pb\";\nimport { file_google_type_color } from \"../../google/type/color_pb\";\nimport type { Message } from \"@bufbuild/protobuf\";\n\n/**\n * Describes the file api/v1/instance_service.proto.\n */\nexport const file_api_v1_instance_service: GenFile = /*@__PURE__*/\n  fileDesc(\"Ch1hcGkvdjEvaW5zdGFuY2Vfc2VydmljZS5wcm90bxIMbWVtb3MuYXBpLnYxImkKD0luc3RhbmNlUHJvZmlsZRIPCgd2ZXJzaW9uGAIgASgJEgwKBGRlbW8YAyABKAgSFAoMaW5zdGFuY2VfdXJsGAYgASgJEiEKBWFkbWluGAcgASgLMhIubWVtb3MuYXBpLnYxLlVzZXIiGwoZR2V0SW5zdGFuY2VQcm9maWxlUmVxdWVzdCLhEAoPSW5zdGFuY2VTZXR0aW5nEhEKBG5hbWUYASABKAlCA+BBCBJHCg9nZW5lcmFsX3NldHRpbmcYAiABKAsyLC5tZW1vcy5hcGkudjEuSW5zdGFuY2VTZXR0aW5nLkdlbmVyYWxTZXR0aW5nSAASRwoPc3RvcmFnZV9zZXR0aW5nGAMgASgLMiwubWVtb3MuYXBpLnYxLkluc3RhbmNlU2V0dGluZy5TdG9yYWdlU2V0dGluZ0gAElAKFG1lbW9fcmVsYXRlZF9zZXR0aW5nGAQgASgLMjAubWVtb3MuYXBpLnYxLkluc3RhbmNlU2V0dGluZy5NZW1vUmVsYXRlZFNldHRpbmdIABJBCgx0YWdzX3NldHRpbmcYBSABKAsyKS5tZW1vcy5hcGkudjEuSW5zdGFuY2VTZXR0aW5nLlRhZ3NTZXR0aW5nSAASUQoUbm90aWZpY2F0aW9uX3NldHRpbmcYBiABKAsyMS5tZW1vcy5hcGkudjEuSW5zdGFuY2VTZXR0aW5nLk5vdGlmaWNhdGlvblNldHRpbmdIABqHAwoOR2VuZXJhbFNldHRpbmcSIgoaZGlzYWxsb3dfdXNlcl9yZWdpc3RyYXRpb24YAiABKAgSHgoWZGlzYWxsb3dfcGFzc3dvcmRfYXV0aBgDIAEoCBIZChFhZGRpdGlvbmFsX3NjcmlwdBgEIAEoCRIYChBhZGRpdGlvbmFsX3N0eWxlGAUgASgJElIKDmN1c3RvbV9wcm9maWxlGAYgASgLMjoubWVtb3MuYXBpLnYxLkluc3RhbmNlU2V0dGluZy5HZW5lcmFsU2V0dGluZy5DdXN0b21Qcm9maWxlEh0KFXdlZWtfc3RhcnRfZGF5X29mZnNldBgHIAEoBRIgChhkaXNhbGxvd19jaGFuZ2VfdXNlcm5hbWUYCCABKAgSIAoYZGlzYWxsb3dfY2hhbmdlX25pY2tuYW1lGAkgASgIGkUKDUN1c3RvbVByb2ZpbGUSDQoFdGl0bGUYASABKAkSEwoLZGVzY3JpcHRpb24YAiABKAkSEAoIbG9nb191cmwYAyABKAkaugMKDlN0b3JhZ2VTZXR0aW5nEk4KDHN0b3JhZ2VfdHlwZRgBIAEoDjI4Lm1lbW9zLmFwaS52MS5JbnN0YW5jZVNldHRpbmcuU3RvcmFnZVNldHRpbmcuU3RvcmFnZVR5cGUSGQoRZmlsZXBhdGhfdGVtcGxhdGUYAiABKAkSHAoUdXBsb2FkX3NpemVfbGltaXRfbWIYAyABKAMSSAoJczNfY29uZmlnGAQgASgLMjUubWVtb3MuYXBpLnYxLkluc3RhbmNlU2V0dGluZy5TdG9yYWdlU2V0dGluZy5TM0NvbmZpZxqGAQoIUzNDb25maWcSFQoNYWNjZXNzX2tleV9pZBgBIAEoCRIZChFhY2Nlc3Nfa2V5X3NlY3JldBgCIAEoCRIQCghlbmRwb2ludBgDIAEoCRIOCgZyZWdpb24YBCABKAkSDgoGYnVja2V0GAUgASgJEhYKDnVzZV9wYXRoX3N0eWxlGAYgASgIIkwKC1N0b3JhZ2VUeXBlEhwKGFNUT1JBR0VfVFlQRV9VTlNQRUNJRklFRBAAEgwKCERBVEFCQVNFEAESCQoFTE9DQUwQAhIGCgJTMxADGokBChJNZW1vUmVsYXRlZFNldHRpbmcSIAoYZGlzcGxheV93aXRoX3VwZGF0ZV90aW1lGAIgASgIEhwKFGNvbnRlbnRfbGVuZ3RoX2xpbWl0GAMgASgFEiAKGGVuYWJsZV9kb3VibGVfY2xpY2tfZWRpdBgEIAEoCBIRCglyZWFjdGlvbnMYByADKAkaOwoLVGFnTWV0YWRhdGESLAoQYmFja2dyb3VuZF9jb2xvchgBIAEoCzISLmdvb2dsZS50eXBlLkNvbG9yGqgBCgtUYWdzU2V0dGluZxJBCgR0YWdzGAEgAygLMjMubWVtb3MuYXBpLnYxLkluc3RhbmNlU2V0dGluZy5UYWdzU2V0dGluZy5UYWdzRW50cnkaVgoJVGFnc0VudHJ5EgsKA2tleRgBIAEoCRI4CgV2YWx1ZRgCIAEoCzIpLm1lbW9zLmFwaS52MS5JbnN0YW5jZVNldHRpbmcuVGFnTWV0YWRhdGE6AjgBGrUCChNOb3RpZmljYXRpb25TZXR0aW5nEk0KBWVtYWlsGAEgASgLMj4ubWVtb3MuYXBpLnYxLkluc3RhbmNlU2V0dGluZy5Ob3RpZmljYXRpb25TZXR0aW5nLkVtYWlsU2V0dGluZxrOAQoMRW1haWxTZXR0aW5nEg8KB2VuYWJsZWQYASABKAgSEQoJc210cF9ob3N0GAIgASgJEhEKCXNtdHBfcG9ydBgDIAEoBRIVCg1zbXRwX3VzZXJuYW1lGAQgASgJEhUKDXNtdHBfcGFzc3dvcmQYBSABKAkSEgoKZnJvbV9lbWFpbBgGIAEoCRIRCglmcm9tX25hbWUYByABKAkSEAoIcmVwbHlfdG8YCCABKAkSDwoHdXNlX3RscxgJIAEoCBIPCgd1c2Vfc3NsGAogASgIImIKA0tleRITCg9LRVlfVU5TUEVDSUZJRUQQABILCgdHRU5FUkFMEAESCwoHU1RPUkFHRRACEhAKDE1FTU9fUkVMQVRFRBADEggKBFRBR1MQBBIQCgxOT1RJRklDQVRJT04QBTph6kFeChxtZW1vcy5hcGkudjEvSW5zdGFuY2VTZXR0aW5nEhtpbnN0YW5jZS9zZXR0aW5ncy97c2V0dGluZ30qEGluc3RhbmNlU2V0dGluZ3MyD2luc3RhbmNlU2V0dGluZ0IHCgV2YWx1ZSJPChlHZXRJbnN0YW5jZVNldHRpbmdSZXF1ZXN0EjIKBG5hbWUYASABKAlCJOBBAvpBHgocbWVtb3MuYXBpLnYxL0luc3RhbmNlU2V0dGluZyKJAQocVXBkYXRlSW5zdGFuY2VTZXR0aW5nUmVxdWVzdBIzCgdzZXR0aW5nGAEgASgLMh0ubWVtb3MuYXBpLnYxLkluc3RhbmNlU2V0dGluZ0ID4EECEjQKC3VwZGF0ZV9tYXNrGAIgASgLMhouZ29vZ2xlLnByb3RvYnVmLkZpZWxkTWFza0ID4EEBMtsDCg9JbnN0YW5jZVNlcnZpY2USfgoSR2V0SW5zdGFuY2VQcm9maWxlEicubWVtb3MuYXBpLnYxLkdldEluc3RhbmNlUHJvZmlsZVJlcXVlc3QaHS5tZW1vcy5hcGkudjEuSW5zdGFuY2VQcm9maWxlIiCC0+STAhoSGC9hcGkvdjEvaW5zdGFuY2UvcHJvZmlsZRKPAQoSR2V0SW5zdGFuY2VTZXR0aW5nEicubWVtb3MuYXBpLnYxLkdldEluc3RhbmNlU2V0dGluZ1JlcXVlc3QaHS5tZW1vcy5hcGkudjEuSW5zdGFuY2VTZXR0aW5nIjHaQQRuYW1lgtPkkwIkEiIvYXBpL3YxL3tuYW1lPWluc3RhbmNlL3NldHRpbmdzLyp9ErUBChVVcGRhdGVJbnN0YW5jZVNldHRpbmcSKi5tZW1vcy5hcGkudjEuVXBkYXRlSW5zdGFuY2VTZXR0aW5nUmVxdWVzdBodLm1lbW9zLmFwaS52MS5JbnN0YW5jZVNldHRpbmciUdpBE3NldHRpbmcsdXBkYXRlX21hc2uC0+STAjU6B3NldHRpbmcyKi9hcGkvdjEve3NldHRpbmcubmFtZT1pbnN0YW5jZS9zZXR0aW5ncy8qfUKsAQoQY29tLm1lbW9zLmFwaS52MUIUSW5zdGFuY2VTZXJ2aWNlUHJvdG9QAVowZ2l0aHViLmNvbS91c2VtZW1vcy9tZW1vcy9wcm90by9nZW4vYXBpL3YxO2FwaXYxogIDTUFYqgIMTWVtb3MuQXBpLlYxygIMTWVtb3NcQXBpXFYx4gIYTWVtb3NcQXBpXFYxXEdQQk1ldGFkYXRh6gIOTWVtb3M6OkFwaTo6VjFiBnByb3RvMw\", [file_api_v1_user_service, file_google_api_annotations, file_google_api_client, file_google_api_field_behavior, file_google_api_resource, file_google_protobuf_field_mask, file_google_type_color]);\n\n/**\n * Instance profile message containing basic instance information.\n *\n * @generated from message memos.api.v1.InstanceProfile\n */\nexport type InstanceProfile = Message<\"memos.api.v1.InstanceProfile\"> & {\n  /**\n   * Version is the current version of instance.\n   *\n   * @generated from field: string version = 2;\n   */\n  version: string;\n\n  /**\n   * Demo indicates if the instance is in demo mode.\n   *\n   * @generated from field: bool demo = 3;\n   */\n  demo: boolean;\n\n  /**\n   * Instance URL is the URL of the instance.\n   *\n   * @generated from field: string instance_url = 6;\n   */\n  instanceUrl: string;\n\n  /**\n   * The first administrator who set up this instance.\n   * When null, instance requires initial setup (creating the first admin account).\n   *\n   * @generated from field: memos.api.v1.User admin = 7;\n   */\n  admin?: User;\n};\n\n/**\n * Describes the message memos.api.v1.InstanceProfile.\n * Use `create(InstanceProfileSchema)` to create a new message.\n */\nexport const InstanceProfileSchema: GenMessage<InstanceProfile> = /*@__PURE__*/\n  messageDesc(file_api_v1_instance_service, 0);\n\n/**\n * Request for instance profile.\n *\n * @generated from message memos.api.v1.GetInstanceProfileRequest\n */\nexport type GetInstanceProfileRequest = Message<\"memos.api.v1.GetInstanceProfileRequest\"> & {\n};\n\n/**\n * Describes the message memos.api.v1.GetInstanceProfileRequest.\n * Use `create(GetInstanceProfileRequestSchema)` to create a new message.\n */\nexport const GetInstanceProfileRequestSchema: GenMessage<GetInstanceProfileRequest> = /*@__PURE__*/\n  messageDesc(file_api_v1_instance_service, 1);\n\n/**\n * An instance setting resource.\n *\n * @generated from message memos.api.v1.InstanceSetting\n */\nexport type InstanceSetting = Message<\"memos.api.v1.InstanceSetting\"> & {\n  /**\n   * The name of the instance setting.\n   * Format: instance/settings/{setting}\n   *\n   * @generated from field: string name = 1;\n   */\n  name: string;\n\n  /**\n   * @generated from oneof memos.api.v1.InstanceSetting.value\n   */\n  value: {\n    /**\n     * @generated from field: memos.api.v1.InstanceSetting.GeneralSetting general_setting = 2;\n     */\n    value: InstanceSetting_GeneralSetting;\n    case: \"generalSetting\";\n  } | {\n    /**\n     * @generated from field: memos.api.v1.InstanceSetting.StorageSetting storage_setting = 3;\n     */\n    value: InstanceSetting_StorageSetting;\n    case: \"storageSetting\";\n  } | {\n    /**\n     * @generated from field: memos.api.v1.InstanceSetting.MemoRelatedSetting memo_related_setting = 4;\n     */\n    value: InstanceSetting_MemoRelatedSetting;\n    case: \"memoRelatedSetting\";\n  } | {\n    /**\n     * @generated from field: memos.api.v1.InstanceSetting.TagsSetting tags_setting = 5;\n     */\n    value: InstanceSetting_TagsSetting;\n    case: \"tagsSetting\";\n  } | {\n    /**\n     * @generated from field: memos.api.v1.InstanceSetting.NotificationSetting notification_setting = 6;\n     */\n    value: InstanceSetting_NotificationSetting;\n    case: \"notificationSetting\";\n  } | { case: undefined; value?: undefined };\n};\n\n/**\n * Describes the message memos.api.v1.InstanceSetting.\n * Use `create(InstanceSettingSchema)` to create a new message.\n */\nexport const InstanceSettingSchema: GenMessage<InstanceSetting> = /*@__PURE__*/\n  messageDesc(file_api_v1_instance_service, 2);\n\n/**\n * General instance settings configuration.\n *\n * @generated from message memos.api.v1.InstanceSetting.GeneralSetting\n */\nexport type InstanceSetting_GeneralSetting = Message<\"memos.api.v1.InstanceSetting.GeneralSetting\"> & {\n  /**\n   * disallow_user_registration disallows user registration.\n   *\n   * @generated from field: bool disallow_user_registration = 2;\n   */\n  disallowUserRegistration: boolean;\n\n  /**\n   * disallow_password_auth disallows password authentication.\n   *\n   * @generated from field: bool disallow_password_auth = 3;\n   */\n  disallowPasswordAuth: boolean;\n\n  /**\n   * additional_script is the additional script.\n   *\n   * @generated from field: string additional_script = 4;\n   */\n  additionalScript: string;\n\n  /**\n   * additional_style is the additional style.\n   *\n   * @generated from field: string additional_style = 5;\n   */\n  additionalStyle: string;\n\n  /**\n   * custom_profile is the custom profile.\n   *\n   * @generated from field: memos.api.v1.InstanceSetting.GeneralSetting.CustomProfile custom_profile = 6;\n   */\n  customProfile?: InstanceSetting_GeneralSetting_CustomProfile;\n\n  /**\n   * week_start_day_offset is the week start day offset from Sunday.\n   * 0: Sunday, 1: Monday, 2: Tuesday, 3: Wednesday, 4: Thursday, 5: Friday, 6: Saturday\n   * Default is Sunday.\n   *\n   * @generated from field: int32 week_start_day_offset = 7;\n   */\n  weekStartDayOffset: number;\n\n  /**\n   * disallow_change_username disallows changing username.\n   *\n   * @generated from field: bool disallow_change_username = 8;\n   */\n  disallowChangeUsername: boolean;\n\n  /**\n   * disallow_change_nickname disallows changing nickname.\n   *\n   * @generated from field: bool disallow_change_nickname = 9;\n   */\n  disallowChangeNickname: boolean;\n};\n\n/**\n * Describes the message memos.api.v1.InstanceSetting.GeneralSetting.\n * Use `create(InstanceSetting_GeneralSettingSchema)` to create a new message.\n */\nexport const InstanceSetting_GeneralSettingSchema: GenMessage<InstanceSetting_GeneralSetting> = /*@__PURE__*/\n  messageDesc(file_api_v1_instance_service, 2, 0);\n\n/**\n * Custom profile configuration for instance branding.\n *\n * @generated from message memos.api.v1.InstanceSetting.GeneralSetting.CustomProfile\n */\nexport type InstanceSetting_GeneralSetting_CustomProfile = Message<\"memos.api.v1.InstanceSetting.GeneralSetting.CustomProfile\"> & {\n  /**\n   * @generated from field: string title = 1;\n   */\n  title: string;\n\n  /**\n   * @generated from field: string description = 2;\n   */\n  description: string;\n\n  /**\n   * @generated from field: string logo_url = 3;\n   */\n  logoUrl: string;\n};\n\n/**\n * Describes the message memos.api.v1.InstanceSetting.GeneralSetting.CustomProfile.\n * Use `create(InstanceSetting_GeneralSetting_CustomProfileSchema)` to create a new message.\n */\nexport const InstanceSetting_GeneralSetting_CustomProfileSchema: GenMessage<InstanceSetting_GeneralSetting_CustomProfile> = /*@__PURE__*/\n  messageDesc(file_api_v1_instance_service, 2, 0, 0);\n\n/**\n * Storage configuration settings for instance attachments.\n *\n * @generated from message memos.api.v1.InstanceSetting.StorageSetting\n */\nexport type InstanceSetting_StorageSetting = Message<\"memos.api.v1.InstanceSetting.StorageSetting\"> & {\n  /**\n   * storage_type is the storage type.\n   *\n   * @generated from field: memos.api.v1.InstanceSetting.StorageSetting.StorageType storage_type = 1;\n   */\n  storageType: InstanceSetting_StorageSetting_StorageType;\n\n  /**\n   * The template of file path.\n   * e.g. assets/{timestamp}_{filename}\n   *\n   * @generated from field: string filepath_template = 2;\n   */\n  filepathTemplate: string;\n\n  /**\n   * The max upload size in megabytes.\n   *\n   * @generated from field: int64 upload_size_limit_mb = 3;\n   */\n  uploadSizeLimitMb: bigint;\n\n  /**\n   * The S3 config.\n   *\n   * @generated from field: memos.api.v1.InstanceSetting.StorageSetting.S3Config s3_config = 4;\n   */\n  s3Config?: InstanceSetting_StorageSetting_S3Config;\n};\n\n/**\n * Describes the message memos.api.v1.InstanceSetting.StorageSetting.\n * Use `create(InstanceSetting_StorageSettingSchema)` to create a new message.\n */\nexport const InstanceSetting_StorageSettingSchema: GenMessage<InstanceSetting_StorageSetting> = /*@__PURE__*/\n  messageDesc(file_api_v1_instance_service, 2, 1);\n\n/**\n * S3 configuration for cloud storage backend.\n * Reference: https://developers.cloudflare.com/r2/examples/aws/aws-sdk-go/\n *\n * @generated from message memos.api.v1.InstanceSetting.StorageSetting.S3Config\n */\nexport type InstanceSetting_StorageSetting_S3Config = Message<\"memos.api.v1.InstanceSetting.StorageSetting.S3Config\"> & {\n  /**\n   * @generated from field: string access_key_id = 1;\n   */\n  accessKeyId: string;\n\n  /**\n   * @generated from field: string access_key_secret = 2;\n   */\n  accessKeySecret: string;\n\n  /**\n   * @generated from field: string endpoint = 3;\n   */\n  endpoint: string;\n\n  /**\n   * @generated from field: string region = 4;\n   */\n  region: string;\n\n  /**\n   * @generated from field: string bucket = 5;\n   */\n  bucket: string;\n\n  /**\n   * @generated from field: bool use_path_style = 6;\n   */\n  usePathStyle: boolean;\n};\n\n/**\n * Describes the message memos.api.v1.InstanceSetting.StorageSetting.S3Config.\n * Use `create(InstanceSetting_StorageSetting_S3ConfigSchema)` to create a new message.\n */\nexport const InstanceSetting_StorageSetting_S3ConfigSchema: GenMessage<InstanceSetting_StorageSetting_S3Config> = /*@__PURE__*/\n  messageDesc(file_api_v1_instance_service, 2, 1, 0);\n\n/**\n * Storage type enumeration for different storage backends.\n *\n * @generated from enum memos.api.v1.InstanceSetting.StorageSetting.StorageType\n */\nexport enum InstanceSetting_StorageSetting_StorageType {\n  /**\n   * @generated from enum value: STORAGE_TYPE_UNSPECIFIED = 0;\n   */\n  STORAGE_TYPE_UNSPECIFIED = 0,\n\n  /**\n   * DATABASE is the database storage type.\n   *\n   * @generated from enum value: DATABASE = 1;\n   */\n  DATABASE = 1,\n\n  /**\n   * LOCAL is the local storage type.\n   *\n   * @generated from enum value: LOCAL = 2;\n   */\n  LOCAL = 2,\n\n  /**\n   * S3 is the S3 storage type.\n   *\n   * @generated from enum value: S3 = 3;\n   */\n  S3 = 3,\n}\n\n/**\n * Describes the enum memos.api.v1.InstanceSetting.StorageSetting.StorageType.\n */\nexport const InstanceSetting_StorageSetting_StorageTypeSchema: GenEnum<InstanceSetting_StorageSetting_StorageType> = /*@__PURE__*/\n  enumDesc(file_api_v1_instance_service, 2, 1, 0);\n\n/**\n * Memo-related instance settings and policies.\n *\n * @generated from message memos.api.v1.InstanceSetting.MemoRelatedSetting\n */\nexport type InstanceSetting_MemoRelatedSetting = Message<\"memos.api.v1.InstanceSetting.MemoRelatedSetting\"> & {\n  /**\n   * display_with_update_time orders and displays memo with update time.\n   *\n   * @generated from field: bool display_with_update_time = 2;\n   */\n  displayWithUpdateTime: boolean;\n\n  /**\n   * content_length_limit is the limit of content length. Unit is byte.\n   *\n   * @generated from field: int32 content_length_limit = 3;\n   */\n  contentLengthLimit: number;\n\n  /**\n   * enable_double_click_edit enables editing on double click.\n   *\n   * @generated from field: bool enable_double_click_edit = 4;\n   */\n  enableDoubleClickEdit: boolean;\n\n  /**\n   * reactions is the list of reactions.\n   *\n   * @generated from field: repeated string reactions = 7;\n   */\n  reactions: string[];\n};\n\n/**\n * Describes the message memos.api.v1.InstanceSetting.MemoRelatedSetting.\n * Use `create(InstanceSetting_MemoRelatedSettingSchema)` to create a new message.\n */\nexport const InstanceSetting_MemoRelatedSettingSchema: GenMessage<InstanceSetting_MemoRelatedSetting> = /*@__PURE__*/\n  messageDesc(file_api_v1_instance_service, 2, 2);\n\n/**\n * Metadata for a tag.\n *\n * @generated from message memos.api.v1.InstanceSetting.TagMetadata\n */\nexport type InstanceSetting_TagMetadata = Message<\"memos.api.v1.InstanceSetting.TagMetadata\"> & {\n  /**\n   * Background color for the tag label.\n   *\n   * @generated from field: google.type.Color background_color = 1;\n   */\n  backgroundColor?: Color;\n};\n\n/**\n * Describes the message memos.api.v1.InstanceSetting.TagMetadata.\n * Use `create(InstanceSetting_TagMetadataSchema)` to create a new message.\n */\nexport const InstanceSetting_TagMetadataSchema: GenMessage<InstanceSetting_TagMetadata> = /*@__PURE__*/\n  messageDesc(file_api_v1_instance_service, 2, 3);\n\n/**\n * Tag metadata configuration.\n *\n * @generated from message memos.api.v1.InstanceSetting.TagsSetting\n */\nexport type InstanceSetting_TagsSetting = Message<\"memos.api.v1.InstanceSetting.TagsSetting\"> & {\n  /**\n   * @generated from field: map<string, memos.api.v1.InstanceSetting.TagMetadata> tags = 1;\n   */\n  tags: { [key: string]: InstanceSetting_TagMetadata };\n};\n\n/**\n * Describes the message memos.api.v1.InstanceSetting.TagsSetting.\n * Use `create(InstanceSetting_TagsSettingSchema)` to create a new message.\n */\nexport const InstanceSetting_TagsSettingSchema: GenMessage<InstanceSetting_TagsSetting> = /*@__PURE__*/\n  messageDesc(file_api_v1_instance_service, 2, 4);\n\n/**\n * Notification transport configuration.\n *\n * @generated from message memos.api.v1.InstanceSetting.NotificationSetting\n */\nexport type InstanceSetting_NotificationSetting = Message<\"memos.api.v1.InstanceSetting.NotificationSetting\"> & {\n  /**\n   * @generated from field: memos.api.v1.InstanceSetting.NotificationSetting.EmailSetting email = 1;\n   */\n  email?: InstanceSetting_NotificationSetting_EmailSetting;\n};\n\n/**\n * Describes the message memos.api.v1.InstanceSetting.NotificationSetting.\n * Use `create(InstanceSetting_NotificationSettingSchema)` to create a new message.\n */\nexport const InstanceSetting_NotificationSettingSchema: GenMessage<InstanceSetting_NotificationSetting> = /*@__PURE__*/\n  messageDesc(file_api_v1_instance_service, 2, 5);\n\n/**\n * Email delivery configuration for notifications.\n *\n * @generated from message memos.api.v1.InstanceSetting.NotificationSetting.EmailSetting\n */\nexport type InstanceSetting_NotificationSetting_EmailSetting = Message<\"memos.api.v1.InstanceSetting.NotificationSetting.EmailSetting\"> & {\n  /**\n   * @generated from field: bool enabled = 1;\n   */\n  enabled: boolean;\n\n  /**\n   * @generated from field: string smtp_host = 2;\n   */\n  smtpHost: string;\n\n  /**\n   * @generated from field: int32 smtp_port = 3;\n   */\n  smtpPort: number;\n\n  /**\n   * @generated from field: string smtp_username = 4;\n   */\n  smtpUsername: string;\n\n  /**\n   * @generated from field: string smtp_password = 5;\n   */\n  smtpPassword: string;\n\n  /**\n   * @generated from field: string from_email = 6;\n   */\n  fromEmail: string;\n\n  /**\n   * @generated from field: string from_name = 7;\n   */\n  fromName: string;\n\n  /**\n   * @generated from field: string reply_to = 8;\n   */\n  replyTo: string;\n\n  /**\n   * @generated from field: bool use_tls = 9;\n   */\n  useTls: boolean;\n\n  /**\n   * @generated from field: bool use_ssl = 10;\n   */\n  useSsl: boolean;\n};\n\n/**\n * Describes the message memos.api.v1.InstanceSetting.NotificationSetting.EmailSetting.\n * Use `create(InstanceSetting_NotificationSetting_EmailSettingSchema)` to create a new message.\n */\nexport const InstanceSetting_NotificationSetting_EmailSettingSchema: GenMessage<InstanceSetting_NotificationSetting_EmailSetting> = /*@__PURE__*/\n  messageDesc(file_api_v1_instance_service, 2, 5, 0);\n\n/**\n * Enumeration of instance setting keys.\n *\n * @generated from enum memos.api.v1.InstanceSetting.Key\n */\nexport enum InstanceSetting_Key {\n  /**\n   * @generated from enum value: KEY_UNSPECIFIED = 0;\n   */\n  KEY_UNSPECIFIED = 0,\n\n  /**\n   * GENERAL is the key for general settings.\n   *\n   * @generated from enum value: GENERAL = 1;\n   */\n  GENERAL = 1,\n\n  /**\n   * STORAGE is the key for storage settings.\n   *\n   * @generated from enum value: STORAGE = 2;\n   */\n  STORAGE = 2,\n\n  /**\n   * MEMO_RELATED is the key for memo related settings.\n   *\n   * @generated from enum value: MEMO_RELATED = 3;\n   */\n  MEMO_RELATED = 3,\n\n  /**\n   * TAGS is the key for tag metadata.\n   *\n   * @generated from enum value: TAGS = 4;\n   */\n  TAGS = 4,\n\n  /**\n   * NOTIFICATION is the key for notification transport settings.\n   *\n   * @generated from enum value: NOTIFICATION = 5;\n   */\n  NOTIFICATION = 5,\n}\n\n/**\n * Describes the enum memos.api.v1.InstanceSetting.Key.\n */\nexport const InstanceSetting_KeySchema: GenEnum<InstanceSetting_Key> = /*@__PURE__*/\n  enumDesc(file_api_v1_instance_service, 2, 0);\n\n/**\n * Request message for GetInstanceSetting method.\n *\n * @generated from message memos.api.v1.GetInstanceSettingRequest\n */\nexport type GetInstanceSettingRequest = Message<\"memos.api.v1.GetInstanceSettingRequest\"> & {\n  /**\n   * The resource name of the instance setting.\n   * Format: instance/settings/{setting}\n   *\n   * @generated from field: string name = 1;\n   */\n  name: string;\n};\n\n/**\n * Describes the message memos.api.v1.GetInstanceSettingRequest.\n * Use `create(GetInstanceSettingRequestSchema)` to create a new message.\n */\nexport const GetInstanceSettingRequestSchema: GenMessage<GetInstanceSettingRequest> = /*@__PURE__*/\n  messageDesc(file_api_v1_instance_service, 3);\n\n/**\n * Request message for UpdateInstanceSetting method.\n *\n * @generated from message memos.api.v1.UpdateInstanceSettingRequest\n */\nexport type UpdateInstanceSettingRequest = Message<\"memos.api.v1.UpdateInstanceSettingRequest\"> & {\n  /**\n   * The instance setting resource which replaces the resource on the server.\n   *\n   * @generated from field: memos.api.v1.InstanceSetting setting = 1;\n   */\n  setting?: InstanceSetting;\n\n  /**\n   * The list of fields to update.\n   *\n   * @generated from field: google.protobuf.FieldMask update_mask = 2;\n   */\n  updateMask?: FieldMask;\n};\n\n/**\n * Describes the message memos.api.v1.UpdateInstanceSettingRequest.\n * Use `create(UpdateInstanceSettingRequestSchema)` to create a new message.\n */\nexport const UpdateInstanceSettingRequestSchema: GenMessage<UpdateInstanceSettingRequest> = /*@__PURE__*/\n  messageDesc(file_api_v1_instance_service, 4);\n\n/**\n * @generated from service memos.api.v1.InstanceService\n */\nexport const InstanceService: GenService<{\n  /**\n   * Gets the instance profile.\n   *\n   * @generated from rpc memos.api.v1.InstanceService.GetInstanceProfile\n   */\n  getInstanceProfile: {\n    methodKind: \"unary\";\n    input: typeof GetInstanceProfileRequestSchema;\n    output: typeof InstanceProfileSchema;\n  },\n  /**\n   * Gets an instance setting.\n   *\n   * @generated from rpc memos.api.v1.InstanceService.GetInstanceSetting\n   */\n  getInstanceSetting: {\n    methodKind: \"unary\";\n    input: typeof GetInstanceSettingRequestSchema;\n    output: typeof InstanceSettingSchema;\n  },\n  /**\n   * Updates an instance setting.\n   *\n   * @generated from rpc memos.api.v1.InstanceService.UpdateInstanceSetting\n   */\n  updateInstanceSetting: {\n    methodKind: \"unary\";\n    input: typeof UpdateInstanceSettingRequestSchema;\n    output: typeof InstanceSettingSchema;\n  },\n}> = /*@__PURE__*/\n  serviceDesc(file_api_v1_instance_service, 0);\n\n"
  },
  {
    "path": "web/src/types/proto/api/v1/memo_service_pb.ts",
    "content": "// @generated by protoc-gen-es v2.11.0 with parameter \"target=ts\"\n// @generated from file api/v1/memo_service.proto (package memos.api.v1, syntax proto3)\n/* eslint-disable */\n\nimport type { GenEnum, GenFile, GenMessage, GenService } from \"@bufbuild/protobuf/codegenv2\";\nimport { enumDesc, fileDesc, messageDesc, serviceDesc } from \"@bufbuild/protobuf/codegenv2\";\nimport type { Attachment } from \"./attachment_service_pb\";\nimport { file_api_v1_attachment_service } from \"./attachment_service_pb\";\nimport type { State } from \"./common_pb\";\nimport { file_api_v1_common } from \"./common_pb\";\nimport { file_google_api_annotations } from \"../../google/api/annotations_pb\";\nimport { file_google_api_client } from \"../../google/api/client_pb\";\nimport { file_google_api_field_behavior } from \"../../google/api/field_behavior_pb\";\nimport { file_google_api_resource } from \"../../google/api/resource_pb\";\nimport type { EmptySchema, FieldMask, Timestamp } from \"@bufbuild/protobuf/wkt\";\nimport { file_google_protobuf_empty, file_google_protobuf_field_mask, file_google_protobuf_timestamp } from \"@bufbuild/protobuf/wkt\";\nimport type { Message } from \"@bufbuild/protobuf\";\n\n/**\n * Describes the file api/v1/memo_service.proto.\n */\nexport const file_api_v1_memo_service: GenFile = /*@__PURE__*/\n  fileDesc(\"ChlhcGkvdjEvbWVtb19zZXJ2aWNlLnByb3RvEgxtZW1vcy5hcGkudjEipwIKCFJlYWN0aW9uEhQKBG5hbWUYASABKAlCBuBBA+BBCBIqCgdjcmVhdG9yGAIgASgJQhngQQP6QRMKEW1lbW9zLmFwaS52MS9Vc2VyEi0KCmNvbnRlbnRfaWQYAyABKAlCGeBBAvpBEwoRbWVtb3MuYXBpLnYxL01lbW8SGgoNcmVhY3Rpb25fdHlwZRgEIAEoCUID4EECEjQKC2NyZWF0ZV90aW1lGAUgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcEID4EEDOljqQVUKFW1lbW9zLmFwaS52MS9SZWFjdGlvbhIhbWVtb3Mve21lbW99L3JlYWN0aW9ucy97cmVhY3Rpb259GgRuYW1lKglyZWFjdGlvbnMyCHJlYWN0aW9uIo0HCgRNZW1vEhEKBG5hbWUYASABKAlCA+BBCBInCgVzdGF0ZRgCIAEoDjITLm1lbW9zLmFwaS52MS5TdGF0ZUID4EECEioKB2NyZWF0b3IYAyABKAlCGeBBA/pBEwoRbWVtb3MuYXBpLnYxL1VzZXISNAoLY3JlYXRlX3RpbWUYBCABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wQgPgQQESNAoLdXBkYXRlX3RpbWUYBSABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wQgPgQQESNQoMZGlzcGxheV90aW1lGAYgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcEID4EEBEhQKB2NvbnRlbnQYByABKAlCA+BBAhIxCgp2aXNpYmlsaXR5GAkgASgOMhgubWVtb3MuYXBpLnYxLlZpc2liaWxpdHlCA+BBAhIRCgR0YWdzGAogAygJQgPgQQMSEwoGcGlubmVkGAsgASgIQgPgQQESMgoLYXR0YWNobWVudHMYDCADKAsyGC5tZW1vcy5hcGkudjEuQXR0YWNobWVudEID4EEBEjIKCXJlbGF0aW9ucxgNIAMoCzIaLm1lbW9zLmFwaS52MS5NZW1vUmVsYXRpb25CA+BBARIuCglyZWFjdGlvbnMYDiADKAsyFi5tZW1vcy5hcGkudjEuUmVhY3Rpb25CA+BBAxIyCghwcm9wZXJ0eRgPIAEoCzIbLm1lbW9zLmFwaS52MS5NZW1vLlByb3BlcnR5QgPgQQMSLgoGcGFyZW50GBAgASgJQhngQQP6QRMKEW1lbW9zLmFwaS52MS9NZW1vSACIAQESFAoHc25pcHBldBgRIAEoCUID4EEDEjIKCGxvY2F0aW9uGBIgASgLMhYubWVtb3MuYXBpLnYxLkxvY2F0aW9uQgPgQQFIAYgBARpyCghQcm9wZXJ0eRIQCghoYXNfbGluaxgBIAEoCBIVCg1oYXNfdGFza19saXN0GAIgASgIEhAKCGhhc19jb2RlGAMgASgIEhwKFGhhc19pbmNvbXBsZXRlX3Rhc2tzGAQgASgIEg0KBXRpdGxlGAUgASgJOjfqQTQKEW1lbW9zLmFwaS52MS9NZW1vEgxtZW1vcy97bWVtb30aBG5hbWUqBW1lbW9zMgRtZW1vQgkKB19wYXJlbnRCCwoJX2xvY2F0aW9uIlMKCExvY2F0aW9uEhgKC3BsYWNlaG9sZGVyGAEgASgJQgPgQQESFQoIbGF0aXR1ZGUYAiABKAFCA+BBARIWCglsb25naXR1ZGUYAyABKAFCA+BBASJQChFDcmVhdGVNZW1vUmVxdWVzdBIlCgRtZW1vGAEgASgLMhIubWVtb3MuYXBpLnYxLk1lbW9CA+BBAhIUCgdtZW1vX2lkGAIgASgJQgPgQQEiswEKEExpc3RNZW1vc1JlcXVlc3QSFgoJcGFnZV9zaXplGAEgASgFQgPgQQESFwoKcGFnZV90b2tlbhgCIAEoCUID4EEBEicKBXN0YXRlGAMgASgOMhMubWVtb3MuYXBpLnYxLlN0YXRlQgPgQQESFQoIb3JkZXJfYnkYBCABKAlCA+BBARITCgZmaWx0ZXIYBSABKAlCA+BBARIZCgxzaG93X2RlbGV0ZWQYBiABKAhCA+BBASJPChFMaXN0TWVtb3NSZXNwb25zZRIhCgVtZW1vcxgBIAMoCzISLm1lbW9zLmFwaS52MS5NZW1vEhcKD25leHRfcGFnZV90b2tlbhgCIAEoCSI5Cg5HZXRNZW1vUmVxdWVzdBInCgRuYW1lGAEgASgJQhngQQL6QRMKEW1lbW9zLmFwaS52MS9NZW1vInAKEVVwZGF0ZU1lbW9SZXF1ZXN0EiUKBG1lbW8YASABKAsyEi5tZW1vcy5hcGkudjEuTWVtb0ID4EECEjQKC3VwZGF0ZV9tYXNrGAIgASgLMhouZ29vZ2xlLnByb3RvYnVmLkZpZWxkTWFza0ID4EECIlAKEURlbGV0ZU1lbW9SZXF1ZXN0EicKBG5hbWUYASABKAlCGeBBAvpBEwoRbWVtb3MuYXBpLnYxL01lbW8SEgoFZm9yY2UYAiABKAhCA+BBASJ4ChlTZXRNZW1vQXR0YWNobWVudHNSZXF1ZXN0EicKBG5hbWUYASABKAlCGeBBAvpBEwoRbWVtb3MuYXBpLnYxL01lbW8SMgoLYXR0YWNobWVudHMYAiADKAsyGC5tZW1vcy5hcGkudjEuQXR0YWNobWVudEID4EECInYKGkxpc3RNZW1vQXR0YWNobWVudHNSZXF1ZXN0EicKBG5hbWUYASABKAlCGeBBAvpBEwoRbWVtb3MuYXBpLnYxL01lbW8SFgoJcGFnZV9zaXplGAIgASgFQgPgQQESFwoKcGFnZV90b2tlbhgDIAEoCUID4EEBImUKG0xpc3RNZW1vQXR0YWNobWVudHNSZXNwb25zZRItCgthdHRhY2htZW50cxgBIAMoCzIYLm1lbW9zLmFwaS52MS5BdHRhY2htZW50EhcKD25leHRfcGFnZV90b2tlbhgCIAEoCSKzAgoMTWVtb1JlbGF0aW9uEjIKBG1lbW8YASABKAsyHy5tZW1vcy5hcGkudjEuTWVtb1JlbGF0aW9uLk1lbW9CA+BBAhI6CgxyZWxhdGVkX21lbW8YAiABKAsyHy5tZW1vcy5hcGkudjEuTWVtb1JlbGF0aW9uLk1lbW9CA+BBAhIyCgR0eXBlGAMgASgOMh8ubWVtb3MuYXBpLnYxLk1lbW9SZWxhdGlvbi5UeXBlQgPgQQIaRQoETWVtbxInCgRuYW1lGAEgASgJQhngQQL6QRMKEW1lbW9zLmFwaS52MS9NZW1vEhQKB3NuaXBwZXQYAiABKAlCA+BBAyI4CgRUeXBlEhQKEFRZUEVfVU5TUEVDSUZJRUQQABINCglSRUZFUkVOQ0UQARILCgdDT01NRU5UEAIidgoXU2V0TWVtb1JlbGF0aW9uc1JlcXVlc3QSJwoEbmFtZRgBIAEoCUIZ4EEC+kETChFtZW1vcy5hcGkudjEvTWVtbxIyCglyZWxhdGlvbnMYAiADKAsyGi5tZW1vcy5hcGkudjEuTWVtb1JlbGF0aW9uQgPgQQIidAoYTGlzdE1lbW9SZWxhdGlvbnNSZXF1ZXN0EicKBG5hbWUYASABKAlCGeBBAvpBEwoRbWVtb3MuYXBpLnYxL01lbW8SFgoJcGFnZV9zaXplGAIgASgFQgPgQQESFwoKcGFnZV90b2tlbhgDIAEoCUID4EEBImMKGUxpc3RNZW1vUmVsYXRpb25zUmVzcG9uc2USLQoJcmVsYXRpb25zGAEgAygLMhoubWVtb3MuYXBpLnYxLk1lbW9SZWxhdGlvbhIXCg9uZXh0X3BhZ2VfdG9rZW4YAiABKAkihgEKGENyZWF0ZU1lbW9Db21tZW50UmVxdWVzdBInCgRuYW1lGAEgASgJQhngQQL6QRMKEW1lbW9zLmFwaS52MS9NZW1vEigKB2NvbW1lbnQYAiABKAsyEi5tZW1vcy5hcGkudjEuTWVtb0ID4EECEhcKCmNvbW1lbnRfaWQYAyABKAlCA+BBASKKAQoXTGlzdE1lbW9Db21tZW50c1JlcXVlc3QSJwoEbmFtZRgBIAEoCUIZ4EEC+kETChFtZW1vcy5hcGkudjEvTWVtbxIWCglwYWdlX3NpemUYAiABKAVCA+BBARIXCgpwYWdlX3Rva2VuGAMgASgJQgPgQQESFQoIb3JkZXJfYnkYBCABKAlCA+BBASJqChhMaXN0TWVtb0NvbW1lbnRzUmVzcG9uc2USIQoFbWVtb3MYASADKAsyEi5tZW1vcy5hcGkudjEuTWVtbxIXCg9uZXh0X3BhZ2VfdG9rZW4YAiABKAkSEgoKdG90YWxfc2l6ZRgDIAEoBSJ0ChhMaXN0TWVtb1JlYWN0aW9uc1JlcXVlc3QSJwoEbmFtZRgBIAEoCUIZ4EEC+kETChFtZW1vcy5hcGkudjEvTWVtbxIWCglwYWdlX3NpemUYAiABKAVCA+BBARIXCgpwYWdlX3Rva2VuGAMgASgJQgPgQQEicwoZTGlzdE1lbW9SZWFjdGlvbnNSZXNwb25zZRIpCglyZWFjdGlvbnMYASADKAsyFi5tZW1vcy5hcGkudjEuUmVhY3Rpb24SFwoPbmV4dF9wYWdlX3Rva2VuGAIgASgJEhIKCnRvdGFsX3NpemUYAyABKAUicwoZVXBzZXJ0TWVtb1JlYWN0aW9uUmVxdWVzdBInCgRuYW1lGAEgASgJQhngQQL6QRMKEW1lbW9zLmFwaS52MS9NZW1vEi0KCHJlYWN0aW9uGAIgASgLMhYubWVtb3MuYXBpLnYxLlJlYWN0aW9uQgPgQQIiSAoZRGVsZXRlTWVtb1JlYWN0aW9uUmVxdWVzdBIrCgRuYW1lGAEgASgJQh3gQQL6QRcKFW1lbW9zLmFwaS52MS9SZWFjdGlvbiLoAQoJTWVtb1NoYXJlEhEKBG5hbWUYASABKAlCA+BBCBI0CgtjcmVhdGVfdGltZRgCIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXBCA+BBAxI5CgtleHBpcmVfdGltZRgDIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXBCA+BBAUgAiAEBOkfqQUQKFm1lbW9zLmFwaS52MS9NZW1vU2hhcmUSG21lbW9zL3ttZW1vfS9zaGFyZXMve3NoYXJlfSoGc2hhcmVzMgVzaGFyZUIOCgxfZXhwaXJlX3RpbWUidQoWQ3JlYXRlTWVtb1NoYXJlUmVxdWVzdBIpCgZwYXJlbnQYASABKAlCGeBBAvpBEwoRbWVtb3MuYXBpLnYxL01lbW8SMAoKbWVtb19zaGFyZRgCIAEoCzIXLm1lbW9zLmFwaS52MS5NZW1vU2hhcmVCA+BBAiJCChVMaXN0TWVtb1NoYXJlc1JlcXVlc3QSKQoGcGFyZW50GAEgASgJQhngQQL6QRMKEW1lbW9zLmFwaS52MS9NZW1vIkYKFkxpc3RNZW1vU2hhcmVzUmVzcG9uc2USLAoLbWVtb19zaGFyZXMYASADKAsyFy5tZW1vcy5hcGkudjEuTWVtb1NoYXJlIkYKFkRlbGV0ZU1lbW9TaGFyZVJlcXVlc3QSLAoEbmFtZRgBIAEoCUIe4EEC+kEYChZtZW1vcy5hcGkudjEvTWVtb1NoYXJlIi4KFUdldE1lbW9CeVNoYXJlUmVxdWVzdBIVCghzaGFyZV9pZBgBIAEoCUID4EECKlAKClZpc2liaWxpdHkSGgoWVklTSUJJTElUWV9VTlNQRUNJRklFRBAAEgsKB1BSSVZBVEUQARINCglQUk9URUNURUQQAhIKCgZQVUJMSUMQAzLuEgoLTWVtb1NlcnZpY2USZQoKQ3JlYXRlTWVtbxIfLm1lbW9zLmFwaS52MS5DcmVhdGVNZW1vUmVxdWVzdBoSLm1lbW9zLmFwaS52MS5NZW1vIiLaQQRtZW1vgtPkkwIVOgRtZW1vIg0vYXBpL3YxL21lbW9zEmYKCUxpc3RNZW1vcxIeLm1lbW9zLmFwaS52MS5MaXN0TWVtb3NSZXF1ZXN0Gh8ubWVtb3MuYXBpLnYxLkxpc3RNZW1vc1Jlc3BvbnNlIhjaQQCC0+STAg8SDS9hcGkvdjEvbWVtb3MSYgoHR2V0TWVtbxIcLm1lbW9zLmFwaS52MS5HZXRNZW1vUmVxdWVzdBoSLm1lbW9zLmFwaS52MS5NZW1vIiXaQQRuYW1lgtPkkwIYEhYvYXBpL3YxL3tuYW1lPW1lbW9zLyp9En8KClVwZGF0ZU1lbW8SHy5tZW1vcy5hcGkudjEuVXBkYXRlTWVtb1JlcXVlc3QaEi5tZW1vcy5hcGkudjEuTWVtbyI82kEQbWVtbyx1cGRhdGVfbWFza4LT5JMCIzoEbWVtbzIbL2FwaS92MS97bWVtby5uYW1lPW1lbW9zLyp9EmwKCkRlbGV0ZU1lbW8SHy5tZW1vcy5hcGkudjEuRGVsZXRlTWVtb1JlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkiJdpBBG5hbWWC0+STAhgqFi9hcGkvdjEve25hbWU9bWVtb3MvKn0SiwEKElNldE1lbW9BdHRhY2htZW50cxInLm1lbW9zLmFwaS52MS5TZXRNZW1vQXR0YWNobWVudHNSZXF1ZXN0GhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5IjTaQQRuYW1lgtPkkwInOgEqMiIvYXBpL3YxL3tuYW1lPW1lbW9zLyp9L2F0dGFjaG1lbnRzEp0BChNMaXN0TWVtb0F0dGFjaG1lbnRzEigubWVtb3MuYXBpLnYxLkxpc3RNZW1vQXR0YWNobWVudHNSZXF1ZXN0GikubWVtb3MuYXBpLnYxLkxpc3RNZW1vQXR0YWNobWVudHNSZXNwb25zZSIx2kEEbmFtZYLT5JMCJBIiL2FwaS92MS97bmFtZT1tZW1vcy8qfS9hdHRhY2htZW50cxKFAQoQU2V0TWVtb1JlbGF0aW9ucxIlLm1lbW9zLmFwaS52MS5TZXRNZW1vUmVsYXRpb25zUmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSIy2kEEbmFtZYLT5JMCJToBKjIgL2FwaS92MS97bmFtZT1tZW1vcy8qfS9yZWxhdGlvbnMSlQEKEUxpc3RNZW1vUmVsYXRpb25zEiYubWVtb3MuYXBpLnYxLkxpc3RNZW1vUmVsYXRpb25zUmVxdWVzdBonLm1lbW9zLmFwaS52MS5MaXN0TWVtb1JlbGF0aW9uc1Jlc3BvbnNlIi/aQQRuYW1lgtPkkwIiEiAvYXBpL3YxL3tuYW1lPW1lbW9zLyp9L3JlbGF0aW9ucxKQAQoRQ3JlYXRlTWVtb0NvbW1lbnQSJi5tZW1vcy5hcGkudjEuQ3JlYXRlTWVtb0NvbW1lbnRSZXF1ZXN0GhIubWVtb3MuYXBpLnYxLk1lbW8iP9pBDG5hbWUsY29tbWVudILT5JMCKjoHY29tbWVudCIfL2FwaS92MS97bmFtZT1tZW1vcy8qfS9jb21tZW50cxKRAQoQTGlzdE1lbW9Db21tZW50cxIlLm1lbW9zLmFwaS52MS5MaXN0TWVtb0NvbW1lbnRzUmVxdWVzdBomLm1lbW9zLmFwaS52MS5MaXN0TWVtb0NvbW1lbnRzUmVzcG9uc2UiLtpBBG5hbWWC0+STAiESHy9hcGkvdjEve25hbWU9bWVtb3MvKn0vY29tbWVudHMSlQEKEUxpc3RNZW1vUmVhY3Rpb25zEiYubWVtb3MuYXBpLnYxLkxpc3RNZW1vUmVhY3Rpb25zUmVxdWVzdBonLm1lbW9zLmFwaS52MS5MaXN0TWVtb1JlYWN0aW9uc1Jlc3BvbnNlIi/aQQRuYW1lgtPkkwIiEiAvYXBpL3YxL3tuYW1lPW1lbW9zLyp9L3JlYWN0aW9ucxKJAQoSVXBzZXJ0TWVtb1JlYWN0aW9uEicubWVtb3MuYXBpLnYxLlVwc2VydE1lbW9SZWFjdGlvblJlcXVlc3QaFi5tZW1vcy5hcGkudjEuUmVhY3Rpb24iMtpBBG5hbWWC0+STAiU6ASoiIC9hcGkvdjEve25hbWU9bWVtb3MvKn0vcmVhY3Rpb25zEogBChJEZWxldGVNZW1vUmVhY3Rpb24SJy5tZW1vcy5hcGkudjEuRGVsZXRlTWVtb1JlYWN0aW9uUmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSIx2kEEbmFtZYLT5JMCJCoiL2FwaS92MS97bmFtZT1tZW1vcy8qL3JlYWN0aW9ucy8qfRKZAQoPQ3JlYXRlTWVtb1NoYXJlEiQubWVtb3MuYXBpLnYxLkNyZWF0ZU1lbW9TaGFyZVJlcXVlc3QaFy5tZW1vcy5hcGkudjEuTWVtb1NoYXJlIkfaQRFwYXJlbnQsbWVtb19zaGFyZYLT5JMCLToKbWVtb19zaGFyZSIfL2FwaS92MS97cGFyZW50PW1lbW9zLyp9L3NoYXJlcxKNAQoOTGlzdE1lbW9TaGFyZXMSIy5tZW1vcy5hcGkudjEuTGlzdE1lbW9TaGFyZXNSZXF1ZXN0GiQubWVtb3MuYXBpLnYxLkxpc3RNZW1vU2hhcmVzUmVzcG9uc2UiMNpBBnBhcmVudILT5JMCIRIfL2FwaS92MS97cGFyZW50PW1lbW9zLyp9L3NoYXJlcxJ/Cg9EZWxldGVNZW1vU2hhcmUSJC5tZW1vcy5hcGkudjEuRGVsZXRlTWVtb1NoYXJlUmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSIu2kEEbmFtZYLT5JMCISofL2FwaS92MS97bmFtZT1tZW1vcy8qL3NoYXJlcy8qfRJsCg5HZXRNZW1vQnlTaGFyZRIjLm1lbW9zLmFwaS52MS5HZXRNZW1vQnlTaGFyZVJlcXVlc3QaEi5tZW1vcy5hcGkudjEuTWVtbyIhgtPkkwIbEhkvYXBpL3YxL3NoYXJlcy97c2hhcmVfaWR9QqgBChBjb20ubWVtb3MuYXBpLnYxQhBNZW1vU2VydmljZVByb3RvUAFaMGdpdGh1Yi5jb20vdXNlbWVtb3MvbWVtb3MvcHJvdG8vZ2VuL2FwaS92MTthcGl2MaICA01BWKoCDE1lbW9zLkFwaS5WMcoCDE1lbW9zXEFwaVxWMeICGE1lbW9zXEFwaVxWMVxHUEJNZXRhZGF0YeoCDk1lbW9zOjpBcGk6OlYxYgZwcm90bzM\", [file_api_v1_attachment_service, file_api_v1_common, file_google_api_annotations, file_google_api_client, file_google_api_field_behavior, file_google_api_resource, file_google_protobuf_empty, file_google_protobuf_field_mask, file_google_protobuf_timestamp]);\n\n/**\n * @generated from message memos.api.v1.Reaction\n */\nexport type Reaction = Message<\"memos.api.v1.Reaction\"> & {\n  /**\n   * The resource name of the reaction.\n   * Format: memos/{memo}/reactions/{reaction}\n   *\n   * @generated from field: string name = 1;\n   */\n  name: string;\n\n  /**\n   * The resource name of the creator.\n   * Format: users/{user}\n   *\n   * @generated from field: string creator = 2;\n   */\n  creator: string;\n\n  /**\n   * The resource name of the content.\n   * For memo reactions, this should be the memo's resource name.\n   * Format: memos/{memo}\n   *\n   * @generated from field: string content_id = 3;\n   */\n  contentId: string;\n\n  /**\n   * Required. The type of reaction (e.g., \"👍\", \"❤️\", \"😄\").\n   *\n   * @generated from field: string reaction_type = 4;\n   */\n  reactionType: string;\n\n  /**\n   * Output only. The creation timestamp.\n   *\n   * @generated from field: google.protobuf.Timestamp create_time = 5;\n   */\n  createTime?: Timestamp;\n};\n\n/**\n * Describes the message memos.api.v1.Reaction.\n * Use `create(ReactionSchema)` to create a new message.\n */\nexport const ReactionSchema: GenMessage<Reaction> = /*@__PURE__*/\n  messageDesc(file_api_v1_memo_service, 0);\n\n/**\n * @generated from message memos.api.v1.Memo\n */\nexport type Memo = Message<\"memos.api.v1.Memo\"> & {\n  /**\n   * The resource name of the memo.\n   * Format: memos/{memo}, memo is the user defined id or uuid.\n   *\n   * @generated from field: string name = 1;\n   */\n  name: string;\n\n  /**\n   * The state of the memo.\n   *\n   * @generated from field: memos.api.v1.State state = 2;\n   */\n  state: State;\n\n  /**\n   * The name of the creator.\n   * Format: users/{user}\n   *\n   * @generated from field: string creator = 3;\n   */\n  creator: string;\n\n  /**\n   * The creation timestamp.\n   * If not set on creation, the server will set it to the current time.\n   *\n   * @generated from field: google.protobuf.Timestamp create_time = 4;\n   */\n  createTime?: Timestamp;\n\n  /**\n   * The last update timestamp.\n   * If not set on creation, the server will set it to the current time.\n   *\n   * @generated from field: google.protobuf.Timestamp update_time = 5;\n   */\n  updateTime?: Timestamp;\n\n  /**\n   * The display timestamp of the memo.\n   *\n   * @generated from field: google.protobuf.Timestamp display_time = 6;\n   */\n  displayTime?: Timestamp;\n\n  /**\n   * Required. The content of the memo in Markdown format.\n   *\n   * @generated from field: string content = 7;\n   */\n  content: string;\n\n  /**\n   * The visibility of the memo.\n   *\n   * @generated from field: memos.api.v1.Visibility visibility = 9;\n   */\n  visibility: Visibility;\n\n  /**\n   * Output only. The tags extracted from the content.\n   *\n   * @generated from field: repeated string tags = 10;\n   */\n  tags: string[];\n\n  /**\n   * Whether the memo is pinned.\n   *\n   * @generated from field: bool pinned = 11;\n   */\n  pinned: boolean;\n\n  /**\n   * Optional. The attachments of the memo.\n   *\n   * @generated from field: repeated memos.api.v1.Attachment attachments = 12;\n   */\n  attachments: Attachment[];\n\n  /**\n   * Optional. The relations of the memo.\n   *\n   * @generated from field: repeated memos.api.v1.MemoRelation relations = 13;\n   */\n  relations: MemoRelation[];\n\n  /**\n   * Output only. The reactions to the memo.\n   *\n   * @generated from field: repeated memos.api.v1.Reaction reactions = 14;\n   */\n  reactions: Reaction[];\n\n  /**\n   * Output only. The computed properties of the memo.\n   *\n   * @generated from field: memos.api.v1.Memo.Property property = 15;\n   */\n  property?: Memo_Property;\n\n  /**\n   * Output only. The name of the parent memo.\n   * Format: memos/{memo}\n   *\n   * @generated from field: optional string parent = 16;\n   */\n  parent?: string;\n\n  /**\n   * Output only. The snippet of the memo content. Plain text only.\n   *\n   * @generated from field: string snippet = 17;\n   */\n  snippet: string;\n\n  /**\n   * Optional. The location of the memo.\n   *\n   * @generated from field: optional memos.api.v1.Location location = 18;\n   */\n  location?: Location;\n};\n\n/**\n * Describes the message memos.api.v1.Memo.\n * Use `create(MemoSchema)` to create a new message.\n */\nexport const MemoSchema: GenMessage<Memo> = /*@__PURE__*/\n  messageDesc(file_api_v1_memo_service, 1);\n\n/**\n * Computed properties of a memo.\n *\n * @generated from message memos.api.v1.Memo.Property\n */\nexport type Memo_Property = Message<\"memos.api.v1.Memo.Property\"> & {\n  /**\n   * @generated from field: bool has_link = 1;\n   */\n  hasLink: boolean;\n\n  /**\n   * @generated from field: bool has_task_list = 2;\n   */\n  hasTaskList: boolean;\n\n  /**\n   * @generated from field: bool has_code = 3;\n   */\n  hasCode: boolean;\n\n  /**\n   * @generated from field: bool has_incomplete_tasks = 4;\n   */\n  hasIncompleteTasks: boolean;\n\n  /**\n   * The title extracted from the first H1 heading, if present.\n   *\n   * @generated from field: string title = 5;\n   */\n  title: string;\n};\n\n/**\n * Describes the message memos.api.v1.Memo.Property.\n * Use `create(Memo_PropertySchema)` to create a new message.\n */\nexport const Memo_PropertySchema: GenMessage<Memo_Property> = /*@__PURE__*/\n  messageDesc(file_api_v1_memo_service, 1, 0);\n\n/**\n * @generated from message memos.api.v1.Location\n */\nexport type Location = Message<\"memos.api.v1.Location\"> & {\n  /**\n   * A placeholder text for the location.\n   *\n   * @generated from field: string placeholder = 1;\n   */\n  placeholder: string;\n\n  /**\n   * The latitude of the location.\n   *\n   * @generated from field: double latitude = 2;\n   */\n  latitude: number;\n\n  /**\n   * The longitude of the location.\n   *\n   * @generated from field: double longitude = 3;\n   */\n  longitude: number;\n};\n\n/**\n * Describes the message memos.api.v1.Location.\n * Use `create(LocationSchema)` to create a new message.\n */\nexport const LocationSchema: GenMessage<Location> = /*@__PURE__*/\n  messageDesc(file_api_v1_memo_service, 2);\n\n/**\n * @generated from message memos.api.v1.CreateMemoRequest\n */\nexport type CreateMemoRequest = Message<\"memos.api.v1.CreateMemoRequest\"> & {\n  /**\n   * Required. The memo to create.\n   *\n   * @generated from field: memos.api.v1.Memo memo = 1;\n   */\n  memo?: Memo;\n\n  /**\n   * Optional. The memo ID to use for this memo.\n   * If empty, a unique ID will be generated.\n   *\n   * @generated from field: string memo_id = 2;\n   */\n  memoId: string;\n};\n\n/**\n * Describes the message memos.api.v1.CreateMemoRequest.\n * Use `create(CreateMemoRequestSchema)` to create a new message.\n */\nexport const CreateMemoRequestSchema: GenMessage<CreateMemoRequest> = /*@__PURE__*/\n  messageDesc(file_api_v1_memo_service, 3);\n\n/**\n * @generated from message memos.api.v1.ListMemosRequest\n */\nexport type ListMemosRequest = Message<\"memos.api.v1.ListMemosRequest\"> & {\n  /**\n   * Optional. The maximum number of memos to return.\n   * The service may return fewer than this value.\n   * If unspecified, at most 50 memos will be returned.\n   * The maximum value is 1000; values above 1000 will be coerced to 1000.\n   *\n   * @generated from field: int32 page_size = 1;\n   */\n  pageSize: number;\n\n  /**\n   * Optional. A page token, received from a previous `ListMemos` call.\n   * Provide this to retrieve the subsequent page.\n   *\n   * @generated from field: string page_token = 2;\n   */\n  pageToken: string;\n\n  /**\n   * Optional. The state of the memos to list.\n   * Default to `NORMAL`. Set to `ARCHIVED` to list archived memos.\n   *\n   * @generated from field: memos.api.v1.State state = 3;\n   */\n  state: State;\n\n  /**\n   * Optional. The order to sort results by.\n   * Default to \"display_time desc\".\n   * Supports comma-separated list of fields following AIP-132.\n   * Example: \"pinned desc, display_time desc\" or \"create_time asc\"\n   * Supported fields: pinned, display_time, create_time, update_time, name\n   *\n   * @generated from field: string order_by = 4;\n   */\n  orderBy: string;\n\n  /**\n   * Optional. Filter to apply to the list results.\n   * Filter is a CEL expression to filter memos.\n   * Refer to `Shortcut.filter`.\n   *\n   * @generated from field: string filter = 5;\n   */\n  filter: string;\n\n  /**\n   * Optional. If true, show deleted memos in the response.\n   *\n   * @generated from field: bool show_deleted = 6;\n   */\n  showDeleted: boolean;\n};\n\n/**\n * Describes the message memos.api.v1.ListMemosRequest.\n * Use `create(ListMemosRequestSchema)` to create a new message.\n */\nexport const ListMemosRequestSchema: GenMessage<ListMemosRequest> = /*@__PURE__*/\n  messageDesc(file_api_v1_memo_service, 4);\n\n/**\n * @generated from message memos.api.v1.ListMemosResponse\n */\nexport type ListMemosResponse = Message<\"memos.api.v1.ListMemosResponse\"> & {\n  /**\n   * The list of memos.\n   *\n   * @generated from field: repeated memos.api.v1.Memo memos = 1;\n   */\n  memos: Memo[];\n\n  /**\n   * A token that can be sent as `page_token` to retrieve the next page.\n   * If this field is omitted, there are no subsequent pages.\n   *\n   * @generated from field: string next_page_token = 2;\n   */\n  nextPageToken: string;\n};\n\n/**\n * Describes the message memos.api.v1.ListMemosResponse.\n * Use `create(ListMemosResponseSchema)` to create a new message.\n */\nexport const ListMemosResponseSchema: GenMessage<ListMemosResponse> = /*@__PURE__*/\n  messageDesc(file_api_v1_memo_service, 5);\n\n/**\n * @generated from message memos.api.v1.GetMemoRequest\n */\nexport type GetMemoRequest = Message<\"memos.api.v1.GetMemoRequest\"> & {\n  /**\n   * Required. The resource name of the memo.\n   * Format: memos/{memo}\n   *\n   * @generated from field: string name = 1;\n   */\n  name: string;\n};\n\n/**\n * Describes the message memos.api.v1.GetMemoRequest.\n * Use `create(GetMemoRequestSchema)` to create a new message.\n */\nexport const GetMemoRequestSchema: GenMessage<GetMemoRequest> = /*@__PURE__*/\n  messageDesc(file_api_v1_memo_service, 6);\n\n/**\n * @generated from message memos.api.v1.UpdateMemoRequest\n */\nexport type UpdateMemoRequest = Message<\"memos.api.v1.UpdateMemoRequest\"> & {\n  /**\n   * Required. The memo to update.\n   * The `name` field is required.\n   *\n   * @generated from field: memos.api.v1.Memo memo = 1;\n   */\n  memo?: Memo;\n\n  /**\n   * Required. The list of fields to update.\n   *\n   * @generated from field: google.protobuf.FieldMask update_mask = 2;\n   */\n  updateMask?: FieldMask;\n};\n\n/**\n * Describes the message memos.api.v1.UpdateMemoRequest.\n * Use `create(UpdateMemoRequestSchema)` to create a new message.\n */\nexport const UpdateMemoRequestSchema: GenMessage<UpdateMemoRequest> = /*@__PURE__*/\n  messageDesc(file_api_v1_memo_service, 7);\n\n/**\n * @generated from message memos.api.v1.DeleteMemoRequest\n */\nexport type DeleteMemoRequest = Message<\"memos.api.v1.DeleteMemoRequest\"> & {\n  /**\n   * Required. The resource name of the memo to delete.\n   * Format: memos/{memo}\n   *\n   * @generated from field: string name = 1;\n   */\n  name: string;\n\n  /**\n   * Optional. If set to true, the memo will be deleted even if it has associated data.\n   *\n   * @generated from field: bool force = 2;\n   */\n  force: boolean;\n};\n\n/**\n * Describes the message memos.api.v1.DeleteMemoRequest.\n * Use `create(DeleteMemoRequestSchema)` to create a new message.\n */\nexport const DeleteMemoRequestSchema: GenMessage<DeleteMemoRequest> = /*@__PURE__*/\n  messageDesc(file_api_v1_memo_service, 8);\n\n/**\n * @generated from message memos.api.v1.SetMemoAttachmentsRequest\n */\nexport type SetMemoAttachmentsRequest = Message<\"memos.api.v1.SetMemoAttachmentsRequest\"> & {\n  /**\n   * Required. The resource name of the memo.\n   * Format: memos/{memo}\n   *\n   * @generated from field: string name = 1;\n   */\n  name: string;\n\n  /**\n   * Required. The attachments to set for the memo.\n   *\n   * @generated from field: repeated memos.api.v1.Attachment attachments = 2;\n   */\n  attachments: Attachment[];\n};\n\n/**\n * Describes the message memos.api.v1.SetMemoAttachmentsRequest.\n * Use `create(SetMemoAttachmentsRequestSchema)` to create a new message.\n */\nexport const SetMemoAttachmentsRequestSchema: GenMessage<SetMemoAttachmentsRequest> = /*@__PURE__*/\n  messageDesc(file_api_v1_memo_service, 9);\n\n/**\n * @generated from message memos.api.v1.ListMemoAttachmentsRequest\n */\nexport type ListMemoAttachmentsRequest = Message<\"memos.api.v1.ListMemoAttachmentsRequest\"> & {\n  /**\n   * Required. The resource name of the memo.\n   * Format: memos/{memo}\n   *\n   * @generated from field: string name = 1;\n   */\n  name: string;\n\n  /**\n   * Optional. The maximum number of attachments to return.\n   *\n   * @generated from field: int32 page_size = 2;\n   */\n  pageSize: number;\n\n  /**\n   * Optional. A page token for pagination.\n   *\n   * @generated from field: string page_token = 3;\n   */\n  pageToken: string;\n};\n\n/**\n * Describes the message memos.api.v1.ListMemoAttachmentsRequest.\n * Use `create(ListMemoAttachmentsRequestSchema)` to create a new message.\n */\nexport const ListMemoAttachmentsRequestSchema: GenMessage<ListMemoAttachmentsRequest> = /*@__PURE__*/\n  messageDesc(file_api_v1_memo_service, 10);\n\n/**\n * @generated from message memos.api.v1.ListMemoAttachmentsResponse\n */\nexport type ListMemoAttachmentsResponse = Message<\"memos.api.v1.ListMemoAttachmentsResponse\"> & {\n  /**\n   * The list of attachments.\n   *\n   * @generated from field: repeated memos.api.v1.Attachment attachments = 1;\n   */\n  attachments: Attachment[];\n\n  /**\n   * A token for the next page of results.\n   *\n   * @generated from field: string next_page_token = 2;\n   */\n  nextPageToken: string;\n};\n\n/**\n * Describes the message memos.api.v1.ListMemoAttachmentsResponse.\n * Use `create(ListMemoAttachmentsResponseSchema)` to create a new message.\n */\nexport const ListMemoAttachmentsResponseSchema: GenMessage<ListMemoAttachmentsResponse> = /*@__PURE__*/\n  messageDesc(file_api_v1_memo_service, 11);\n\n/**\n * @generated from message memos.api.v1.MemoRelation\n */\nexport type MemoRelation = Message<\"memos.api.v1.MemoRelation\"> & {\n  /**\n   * The memo in the relation.\n   *\n   * @generated from field: memos.api.v1.MemoRelation.Memo memo = 1;\n   */\n  memo?: MemoRelation_Memo;\n\n  /**\n   * The related memo.\n   *\n   * @generated from field: memos.api.v1.MemoRelation.Memo related_memo = 2;\n   */\n  relatedMemo?: MemoRelation_Memo;\n\n  /**\n   * @generated from field: memos.api.v1.MemoRelation.Type type = 3;\n   */\n  type: MemoRelation_Type;\n};\n\n/**\n * Describes the message memos.api.v1.MemoRelation.\n * Use `create(MemoRelationSchema)` to create a new message.\n */\nexport const MemoRelationSchema: GenMessage<MemoRelation> = /*@__PURE__*/\n  messageDesc(file_api_v1_memo_service, 12);\n\n/**\n * Memo reference in relations.\n *\n * @generated from message memos.api.v1.MemoRelation.Memo\n */\nexport type MemoRelation_Memo = Message<\"memos.api.v1.MemoRelation.Memo\"> & {\n  /**\n   * The resource name of the memo.\n   * Format: memos/{memo}\n   *\n   * @generated from field: string name = 1;\n   */\n  name: string;\n\n  /**\n   * Output only. The snippet of the memo content. Plain text only.\n   *\n   * @generated from field: string snippet = 2;\n   */\n  snippet: string;\n};\n\n/**\n * Describes the message memos.api.v1.MemoRelation.Memo.\n * Use `create(MemoRelation_MemoSchema)` to create a new message.\n */\nexport const MemoRelation_MemoSchema: GenMessage<MemoRelation_Memo> = /*@__PURE__*/\n  messageDesc(file_api_v1_memo_service, 12, 0);\n\n/**\n * The type of the relation.\n *\n * @generated from enum memos.api.v1.MemoRelation.Type\n */\nexport enum MemoRelation_Type {\n  /**\n   * @generated from enum value: TYPE_UNSPECIFIED = 0;\n   */\n  TYPE_UNSPECIFIED = 0,\n\n  /**\n   * @generated from enum value: REFERENCE = 1;\n   */\n  REFERENCE = 1,\n\n  /**\n   * @generated from enum value: COMMENT = 2;\n   */\n  COMMENT = 2,\n}\n\n/**\n * Describes the enum memos.api.v1.MemoRelation.Type.\n */\nexport const MemoRelation_TypeSchema: GenEnum<MemoRelation_Type> = /*@__PURE__*/\n  enumDesc(file_api_v1_memo_service, 12, 0);\n\n/**\n * @generated from message memos.api.v1.SetMemoRelationsRequest\n */\nexport type SetMemoRelationsRequest = Message<\"memos.api.v1.SetMemoRelationsRequest\"> & {\n  /**\n   * Required. The resource name of the memo.\n   * Format: memos/{memo}\n   *\n   * @generated from field: string name = 1;\n   */\n  name: string;\n\n  /**\n   * Required. The relations to set for the memo.\n   *\n   * @generated from field: repeated memos.api.v1.MemoRelation relations = 2;\n   */\n  relations: MemoRelation[];\n};\n\n/**\n * Describes the message memos.api.v1.SetMemoRelationsRequest.\n * Use `create(SetMemoRelationsRequestSchema)` to create a new message.\n */\nexport const SetMemoRelationsRequestSchema: GenMessage<SetMemoRelationsRequest> = /*@__PURE__*/\n  messageDesc(file_api_v1_memo_service, 13);\n\n/**\n * @generated from message memos.api.v1.ListMemoRelationsRequest\n */\nexport type ListMemoRelationsRequest = Message<\"memos.api.v1.ListMemoRelationsRequest\"> & {\n  /**\n   * Required. The resource name of the memo.\n   * Format: memos/{memo}\n   *\n   * @generated from field: string name = 1;\n   */\n  name: string;\n\n  /**\n   * Optional. The maximum number of relations to return.\n   *\n   * @generated from field: int32 page_size = 2;\n   */\n  pageSize: number;\n\n  /**\n   * Optional. A page token for pagination.\n   *\n   * @generated from field: string page_token = 3;\n   */\n  pageToken: string;\n};\n\n/**\n * Describes the message memos.api.v1.ListMemoRelationsRequest.\n * Use `create(ListMemoRelationsRequestSchema)` to create a new message.\n */\nexport const ListMemoRelationsRequestSchema: GenMessage<ListMemoRelationsRequest> = /*@__PURE__*/\n  messageDesc(file_api_v1_memo_service, 14);\n\n/**\n * @generated from message memos.api.v1.ListMemoRelationsResponse\n */\nexport type ListMemoRelationsResponse = Message<\"memos.api.v1.ListMemoRelationsResponse\"> & {\n  /**\n   * The list of relations.\n   *\n   * @generated from field: repeated memos.api.v1.MemoRelation relations = 1;\n   */\n  relations: MemoRelation[];\n\n  /**\n   * A token for the next page of results.\n   *\n   * @generated from field: string next_page_token = 2;\n   */\n  nextPageToken: string;\n};\n\n/**\n * Describes the message memos.api.v1.ListMemoRelationsResponse.\n * Use `create(ListMemoRelationsResponseSchema)` to create a new message.\n */\nexport const ListMemoRelationsResponseSchema: GenMessage<ListMemoRelationsResponse> = /*@__PURE__*/\n  messageDesc(file_api_v1_memo_service, 15);\n\n/**\n * @generated from message memos.api.v1.CreateMemoCommentRequest\n */\nexport type CreateMemoCommentRequest = Message<\"memos.api.v1.CreateMemoCommentRequest\"> & {\n  /**\n   * Required. The resource name of the memo.\n   * Format: memos/{memo}\n   *\n   * @generated from field: string name = 1;\n   */\n  name: string;\n\n  /**\n   * Required. The comment to create.\n   *\n   * @generated from field: memos.api.v1.Memo comment = 2;\n   */\n  comment?: Memo;\n\n  /**\n   * Optional. The comment ID to use.\n   *\n   * @generated from field: string comment_id = 3;\n   */\n  commentId: string;\n};\n\n/**\n * Describes the message memos.api.v1.CreateMemoCommentRequest.\n * Use `create(CreateMemoCommentRequestSchema)` to create a new message.\n */\nexport const CreateMemoCommentRequestSchema: GenMessage<CreateMemoCommentRequest> = /*@__PURE__*/\n  messageDesc(file_api_v1_memo_service, 16);\n\n/**\n * @generated from message memos.api.v1.ListMemoCommentsRequest\n */\nexport type ListMemoCommentsRequest = Message<\"memos.api.v1.ListMemoCommentsRequest\"> & {\n  /**\n   * Required. The resource name of the memo.\n   * Format: memos/{memo}\n   *\n   * @generated from field: string name = 1;\n   */\n  name: string;\n\n  /**\n   * Optional. The maximum number of comments to return.\n   *\n   * @generated from field: int32 page_size = 2;\n   */\n  pageSize: number;\n\n  /**\n   * Optional. A page token for pagination.\n   *\n   * @generated from field: string page_token = 3;\n   */\n  pageToken: string;\n\n  /**\n   * Optional. The order to sort results by.\n   *\n   * @generated from field: string order_by = 4;\n   */\n  orderBy: string;\n};\n\n/**\n * Describes the message memos.api.v1.ListMemoCommentsRequest.\n * Use `create(ListMemoCommentsRequestSchema)` to create a new message.\n */\nexport const ListMemoCommentsRequestSchema: GenMessage<ListMemoCommentsRequest> = /*@__PURE__*/\n  messageDesc(file_api_v1_memo_service, 17);\n\n/**\n * @generated from message memos.api.v1.ListMemoCommentsResponse\n */\nexport type ListMemoCommentsResponse = Message<\"memos.api.v1.ListMemoCommentsResponse\"> & {\n  /**\n   * The list of comment memos.\n   *\n   * @generated from field: repeated memos.api.v1.Memo memos = 1;\n   */\n  memos: Memo[];\n\n  /**\n   * A token for the next page of results.\n   *\n   * @generated from field: string next_page_token = 2;\n   */\n  nextPageToken: string;\n\n  /**\n   * The total count of comments.\n   *\n   * @generated from field: int32 total_size = 3;\n   */\n  totalSize: number;\n};\n\n/**\n * Describes the message memos.api.v1.ListMemoCommentsResponse.\n * Use `create(ListMemoCommentsResponseSchema)` to create a new message.\n */\nexport const ListMemoCommentsResponseSchema: GenMessage<ListMemoCommentsResponse> = /*@__PURE__*/\n  messageDesc(file_api_v1_memo_service, 18);\n\n/**\n * @generated from message memos.api.v1.ListMemoReactionsRequest\n */\nexport type ListMemoReactionsRequest = Message<\"memos.api.v1.ListMemoReactionsRequest\"> & {\n  /**\n   * Required. The resource name of the memo.\n   * Format: memos/{memo}\n   *\n   * @generated from field: string name = 1;\n   */\n  name: string;\n\n  /**\n   * Optional. The maximum number of reactions to return.\n   *\n   * @generated from field: int32 page_size = 2;\n   */\n  pageSize: number;\n\n  /**\n   * Optional. A page token for pagination.\n   *\n   * @generated from field: string page_token = 3;\n   */\n  pageToken: string;\n};\n\n/**\n * Describes the message memos.api.v1.ListMemoReactionsRequest.\n * Use `create(ListMemoReactionsRequestSchema)` to create a new message.\n */\nexport const ListMemoReactionsRequestSchema: GenMessage<ListMemoReactionsRequest> = /*@__PURE__*/\n  messageDesc(file_api_v1_memo_service, 19);\n\n/**\n * @generated from message memos.api.v1.ListMemoReactionsResponse\n */\nexport type ListMemoReactionsResponse = Message<\"memos.api.v1.ListMemoReactionsResponse\"> & {\n  /**\n   * The list of reactions.\n   *\n   * @generated from field: repeated memos.api.v1.Reaction reactions = 1;\n   */\n  reactions: Reaction[];\n\n  /**\n   * A token for the next page of results.\n   *\n   * @generated from field: string next_page_token = 2;\n   */\n  nextPageToken: string;\n\n  /**\n   * The total count of reactions.\n   *\n   * @generated from field: int32 total_size = 3;\n   */\n  totalSize: number;\n};\n\n/**\n * Describes the message memos.api.v1.ListMemoReactionsResponse.\n * Use `create(ListMemoReactionsResponseSchema)` to create a new message.\n */\nexport const ListMemoReactionsResponseSchema: GenMessage<ListMemoReactionsResponse> = /*@__PURE__*/\n  messageDesc(file_api_v1_memo_service, 20);\n\n/**\n * @generated from message memos.api.v1.UpsertMemoReactionRequest\n */\nexport type UpsertMemoReactionRequest = Message<\"memos.api.v1.UpsertMemoReactionRequest\"> & {\n  /**\n   * Required. The resource name of the memo.\n   * Format: memos/{memo}\n   *\n   * @generated from field: string name = 1;\n   */\n  name: string;\n\n  /**\n   * Required. The reaction to upsert.\n   *\n   * @generated from field: memos.api.v1.Reaction reaction = 2;\n   */\n  reaction?: Reaction;\n};\n\n/**\n * Describes the message memos.api.v1.UpsertMemoReactionRequest.\n * Use `create(UpsertMemoReactionRequestSchema)` to create a new message.\n */\nexport const UpsertMemoReactionRequestSchema: GenMessage<UpsertMemoReactionRequest> = /*@__PURE__*/\n  messageDesc(file_api_v1_memo_service, 21);\n\n/**\n * @generated from message memos.api.v1.DeleteMemoReactionRequest\n */\nexport type DeleteMemoReactionRequest = Message<\"memos.api.v1.DeleteMemoReactionRequest\"> & {\n  /**\n   * Required. The resource name of the reaction to delete.\n   * Format: memos/{memo}/reactions/{reaction}\n   *\n   * @generated from field: string name = 1;\n   */\n  name: string;\n};\n\n/**\n * Describes the message memos.api.v1.DeleteMemoReactionRequest.\n * Use `create(DeleteMemoReactionRequestSchema)` to create a new message.\n */\nexport const DeleteMemoReactionRequestSchema: GenMessage<DeleteMemoReactionRequest> = /*@__PURE__*/\n  messageDesc(file_api_v1_memo_service, 22);\n\n/**\n * MemoShare is an access grant that permits read-only access to a memo via an opaque bearer token.\n *\n * @generated from message memos.api.v1.MemoShare\n */\nexport type MemoShare = Message<\"memos.api.v1.MemoShare\"> & {\n  /**\n   * The resource name of the share. Format: memos/{memo}/shares/{share}\n   * The {share} segment is the opaque token used in the share URL.\n   *\n   * @generated from field: string name = 1;\n   */\n  name: string;\n\n  /**\n   * Output only. When this share link was created.\n   *\n   * @generated from field: google.protobuf.Timestamp create_time = 2;\n   */\n  createTime?: Timestamp;\n\n  /**\n   * Optional. When set, the share link stops working after this time.\n   * If unset, the link never expires.\n   *\n   * @generated from field: optional google.protobuf.Timestamp expire_time = 3;\n   */\n  expireTime?: Timestamp;\n};\n\n/**\n * Describes the message memos.api.v1.MemoShare.\n * Use `create(MemoShareSchema)` to create a new message.\n */\nexport const MemoShareSchema: GenMessage<MemoShare> = /*@__PURE__*/\n  messageDesc(file_api_v1_memo_service, 23);\n\n/**\n * @generated from message memos.api.v1.CreateMemoShareRequest\n */\nexport type CreateMemoShareRequest = Message<\"memos.api.v1.CreateMemoShareRequest\"> & {\n  /**\n   * Required. The resource name of the memo to share.\n   * Format: memos/{memo}\n   *\n   * @generated from field: string parent = 1;\n   */\n  parent: string;\n\n  /**\n   * Required. The share to create.\n   *\n   * @generated from field: memos.api.v1.MemoShare memo_share = 2;\n   */\n  memoShare?: MemoShare;\n};\n\n/**\n * Describes the message memos.api.v1.CreateMemoShareRequest.\n * Use `create(CreateMemoShareRequestSchema)` to create a new message.\n */\nexport const CreateMemoShareRequestSchema: GenMessage<CreateMemoShareRequest> = /*@__PURE__*/\n  messageDesc(file_api_v1_memo_service, 24);\n\n/**\n * @generated from message memos.api.v1.ListMemoSharesRequest\n */\nexport type ListMemoSharesRequest = Message<\"memos.api.v1.ListMemoSharesRequest\"> & {\n  /**\n   * Required. The resource name of the memo.\n   * Format: memos/{memo}\n   *\n   * @generated from field: string parent = 1;\n   */\n  parent: string;\n};\n\n/**\n * Describes the message memos.api.v1.ListMemoSharesRequest.\n * Use `create(ListMemoSharesRequestSchema)` to create a new message.\n */\nexport const ListMemoSharesRequestSchema: GenMessage<ListMemoSharesRequest> = /*@__PURE__*/\n  messageDesc(file_api_v1_memo_service, 25);\n\n/**\n * @generated from message memos.api.v1.ListMemoSharesResponse\n */\nexport type ListMemoSharesResponse = Message<\"memos.api.v1.ListMemoSharesResponse\"> & {\n  /**\n   * The list of share links.\n   *\n   * @generated from field: repeated memos.api.v1.MemoShare memo_shares = 1;\n   */\n  memoShares: MemoShare[];\n};\n\n/**\n * Describes the message memos.api.v1.ListMemoSharesResponse.\n * Use `create(ListMemoSharesResponseSchema)` to create a new message.\n */\nexport const ListMemoSharesResponseSchema: GenMessage<ListMemoSharesResponse> = /*@__PURE__*/\n  messageDesc(file_api_v1_memo_service, 26);\n\n/**\n * @generated from message memos.api.v1.DeleteMemoShareRequest\n */\nexport type DeleteMemoShareRequest = Message<\"memos.api.v1.DeleteMemoShareRequest\"> & {\n  /**\n   * Required. The resource name of the share to delete.\n   * Format: memos/{memo}/shares/{share}\n   *\n   * @generated from field: string name = 1;\n   */\n  name: string;\n};\n\n/**\n * Describes the message memos.api.v1.DeleteMemoShareRequest.\n * Use `create(DeleteMemoShareRequestSchema)` to create a new message.\n */\nexport const DeleteMemoShareRequestSchema: GenMessage<DeleteMemoShareRequest> = /*@__PURE__*/\n  messageDesc(file_api_v1_memo_service, 27);\n\n/**\n * @generated from message memos.api.v1.GetMemoByShareRequest\n */\nexport type GetMemoByShareRequest = Message<\"memos.api.v1.GetMemoByShareRequest\"> & {\n  /**\n   * Required. The share token extracted from the share URL (/s/{share_id}).\n   *\n   * @generated from field: string share_id = 1;\n   */\n  shareId: string;\n};\n\n/**\n * Describes the message memos.api.v1.GetMemoByShareRequest.\n * Use `create(GetMemoByShareRequestSchema)` to create a new message.\n */\nexport const GetMemoByShareRequestSchema: GenMessage<GetMemoByShareRequest> = /*@__PURE__*/\n  messageDesc(file_api_v1_memo_service, 28);\n\n/**\n * @generated from enum memos.api.v1.Visibility\n */\nexport enum Visibility {\n  /**\n   * @generated from enum value: VISIBILITY_UNSPECIFIED = 0;\n   */\n  VISIBILITY_UNSPECIFIED = 0,\n\n  /**\n   * @generated from enum value: PRIVATE = 1;\n   */\n  PRIVATE = 1,\n\n  /**\n   * @generated from enum value: PROTECTED = 2;\n   */\n  PROTECTED = 2,\n\n  /**\n   * @generated from enum value: PUBLIC = 3;\n   */\n  PUBLIC = 3,\n}\n\n/**\n * Describes the enum memos.api.v1.Visibility.\n */\nexport const VisibilitySchema: GenEnum<Visibility> = /*@__PURE__*/\n  enumDesc(file_api_v1_memo_service, 0);\n\n/**\n * @generated from service memos.api.v1.MemoService\n */\nexport const MemoService: GenService<{\n  /**\n   * CreateMemo creates a memo.\n   *\n   * @generated from rpc memos.api.v1.MemoService.CreateMemo\n   */\n  createMemo: {\n    methodKind: \"unary\";\n    input: typeof CreateMemoRequestSchema;\n    output: typeof MemoSchema;\n  },\n  /**\n   * ListMemos lists memos with pagination and filter.\n   *\n   * @generated from rpc memos.api.v1.MemoService.ListMemos\n   */\n  listMemos: {\n    methodKind: \"unary\";\n    input: typeof ListMemosRequestSchema;\n    output: typeof ListMemosResponseSchema;\n  },\n  /**\n   * GetMemo gets a memo.\n   *\n   * @generated from rpc memos.api.v1.MemoService.GetMemo\n   */\n  getMemo: {\n    methodKind: \"unary\";\n    input: typeof GetMemoRequestSchema;\n    output: typeof MemoSchema;\n  },\n  /**\n   * UpdateMemo updates a memo.\n   *\n   * @generated from rpc memos.api.v1.MemoService.UpdateMemo\n   */\n  updateMemo: {\n    methodKind: \"unary\";\n    input: typeof UpdateMemoRequestSchema;\n    output: typeof MemoSchema;\n  },\n  /**\n   * DeleteMemo deletes a memo.\n   *\n   * @generated from rpc memos.api.v1.MemoService.DeleteMemo\n   */\n  deleteMemo: {\n    methodKind: \"unary\";\n    input: typeof DeleteMemoRequestSchema;\n    output: typeof EmptySchema;\n  },\n  /**\n   * SetMemoAttachments sets attachments for a memo.\n   *\n   * @generated from rpc memos.api.v1.MemoService.SetMemoAttachments\n   */\n  setMemoAttachments: {\n    methodKind: \"unary\";\n    input: typeof SetMemoAttachmentsRequestSchema;\n    output: typeof EmptySchema;\n  },\n  /**\n   * ListMemoAttachments lists attachments for a memo.\n   *\n   * @generated from rpc memos.api.v1.MemoService.ListMemoAttachments\n   */\n  listMemoAttachments: {\n    methodKind: \"unary\";\n    input: typeof ListMemoAttachmentsRequestSchema;\n    output: typeof ListMemoAttachmentsResponseSchema;\n  },\n  /**\n   * SetMemoRelations sets relations for a memo.\n   *\n   * @generated from rpc memos.api.v1.MemoService.SetMemoRelations\n   */\n  setMemoRelations: {\n    methodKind: \"unary\";\n    input: typeof SetMemoRelationsRequestSchema;\n    output: typeof EmptySchema;\n  },\n  /**\n   * ListMemoRelations lists relations for a memo.\n   *\n   * @generated from rpc memos.api.v1.MemoService.ListMemoRelations\n   */\n  listMemoRelations: {\n    methodKind: \"unary\";\n    input: typeof ListMemoRelationsRequestSchema;\n    output: typeof ListMemoRelationsResponseSchema;\n  },\n  /**\n   * CreateMemoComment creates a comment for a memo.\n   *\n   * @generated from rpc memos.api.v1.MemoService.CreateMemoComment\n   */\n  createMemoComment: {\n    methodKind: \"unary\";\n    input: typeof CreateMemoCommentRequestSchema;\n    output: typeof MemoSchema;\n  },\n  /**\n   * ListMemoComments lists comments for a memo.\n   *\n   * @generated from rpc memos.api.v1.MemoService.ListMemoComments\n   */\n  listMemoComments: {\n    methodKind: \"unary\";\n    input: typeof ListMemoCommentsRequestSchema;\n    output: typeof ListMemoCommentsResponseSchema;\n  },\n  /**\n   * ListMemoReactions lists reactions for a memo.\n   *\n   * @generated from rpc memos.api.v1.MemoService.ListMemoReactions\n   */\n  listMemoReactions: {\n    methodKind: \"unary\";\n    input: typeof ListMemoReactionsRequestSchema;\n    output: typeof ListMemoReactionsResponseSchema;\n  },\n  /**\n   * UpsertMemoReaction upserts a reaction for a memo.\n   *\n   * @generated from rpc memos.api.v1.MemoService.UpsertMemoReaction\n   */\n  upsertMemoReaction: {\n    methodKind: \"unary\";\n    input: typeof UpsertMemoReactionRequestSchema;\n    output: typeof ReactionSchema;\n  },\n  /**\n   * DeleteMemoReaction deletes a reaction for a memo.\n   *\n   * @generated from rpc memos.api.v1.MemoService.DeleteMemoReaction\n   */\n  deleteMemoReaction: {\n    methodKind: \"unary\";\n    input: typeof DeleteMemoReactionRequestSchema;\n    output: typeof EmptySchema;\n  },\n  /**\n   * CreateMemoShare creates a share link for a memo. Requires authentication as the memo creator.\n   *\n   * @generated from rpc memos.api.v1.MemoService.CreateMemoShare\n   */\n  createMemoShare: {\n    methodKind: \"unary\";\n    input: typeof CreateMemoShareRequestSchema;\n    output: typeof MemoShareSchema;\n  },\n  /**\n   * ListMemoShares lists all share links for a memo. Requires authentication as the memo creator.\n   *\n   * @generated from rpc memos.api.v1.MemoService.ListMemoShares\n   */\n  listMemoShares: {\n    methodKind: \"unary\";\n    input: typeof ListMemoSharesRequestSchema;\n    output: typeof ListMemoSharesResponseSchema;\n  },\n  /**\n   * DeleteMemoShare revokes a share link. Requires authentication as the memo creator.\n   *\n   * @generated from rpc memos.api.v1.MemoService.DeleteMemoShare\n   */\n  deleteMemoShare: {\n    methodKind: \"unary\";\n    input: typeof DeleteMemoShareRequestSchema;\n    output: typeof EmptySchema;\n  },\n  /**\n   * GetMemoByShare resolves a share token to its memo. No authentication required.\n   * Returns NOT_FOUND if the token is invalid or expired.\n   *\n   * @generated from rpc memos.api.v1.MemoService.GetMemoByShare\n   */\n  getMemoByShare: {\n    methodKind: \"unary\";\n    input: typeof GetMemoByShareRequestSchema;\n    output: typeof MemoSchema;\n  },\n}> = /*@__PURE__*/\n  serviceDesc(file_api_v1_memo_service, 0);\n\n"
  },
  {
    "path": "web/src/types/proto/api/v1/shortcut_service_pb.ts",
    "content": "// @generated by protoc-gen-es v2.11.0 with parameter \"target=ts\"\n// @generated from file api/v1/shortcut_service.proto (package memos.api.v1, syntax proto3)\n/* eslint-disable */\n\nimport type { GenFile, GenMessage, GenService } from \"@bufbuild/protobuf/codegenv2\";\nimport { fileDesc, messageDesc, serviceDesc } from \"@bufbuild/protobuf/codegenv2\";\nimport { file_google_api_annotations } from \"../../google/api/annotations_pb\";\nimport { file_google_api_client } from \"../../google/api/client_pb\";\nimport { file_google_api_field_behavior } from \"../../google/api/field_behavior_pb\";\nimport { file_google_api_resource } from \"../../google/api/resource_pb\";\nimport type { EmptySchema, FieldMask } from \"@bufbuild/protobuf/wkt\";\nimport { file_google_protobuf_empty, file_google_protobuf_field_mask } from \"@bufbuild/protobuf/wkt\";\nimport type { Message } from \"@bufbuild/protobuf\";\n\n/**\n * Describes the file api/v1/shortcut_service.proto.\n */\nexport const file_api_v1_shortcut_service: GenFile = /*@__PURE__*/\n  fileDesc(\"Ch1hcGkvdjEvc2hvcnRjdXRfc2VydmljZS5wcm90bxIMbWVtb3MuYXBpLnYxIpoBCghTaG9ydGN1dBIRCgRuYW1lGAEgASgJQgPgQQgSEgoFdGl0bGUYAiABKAlCA+BBAhITCgZmaWx0ZXIYAyABKAlCA+BBATpS6kFPChVtZW1vcy5hcGkudjEvU2hvcnRjdXQSIXVzZXJzL3t1c2VyfS9zaG9ydGN1dHMve3Nob3J0Y3V0fSoJc2hvcnRjdXRzMghzaG9ydGN1dCJFChRMaXN0U2hvcnRjdXRzUmVxdWVzdBItCgZwYXJlbnQYASABKAlCHeBBAvpBFxIVbWVtb3MuYXBpLnYxL1Nob3J0Y3V0IkIKFUxpc3RTaG9ydGN1dHNSZXNwb25zZRIpCglzaG9ydGN1dHMYASADKAsyFi5tZW1vcy5hcGkudjEuU2hvcnRjdXQiQQoSR2V0U2hvcnRjdXRSZXF1ZXN0EisKBG5hbWUYASABKAlCHeBBAvpBFwoVbWVtb3MuYXBpLnYxL1Nob3J0Y3V0IpEBChVDcmVhdGVTaG9ydGN1dFJlcXVlc3QSLQoGcGFyZW50GAEgASgJQh3gQQL6QRcSFW1lbW9zLmFwaS52MS9TaG9ydGN1dBItCghzaG9ydGN1dBgCIAEoCzIWLm1lbW9zLmFwaS52MS5TaG9ydGN1dEID4EECEhoKDXZhbGlkYXRlX29ubHkYAyABKAhCA+BBASJ8ChVVcGRhdGVTaG9ydGN1dFJlcXVlc3QSLQoIc2hvcnRjdXQYASABKAsyFi5tZW1vcy5hcGkudjEuU2hvcnRjdXRCA+BBAhI0Cgt1cGRhdGVfbWFzaxgCIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5GaWVsZE1hc2tCA+BBASJEChVEZWxldGVTaG9ydGN1dFJlcXVlc3QSKwoEbmFtZRgBIAEoCUId4EEC+kEXChVtZW1vcy5hcGkudjEvU2hvcnRjdXQy3gUKD1Nob3J0Y3V0U2VydmljZRKNAQoNTGlzdFNob3J0Y3V0cxIiLm1lbW9zLmFwaS52MS5MaXN0U2hvcnRjdXRzUmVxdWVzdBojLm1lbW9zLmFwaS52MS5MaXN0U2hvcnRjdXRzUmVzcG9uc2UiM9pBBnBhcmVudILT5JMCJBIiL2FwaS92MS97cGFyZW50PXVzZXJzLyp9L3Nob3J0Y3V0cxJ6CgtHZXRTaG9ydGN1dBIgLm1lbW9zLmFwaS52MS5HZXRTaG9ydGN1dFJlcXVlc3QaFi5tZW1vcy5hcGkudjEuU2hvcnRjdXQiMdpBBG5hbWWC0+STAiQSIi9hcGkvdjEve25hbWU9dXNlcnMvKi9zaG9ydGN1dHMvKn0SlQEKDkNyZWF0ZVNob3J0Y3V0EiMubWVtb3MuYXBpLnYxLkNyZWF0ZVNob3J0Y3V0UmVxdWVzdBoWLm1lbW9zLmFwaS52MS5TaG9ydGN1dCJG2kEPcGFyZW50LHNob3J0Y3V0gtPkkwIuOghzaG9ydGN1dCIiL2FwaS92MS97cGFyZW50PXVzZXJzLyp9L3Nob3J0Y3V0cxKjAQoOVXBkYXRlU2hvcnRjdXQSIy5tZW1vcy5hcGkudjEuVXBkYXRlU2hvcnRjdXRSZXF1ZXN0GhYubWVtb3MuYXBpLnYxLlNob3J0Y3V0IlTaQRRzaG9ydGN1dCx1cGRhdGVfbWFza4LT5JMCNzoIc2hvcnRjdXQyKy9hcGkvdjEve3Nob3J0Y3V0Lm5hbWU9dXNlcnMvKi9zaG9ydGN1dHMvKn0SgAEKDkRlbGV0ZVNob3J0Y3V0EiMubWVtb3MuYXBpLnYxLkRlbGV0ZVNob3J0Y3V0UmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSIx2kEEbmFtZYLT5JMCJCoiL2FwaS92MS97bmFtZT11c2Vycy8qL3Nob3J0Y3V0cy8qfUKsAQoQY29tLm1lbW9zLmFwaS52MUIUU2hvcnRjdXRTZXJ2aWNlUHJvdG9QAVowZ2l0aHViLmNvbS91c2VtZW1vcy9tZW1vcy9wcm90by9nZW4vYXBpL3YxO2FwaXYxogIDTUFYqgIMTWVtb3MuQXBpLlYxygIMTWVtb3NcQXBpXFYx4gIYTWVtb3NcQXBpXFYxXEdQQk1ldGFkYXRh6gIOTWVtb3M6OkFwaTo6VjFiBnByb3RvMw\", [file_google_api_annotations, file_google_api_client, file_google_api_field_behavior, file_google_api_resource, file_google_protobuf_empty, file_google_protobuf_field_mask]);\n\n/**\n * @generated from message memos.api.v1.Shortcut\n */\nexport type Shortcut = Message<\"memos.api.v1.Shortcut\"> & {\n  /**\n   * The resource name of the shortcut.\n   * Format: users/{user}/shortcuts/{shortcut}\n   *\n   * @generated from field: string name = 1;\n   */\n  name: string;\n\n  /**\n   * The title of the shortcut.\n   *\n   * @generated from field: string title = 2;\n   */\n  title: string;\n\n  /**\n   * The filter expression for the shortcut.\n   *\n   * @generated from field: string filter = 3;\n   */\n  filter: string;\n};\n\n/**\n * Describes the message memos.api.v1.Shortcut.\n * Use `create(ShortcutSchema)` to create a new message.\n */\nexport const ShortcutSchema: GenMessage<Shortcut> = /*@__PURE__*/\n  messageDesc(file_api_v1_shortcut_service, 0);\n\n/**\n * @generated from message memos.api.v1.ListShortcutsRequest\n */\nexport type ListShortcutsRequest = Message<\"memos.api.v1.ListShortcutsRequest\"> & {\n  /**\n   * Required. The parent resource where shortcuts are listed.\n   * Format: users/{user}\n   *\n   * @generated from field: string parent = 1;\n   */\n  parent: string;\n};\n\n/**\n * Describes the message memos.api.v1.ListShortcutsRequest.\n * Use `create(ListShortcutsRequestSchema)` to create a new message.\n */\nexport const ListShortcutsRequestSchema: GenMessage<ListShortcutsRequest> = /*@__PURE__*/\n  messageDesc(file_api_v1_shortcut_service, 1);\n\n/**\n * @generated from message memos.api.v1.ListShortcutsResponse\n */\nexport type ListShortcutsResponse = Message<\"memos.api.v1.ListShortcutsResponse\"> & {\n  /**\n   * The list of shortcuts.\n   *\n   * @generated from field: repeated memos.api.v1.Shortcut shortcuts = 1;\n   */\n  shortcuts: Shortcut[];\n};\n\n/**\n * Describes the message memos.api.v1.ListShortcutsResponse.\n * Use `create(ListShortcutsResponseSchema)` to create a new message.\n */\nexport const ListShortcutsResponseSchema: GenMessage<ListShortcutsResponse> = /*@__PURE__*/\n  messageDesc(file_api_v1_shortcut_service, 2);\n\n/**\n * @generated from message memos.api.v1.GetShortcutRequest\n */\nexport type GetShortcutRequest = Message<\"memos.api.v1.GetShortcutRequest\"> & {\n  /**\n   * Required. The resource name of the shortcut to retrieve.\n   * Format: users/{user}/shortcuts/{shortcut}\n   *\n   * @generated from field: string name = 1;\n   */\n  name: string;\n};\n\n/**\n * Describes the message memos.api.v1.GetShortcutRequest.\n * Use `create(GetShortcutRequestSchema)` to create a new message.\n */\nexport const GetShortcutRequestSchema: GenMessage<GetShortcutRequest> = /*@__PURE__*/\n  messageDesc(file_api_v1_shortcut_service, 3);\n\n/**\n * @generated from message memos.api.v1.CreateShortcutRequest\n */\nexport type CreateShortcutRequest = Message<\"memos.api.v1.CreateShortcutRequest\"> & {\n  /**\n   * Required. The parent resource where this shortcut will be created.\n   * Format: users/{user}\n   *\n   * @generated from field: string parent = 1;\n   */\n  parent: string;\n\n  /**\n   * Required. The shortcut to create.\n   *\n   * @generated from field: memos.api.v1.Shortcut shortcut = 2;\n   */\n  shortcut?: Shortcut;\n\n  /**\n   * Optional. If set, validate the request, but do not actually create the shortcut.\n   *\n   * @generated from field: bool validate_only = 3;\n   */\n  validateOnly: boolean;\n};\n\n/**\n * Describes the message memos.api.v1.CreateShortcutRequest.\n * Use `create(CreateShortcutRequestSchema)` to create a new message.\n */\nexport const CreateShortcutRequestSchema: GenMessage<CreateShortcutRequest> = /*@__PURE__*/\n  messageDesc(file_api_v1_shortcut_service, 4);\n\n/**\n * @generated from message memos.api.v1.UpdateShortcutRequest\n */\nexport type UpdateShortcutRequest = Message<\"memos.api.v1.UpdateShortcutRequest\"> & {\n  /**\n   * Required. The shortcut resource which replaces the resource on the server.\n   *\n   * @generated from field: memos.api.v1.Shortcut shortcut = 1;\n   */\n  shortcut?: Shortcut;\n\n  /**\n   * Optional. The list of fields to update.\n   *\n   * @generated from field: google.protobuf.FieldMask update_mask = 2;\n   */\n  updateMask?: FieldMask;\n};\n\n/**\n * Describes the message memos.api.v1.UpdateShortcutRequest.\n * Use `create(UpdateShortcutRequestSchema)` to create a new message.\n */\nexport const UpdateShortcutRequestSchema: GenMessage<UpdateShortcutRequest> = /*@__PURE__*/\n  messageDesc(file_api_v1_shortcut_service, 5);\n\n/**\n * @generated from message memos.api.v1.DeleteShortcutRequest\n */\nexport type DeleteShortcutRequest = Message<\"memos.api.v1.DeleteShortcutRequest\"> & {\n  /**\n   * Required. The resource name of the shortcut to delete.\n   * Format: users/{user}/shortcuts/{shortcut}\n   *\n   * @generated from field: string name = 1;\n   */\n  name: string;\n};\n\n/**\n * Describes the message memos.api.v1.DeleteShortcutRequest.\n * Use `create(DeleteShortcutRequestSchema)` to create a new message.\n */\nexport const DeleteShortcutRequestSchema: GenMessage<DeleteShortcutRequest> = /*@__PURE__*/\n  messageDesc(file_api_v1_shortcut_service, 6);\n\n/**\n * @generated from service memos.api.v1.ShortcutService\n */\nexport const ShortcutService: GenService<{\n  /**\n   * ListShortcuts returns a list of shortcuts for a user.\n   *\n   * @generated from rpc memos.api.v1.ShortcutService.ListShortcuts\n   */\n  listShortcuts: {\n    methodKind: \"unary\";\n    input: typeof ListShortcutsRequestSchema;\n    output: typeof ListShortcutsResponseSchema;\n  },\n  /**\n   * GetShortcut gets a shortcut by name.\n   *\n   * @generated from rpc memos.api.v1.ShortcutService.GetShortcut\n   */\n  getShortcut: {\n    methodKind: \"unary\";\n    input: typeof GetShortcutRequestSchema;\n    output: typeof ShortcutSchema;\n  },\n  /**\n   * CreateShortcut creates a new shortcut for a user.\n   *\n   * @generated from rpc memos.api.v1.ShortcutService.CreateShortcut\n   */\n  createShortcut: {\n    methodKind: \"unary\";\n    input: typeof CreateShortcutRequestSchema;\n    output: typeof ShortcutSchema;\n  },\n  /**\n   * UpdateShortcut updates a shortcut for a user.\n   *\n   * @generated from rpc memos.api.v1.ShortcutService.UpdateShortcut\n   */\n  updateShortcut: {\n    methodKind: \"unary\";\n    input: typeof UpdateShortcutRequestSchema;\n    output: typeof ShortcutSchema;\n  },\n  /**\n   * DeleteShortcut deletes a shortcut for a user.\n   *\n   * @generated from rpc memos.api.v1.ShortcutService.DeleteShortcut\n   */\n  deleteShortcut: {\n    methodKind: \"unary\";\n    input: typeof DeleteShortcutRequestSchema;\n    output: typeof EmptySchema;\n  },\n}> = /*@__PURE__*/\n  serviceDesc(file_api_v1_shortcut_service, 0);\n\n"
  },
  {
    "path": "web/src/types/proto/api/v1/user_service_pb.ts",
    "content": "// @generated by protoc-gen-es v2.11.0 with parameter \"target=ts\"\n// @generated from file api/v1/user_service.proto (package memos.api.v1, syntax proto3)\n/* eslint-disable */\n\nimport type { GenEnum, GenFile, GenMessage, GenService } from \"@bufbuild/protobuf/codegenv2\";\nimport { enumDesc, fileDesc, messageDesc, serviceDesc } from \"@bufbuild/protobuf/codegenv2\";\nimport type { State } from \"./common_pb\";\nimport { file_api_v1_common } from \"./common_pb\";\nimport { file_google_api_annotations } from \"../../google/api/annotations_pb\";\nimport { file_google_api_client } from \"../../google/api/client_pb\";\nimport { file_google_api_field_behavior } from \"../../google/api/field_behavior_pb\";\nimport { file_google_api_resource } from \"../../google/api/resource_pb\";\nimport type { EmptySchema, FieldMask, Timestamp } from \"@bufbuild/protobuf/wkt\";\nimport { file_google_protobuf_empty, file_google_protobuf_field_mask, file_google_protobuf_timestamp } from \"@bufbuild/protobuf/wkt\";\nimport type { Message } from \"@bufbuild/protobuf\";\n\n/**\n * Describes the file api/v1/user_service.proto.\n */\nexport const file_api_v1_user_service: GenFile = /*@__PURE__*/\n  fileDesc(\"ChlhcGkvdjEvdXNlcl9zZXJ2aWNlLnByb3RvEgxtZW1vcy5hcGkudjEi1gMKBFVzZXISEQoEbmFtZRgBIAEoCUID4EEIEioKBHJvbGUYAiABKA4yFy5tZW1vcy5hcGkudjEuVXNlci5Sb2xlQgPgQQISFQoIdXNlcm5hbWUYAyABKAlCA+BBAhISCgVlbWFpbBgEIAEoCUID4EEBEhkKDGRpc3BsYXlfbmFtZRgFIAEoCUID4EEBEhcKCmF2YXRhcl91cmwYBiABKAlCA+BBARIYCgtkZXNjcmlwdGlvbhgHIAEoCUID4EEBEhUKCHBhc3N3b3JkGAggASgJQgPgQQQSJwoFc3RhdGUYCSABKA4yEy5tZW1vcy5hcGkudjEuU3RhdGVCA+BBAhI0CgtjcmVhdGVfdGltZRgKIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXBCA+BBAxI0Cgt1cGRhdGVfdGltZRgLIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXBCA+BBAyIxCgRSb2xlEhQKEFJPTEVfVU5TUEVDSUZJRUQQABIJCgVBRE1JThACEggKBFVTRVIQAzo36kE0ChFtZW1vcy5hcGkudjEvVXNlchIMdXNlcnMve3VzZXJ9GgRuYW1lKgV1c2VyczIEdXNlciJzChBMaXN0VXNlcnNSZXF1ZXN0EhYKCXBhZ2Vfc2l6ZRgBIAEoBUID4EEBEhcKCnBhZ2VfdG9rZW4YAiABKAlCA+BBARITCgZmaWx0ZXIYAyABKAlCA+BBARIZCgxzaG93X2RlbGV0ZWQYBCABKAhCA+BBASJjChFMaXN0VXNlcnNSZXNwb25zZRIhCgV1c2VycxgBIAMoCzISLm1lbW9zLmFwaS52MS5Vc2VyEhcKD25leHRfcGFnZV90b2tlbhgCIAEoCRISCgp0b3RhbF9zaXplGAMgASgFIm0KDkdldFVzZXJSZXF1ZXN0EicKBG5hbWUYASABKAlCGeBBAvpBEwoRbWVtb3MuYXBpLnYxL1VzZXISMgoJcmVhZF9tYXNrGAIgASgLMhouZ29vZ2xlLnByb3RvYnVmLkZpZWxkTWFza0ID4EEBIogBChFDcmVhdGVVc2VyUmVxdWVzdBIoCgR1c2VyGAEgASgLMhIubWVtb3MuYXBpLnYxLlVzZXJCBuBBAuBBBBIUCgd1c2VyX2lkGAIgASgJQgPgQQESGgoNdmFsaWRhdGVfb25seRgDIAEoCEID4EEBEhcKCnJlcXVlc3RfaWQYBCABKAlCA+BBASKMAQoRVXBkYXRlVXNlclJlcXVlc3QSJQoEdXNlchgBIAEoCzISLm1lbW9zLmFwaS52MS5Vc2VyQgPgQQISNAoLdXBkYXRlX21hc2sYAiABKAsyGi5nb29nbGUucHJvdG9idWYuRmllbGRNYXNrQgPgQQISGgoNYWxsb3dfbWlzc2luZxgDIAEoCEID4EEBIlAKEURlbGV0ZVVzZXJSZXF1ZXN0EicKBG5hbWUYASABKAlCGeBBAvpBEwoRbWVtb3MuYXBpLnYxL1VzZXISEgoFZm9yY2UYAiABKAhCA+BBASLYAwoJVXNlclN0YXRzEhEKBG5hbWUYASABKAlCA+BBCBI7ChdtZW1vX2Rpc3BsYXlfdGltZXN0YW1wcxgCIAMoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASPgoPbWVtb190eXBlX3N0YXRzGAMgASgLMiUubWVtb3MuYXBpLnYxLlVzZXJTdGF0cy5NZW1vVHlwZVN0YXRzEjgKCXRhZ19jb3VudBgEIAMoCzIlLm1lbW9zLmFwaS52MS5Vc2VyU3RhdHMuVGFnQ291bnRFbnRyeRIUCgxwaW5uZWRfbWVtb3MYBSADKAkSGAoQdG90YWxfbWVtb19jb3VudBgGIAEoBRovCg1UYWdDb3VudEVudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoBToCOAEaXwoNTWVtb1R5cGVTdGF0cxISCgpsaW5rX2NvdW50GAEgASgFEhIKCmNvZGVfY291bnQYAiABKAUSEgoKdG9kb19jb3VudBgDIAEoBRISCgp1bmRvX2NvdW50GAQgASgFOj/qQTwKFm1lbW9zLmFwaS52MS9Vc2VyU3RhdHMSDHVzZXJzL3t1c2VyfSoJdXNlclN0YXRzMgl1c2VyU3RhdHMiPgoTR2V0VXNlclN0YXRzUmVxdWVzdBInCgRuYW1lGAEgASgJQhngQQL6QRMKEW1lbW9zLmFwaS52MS9Vc2VyIhkKF0xpc3RBbGxVc2VyU3RhdHNSZXF1ZXN0IkIKGExpc3RBbGxVc2VyU3RhdHNSZXNwb25zZRImCgVzdGF0cxgBIAMoCzIXLm1lbW9zLmFwaS52MS5Vc2VyU3RhdHMi4AMKC1VzZXJTZXR0aW5nEhEKBG5hbWUYASABKAlCA+BBCBJDCg9nZW5lcmFsX3NldHRpbmcYAiABKAsyKC5tZW1vcy5hcGkudjEuVXNlclNldHRpbmcuR2VuZXJhbFNldHRpbmdIABJFChB3ZWJob29rc19zZXR0aW5nGAUgASgLMikubWVtb3MuYXBpLnYxLlVzZXJTZXR0aW5nLldlYmhvb2tzU2V0dGluZ0gAGlcKDkdlbmVyYWxTZXR0aW5nEhMKBmxvY2FsZRgBIAEoCUID4EEBEhwKD21lbW9fdmlzaWJpbGl0eRgDIAEoCUID4EEBEhIKBXRoZW1lGAQgASgJQgPgQQEaPgoPV2ViaG9va3NTZXR0aW5nEisKCHdlYmhvb2tzGAEgAygLMhkubWVtb3MuYXBpLnYxLlVzZXJXZWJob29rIjUKA0tleRITCg9LRVlfVU5TUEVDSUZJRUQQABILCgdHRU5FUkFMEAESDAoIV0VCSE9PS1MQBDpZ6kFWChhtZW1vcy5hcGkudjEvVXNlclNldHRpbmcSH3VzZXJzL3t1c2VyfS9zZXR0aW5ncy97c2V0dGluZ30qDHVzZXJTZXR0aW5nczILdXNlclNldHRpbmdCBwoFdmFsdWUiRwoVR2V0VXNlclNldHRpbmdSZXF1ZXN0Ei4KBG5hbWUYASABKAlCIOBBAvpBGgoYbWVtb3MuYXBpLnYxL1VzZXJTZXR0aW5nIoEBChhVcGRhdGVVc2VyU2V0dGluZ1JlcXVlc3QSLwoHc2V0dGluZxgBIAEoCzIZLm1lbW9zLmFwaS52MS5Vc2VyU2V0dGluZ0ID4EECEjQKC3VwZGF0ZV9tYXNrGAIgASgLMhouZ29vZ2xlLnByb3RvYnVmLkZpZWxkTWFza0ID4EECInUKF0xpc3RVc2VyU2V0dGluZ3NSZXF1ZXN0EikKBnBhcmVudBgBIAEoCUIZ4EEC+kETChFtZW1vcy5hcGkudjEvVXNlchIWCglwYWdlX3NpemUYAiABKAVCA+BBARIXCgpwYWdlX3Rva2VuGAMgASgJQgPgQQEidAoYTGlzdFVzZXJTZXR0aW5nc1Jlc3BvbnNlEisKCHNldHRpbmdzGAEgAygLMhkubWVtb3MuYXBpLnYxLlVzZXJTZXR0aW5nEhcKD25leHRfcGFnZV90b2tlbhgCIAEoCRISCgp0b3RhbF9zaXplGAMgASgFIvICChNQZXJzb25hbEFjY2Vzc1Rva2VuEhEKBG5hbWUYASABKAlCA+BBCBIYCgtkZXNjcmlwdGlvbhgCIAEoCUID4EEBEjMKCmNyZWF0ZWRfYXQYAyABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wQgPgQQMSMwoKZXhwaXJlc19hdBgEIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXBCA+BBARI1CgxsYXN0X3VzZWRfYXQYBSABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wQgPgQQM6jAHqQYgBCiBtZW1vcy5hcGkudjEvUGVyc29uYWxBY2Nlc3NUb2tlbhI5dXNlcnMve3VzZXJ9L3BlcnNvbmFsQWNjZXNzVG9rZW5zL3twZXJzb25hbF9hY2Nlc3NfdG9rZW59KhRwZXJzb25hbEFjY2Vzc1Rva2VuczITcGVyc29uYWxBY2Nlc3NUb2tlbiJ9Ch9MaXN0UGVyc29uYWxBY2Nlc3NUb2tlbnNSZXF1ZXN0EikKBnBhcmVudBgBIAEoCUIZ4EEC+kETChFtZW1vcy5hcGkudjEvVXNlchIWCglwYWdlX3NpemUYAiABKAVCA+BBARIXCgpwYWdlX3Rva2VuGAMgASgJQgPgQQEikgEKIExpc3RQZXJzb25hbEFjY2Vzc1Rva2Vuc1Jlc3BvbnNlEkEKFnBlcnNvbmFsX2FjY2Vzc190b2tlbnMYASADKAsyIS5tZW1vcy5hcGkudjEuUGVyc29uYWxBY2Nlc3NUb2tlbhIXCg9uZXh0X3BhZ2VfdG9rZW4YAiABKAkSEgoKdG90YWxfc2l6ZRgDIAEoBSKFAQogQ3JlYXRlUGVyc29uYWxBY2Nlc3NUb2tlblJlcXVlc3QSKQoGcGFyZW50GAEgASgJQhngQQL6QRMKEW1lbW9zLmFwaS52MS9Vc2VyEhgKC2Rlc2NyaXB0aW9uGAIgASgJQgPgQQESHAoPZXhwaXJlc19pbl9kYXlzGAMgASgFQgPgQQEidAohQ3JlYXRlUGVyc29uYWxBY2Nlc3NUb2tlblJlc3BvbnNlEkAKFXBlcnNvbmFsX2FjY2Vzc190b2tlbhgBIAEoCzIhLm1lbW9zLmFwaS52MS5QZXJzb25hbEFjY2Vzc1Rva2VuEg0KBXRva2VuGAIgASgJIloKIERlbGV0ZVBlcnNvbmFsQWNjZXNzVG9rZW5SZXF1ZXN0EjYKBG5hbWUYASABKAlCKOBBAvpBIgogbWVtb3MuYXBpLnYxL1BlcnNvbmFsQWNjZXNzVG9rZW4iqgEKC1VzZXJXZWJob29rEgwKBG5hbWUYASABKAkSCwoDdXJsGAIgASgJEhQKDGRpc3BsYXlfbmFtZRgDIAEoCRI0CgtjcmVhdGVfdGltZRgEIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXBCA+BBAxI0Cgt1cGRhdGVfdGltZRgFIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXBCA+BBAyIuChdMaXN0VXNlcldlYmhvb2tzUmVxdWVzdBITCgZwYXJlbnQYASABKAlCA+BBAiJHChhMaXN0VXNlcldlYmhvb2tzUmVzcG9uc2USKwoId2ViaG9va3MYASADKAsyGS5tZW1vcy5hcGkudjEuVXNlcldlYmhvb2siYAoYQ3JlYXRlVXNlcldlYmhvb2tSZXF1ZXN0EhMKBnBhcmVudBgBIAEoCUID4EECEi8KB3dlYmhvb2sYAiABKAsyGS5tZW1vcy5hcGkudjEuVXNlcldlYmhvb2tCA+BBAiJ8ChhVcGRhdGVVc2VyV2ViaG9va1JlcXVlc3QSLwoHd2ViaG9vaxgBIAEoCzIZLm1lbW9zLmFwaS52MS5Vc2VyV2ViaG9va0ID4EECEi8KC3VwZGF0ZV9tYXNrGAIgASgLMhouZ29vZ2xlLnByb3RvYnVmLkZpZWxkTWFzayItChhEZWxldGVVc2VyV2ViaG9va1JlcXVlc3QSEQoEbmFtZRgBIAEoCUID4EECIvAEChBVc2VyTm90aWZpY2F0aW9uEhQKBG5hbWUYASABKAlCBuBBA+BBCBIpCgZzZW5kZXIYAiABKAlCGeBBA/pBEwoRbWVtb3MuYXBpLnYxL1VzZXISOgoGc3RhdHVzGAMgASgOMiUubWVtb3MuYXBpLnYxLlVzZXJOb3RpZmljYXRpb24uU3RhdHVzQgPgQQESNAoLY3JlYXRlX3RpbWUYBCABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wQgPgQQMSNgoEdHlwZRgFIAEoDjIjLm1lbW9zLmFwaS52MS5Vc2VyTm90aWZpY2F0aW9uLlR5cGVCA+BBAxJOCgxtZW1vX2NvbW1lbnQYBiABKAsyMS5tZW1vcy5hcGkudjEuVXNlck5vdGlmaWNhdGlvbi5NZW1vQ29tbWVudFBheWxvYWRCA+BBA0gAGjgKEk1lbW9Db21tZW50UGF5bG9hZBIMCgRtZW1vGAEgASgJEhQKDHJlbGF0ZWRfbWVtbxgCIAEoCSI6CgZTdGF0dXMSFgoSU1RBVFVTX1VOU1BFQ0lGSUVEEAASCgoGVU5SRUFEEAESDAoIQVJDSElWRUQQAiIuCgRUeXBlEhQKEFRZUEVfVU5TUEVDSUZJRUQQABIQCgxNRU1PX0NPTU1FTlQQATpw6kFtCh1tZW1vcy5hcGkudjEvVXNlck5vdGlmaWNhdGlvbhIpdXNlcnMve3VzZXJ9L25vdGlmaWNhdGlvbnMve25vdGlmaWNhdGlvbn0aBG5hbWUqDW5vdGlmaWNhdGlvbnMyDG5vdGlmaWNhdGlvbkIJCgdwYXlsb2FkIo8BChxMaXN0VXNlck5vdGlmaWNhdGlvbnNSZXF1ZXN0EikKBnBhcmVudBgBIAEoCUIZ4EEC+kETChFtZW1vcy5hcGkudjEvVXNlchIWCglwYWdlX3NpemUYAiABKAVCA+BBARIXCgpwYWdlX3Rva2VuGAMgASgJQgPgQQESEwoGZmlsdGVyGAQgASgJQgPgQQEibwodTGlzdFVzZXJOb3RpZmljYXRpb25zUmVzcG9uc2USNQoNbm90aWZpY2F0aW9ucxgBIAMoCzIeLm1lbW9zLmFwaS52MS5Vc2VyTm90aWZpY2F0aW9uEhcKD25leHRfcGFnZV90b2tlbhgCIAEoCSKQAQodVXBkYXRlVXNlck5vdGlmaWNhdGlvblJlcXVlc3QSOQoMbm90aWZpY2F0aW9uGAEgASgLMh4ubWVtb3MuYXBpLnYxLlVzZXJOb3RpZmljYXRpb25CA+BBAhI0Cgt1cGRhdGVfbWFzaxgCIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5GaWVsZE1hc2tCA+BBAiJUCh1EZWxldGVVc2VyTm90aWZpY2F0aW9uUmVxdWVzdBIzCgRuYW1lGAEgASgJQiXgQQL6QR8KHW1lbW9zLmFwaS52MS9Vc2VyTm90aWZpY2F0aW9uMoMXCgtVc2VyU2VydmljZRJjCglMaXN0VXNlcnMSHi5tZW1vcy5hcGkudjEuTGlzdFVzZXJzUmVxdWVzdBofLm1lbW9zLmFwaS52MS5MaXN0VXNlcnNSZXNwb25zZSIVgtPkkwIPEg0vYXBpL3YxL3VzZXJzEmIKB0dldFVzZXISHC5tZW1vcy5hcGkudjEuR2V0VXNlclJlcXVlc3QaEi5tZW1vcy5hcGkudjEuVXNlciIl2kEEbmFtZYLT5JMCGBIWL2FwaS92MS97bmFtZT11c2Vycy8qfRJlCgpDcmVhdGVVc2VyEh8ubWVtb3MuYXBpLnYxLkNyZWF0ZVVzZXJSZXF1ZXN0GhIubWVtb3MuYXBpLnYxLlVzZXIiItpBBHVzZXKC0+STAhU6BHVzZXIiDS9hcGkvdjEvdXNlcnMSfwoKVXBkYXRlVXNlchIfLm1lbW9zLmFwaS52MS5VcGRhdGVVc2VyUmVxdWVzdBoSLm1lbW9zLmFwaS52MS5Vc2VyIjzaQRB1c2VyLHVwZGF0ZV9tYXNrgtPkkwIjOgR1c2VyMhsvYXBpL3YxL3t1c2VyLm5hbWU9dXNlcnMvKn0SbAoKRGVsZXRlVXNlchIfLm1lbW9zLmFwaS52MS5EZWxldGVVc2VyUmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSIl2kEEbmFtZYLT5JMCGCoWL2FwaS92MS97bmFtZT11c2Vycy8qfRJ+ChBMaXN0QWxsVXNlclN0YXRzEiUubWVtb3MuYXBpLnYxLkxpc3RBbGxVc2VyU3RhdHNSZXF1ZXN0GiYubWVtb3MuYXBpLnYxLkxpc3RBbGxVc2VyU3RhdHNSZXNwb25zZSIbgtPkkwIVEhMvYXBpL3YxL3VzZXJzOnN0YXRzEnoKDEdldFVzZXJTdGF0cxIhLm1lbW9zLmFwaS52MS5HZXRVc2VyU3RhdHNSZXF1ZXN0GhcubWVtb3MuYXBpLnYxLlVzZXJTdGF0cyIu2kEEbmFtZYLT5JMCIRIfL2FwaS92MS97bmFtZT11c2Vycy8qfTpnZXRTdGF0cxKCAQoOR2V0VXNlclNldHRpbmcSIy5tZW1vcy5hcGkudjEuR2V0VXNlclNldHRpbmdSZXF1ZXN0GhkubWVtb3MuYXBpLnYxLlVzZXJTZXR0aW5nIjDaQQRuYW1lgtPkkwIjEiEvYXBpL3YxL3tuYW1lPXVzZXJzLyovc2V0dGluZ3MvKn0SqAEKEVVwZGF0ZVVzZXJTZXR0aW5nEiYubWVtb3MuYXBpLnYxLlVwZGF0ZVVzZXJTZXR0aW5nUmVxdWVzdBoZLm1lbW9zLmFwaS52MS5Vc2VyU2V0dGluZyJQ2kETc2V0dGluZyx1cGRhdGVfbWFza4LT5JMCNDoHc2V0dGluZzIpL2FwaS92MS97c2V0dGluZy5uYW1lPXVzZXJzLyovc2V0dGluZ3MvKn0SlQEKEExpc3RVc2VyU2V0dGluZ3MSJS5tZW1vcy5hcGkudjEuTGlzdFVzZXJTZXR0aW5nc1JlcXVlc3QaJi5tZW1vcy5hcGkudjEuTGlzdFVzZXJTZXR0aW5nc1Jlc3BvbnNlIjLaQQZwYXJlbnSC0+STAiMSIS9hcGkvdjEve3BhcmVudD11c2Vycy8qfS9zZXR0aW5ncxK5AQoYTGlzdFBlcnNvbmFsQWNjZXNzVG9rZW5zEi0ubWVtb3MuYXBpLnYxLkxpc3RQZXJzb25hbEFjY2Vzc1Rva2Vuc1JlcXVlc3QaLi5tZW1vcy5hcGkudjEuTGlzdFBlcnNvbmFsQWNjZXNzVG9rZW5zUmVzcG9uc2UiPtpBBnBhcmVudILT5JMCLxItL2FwaS92MS97cGFyZW50PXVzZXJzLyp9L3BlcnNvbmFsQWNjZXNzVG9rZW5zErYBChlDcmVhdGVQZXJzb25hbEFjY2Vzc1Rva2VuEi4ubWVtb3MuYXBpLnYxLkNyZWF0ZVBlcnNvbmFsQWNjZXNzVG9rZW5SZXF1ZXN0Gi8ubWVtb3MuYXBpLnYxLkNyZWF0ZVBlcnNvbmFsQWNjZXNzVG9rZW5SZXNwb25zZSI4gtPkkwIyOgEqIi0vYXBpL3YxL3twYXJlbnQ9dXNlcnMvKn0vcGVyc29uYWxBY2Nlc3NUb2tlbnMSoQEKGURlbGV0ZVBlcnNvbmFsQWNjZXNzVG9rZW4SLi5tZW1vcy5hcGkudjEuRGVsZXRlUGVyc29uYWxBY2Nlc3NUb2tlblJlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkiPNpBBG5hbWWC0+STAi8qLS9hcGkvdjEve25hbWU9dXNlcnMvKi9wZXJzb25hbEFjY2Vzc1Rva2Vucy8qfRKVAQoQTGlzdFVzZXJXZWJob29rcxIlLm1lbW9zLmFwaS52MS5MaXN0VXNlcldlYmhvb2tzUmVxdWVzdBomLm1lbW9zLmFwaS52MS5MaXN0VXNlcldlYmhvb2tzUmVzcG9uc2UiMtpBBnBhcmVudILT5JMCIxIhL2FwaS92MS97cGFyZW50PXVzZXJzLyp9L3dlYmhvb2tzEpsBChFDcmVhdGVVc2VyV2ViaG9vaxImLm1lbW9zLmFwaS52MS5DcmVhdGVVc2VyV2ViaG9va1JlcXVlc3QaGS5tZW1vcy5hcGkudjEuVXNlcldlYmhvb2siQ9pBDnBhcmVudCx3ZWJob29rgtPkkwIsOgd3ZWJob29rIiEvYXBpL3YxL3twYXJlbnQ9dXNlcnMvKn0vd2ViaG9va3MSqAEKEVVwZGF0ZVVzZXJXZWJob29rEiYubWVtb3MuYXBpLnYxLlVwZGF0ZVVzZXJXZWJob29rUmVxdWVzdBoZLm1lbW9zLmFwaS52MS5Vc2VyV2ViaG9vayJQ2kETd2ViaG9vayx1cGRhdGVfbWFza4LT5JMCNDoHd2ViaG9vazIpL2FwaS92MS97d2ViaG9vay5uYW1lPXVzZXJzLyovd2ViaG9va3MvKn0ShQEKEURlbGV0ZVVzZXJXZWJob29rEiYubWVtb3MuYXBpLnYxLkRlbGV0ZVVzZXJXZWJob29rUmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSIw2kEEbmFtZYLT5JMCIyohL2FwaS92MS97bmFtZT11c2Vycy8qL3dlYmhvb2tzLyp9EqkBChVMaXN0VXNlck5vdGlmaWNhdGlvbnMSKi5tZW1vcy5hcGkudjEuTGlzdFVzZXJOb3RpZmljYXRpb25zUmVxdWVzdBorLm1lbW9zLmFwaS52MS5MaXN0VXNlck5vdGlmaWNhdGlvbnNSZXNwb25zZSI32kEGcGFyZW50gtPkkwIoEiYvYXBpL3YxL3twYXJlbnQ9dXNlcnMvKn0vbm90aWZpY2F0aW9ucxLLAQoWVXBkYXRlVXNlck5vdGlmaWNhdGlvbhIrLm1lbW9zLmFwaS52MS5VcGRhdGVVc2VyTm90aWZpY2F0aW9uUmVxdWVzdBoeLm1lbW9zLmFwaS52MS5Vc2VyTm90aWZpY2F0aW9uImTaQRhub3RpZmljYXRpb24sdXBkYXRlX21hc2uC0+STAkM6DG5vdGlmaWNhdGlvbjIzL2FwaS92MS97bm90aWZpY2F0aW9uLm5hbWU9dXNlcnMvKi9ub3RpZmljYXRpb25zLyp9EpQBChZEZWxldGVVc2VyTm90aWZpY2F0aW9uEisubWVtb3MuYXBpLnYxLkRlbGV0ZVVzZXJOb3RpZmljYXRpb25SZXF1ZXN0GhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5IjXaQQRuYW1lgtPkkwIoKiYvYXBpL3YxL3tuYW1lPXVzZXJzLyovbm90aWZpY2F0aW9ucy8qfUKoAQoQY29tLm1lbW9zLmFwaS52MUIQVXNlclNlcnZpY2VQcm90b1ABWjBnaXRodWIuY29tL3VzZW1lbW9zL21lbW9zL3Byb3RvL2dlbi9hcGkvdjE7YXBpdjGiAgNNQViqAgxNZW1vcy5BcGkuVjHKAgxNZW1vc1xBcGlcVjHiAhhNZW1vc1xBcGlcVjFcR1BCTWV0YWRhdGHqAg5NZW1vczo6QXBpOjpWMWIGcHJvdG8z\", [file_api_v1_common, file_google_api_annotations, file_google_api_client, file_google_api_field_behavior, file_google_api_resource, file_google_protobuf_empty, file_google_protobuf_field_mask, file_google_protobuf_timestamp]);\n\n/**\n * @generated from message memos.api.v1.User\n */\nexport type User = Message<\"memos.api.v1.User\"> & {\n  /**\n   * The resource name of the user.\n   * Format: users/{user}\n   *\n   * @generated from field: string name = 1;\n   */\n  name: string;\n\n  /**\n   * The role of the user.\n   *\n   * @generated from field: memos.api.v1.User.Role role = 2;\n   */\n  role: User_Role;\n\n  /**\n   * Required. The unique username for login.\n   *\n   * @generated from field: string username = 3;\n   */\n  username: string;\n\n  /**\n   * Optional. The email address of the user.\n   *\n   * @generated from field: string email = 4;\n   */\n  email: string;\n\n  /**\n   * Optional. The display name of the user.\n   *\n   * @generated from field: string display_name = 5;\n   */\n  displayName: string;\n\n  /**\n   * Optional. The avatar URL of the user.\n   *\n   * @generated from field: string avatar_url = 6;\n   */\n  avatarUrl: string;\n\n  /**\n   * Optional. The description of the user.\n   *\n   * @generated from field: string description = 7;\n   */\n  description: string;\n\n  /**\n   * Input only. The password for the user.\n   *\n   * @generated from field: string password = 8;\n   */\n  password: string;\n\n  /**\n   * The state of the user.\n   *\n   * @generated from field: memos.api.v1.State state = 9;\n   */\n  state: State;\n\n  /**\n   * Output only. The creation timestamp.\n   *\n   * @generated from field: google.protobuf.Timestamp create_time = 10;\n   */\n  createTime?: Timestamp;\n\n  /**\n   * Output only. The last update timestamp.\n   *\n   * @generated from field: google.protobuf.Timestamp update_time = 11;\n   */\n  updateTime?: Timestamp;\n};\n\n/**\n * Describes the message memos.api.v1.User.\n * Use `create(UserSchema)` to create a new message.\n */\nexport const UserSchema: GenMessage<User> = /*@__PURE__*/\n  messageDesc(file_api_v1_user_service, 0);\n\n/**\n * User role enumeration.\n *\n * @generated from enum memos.api.v1.User.Role\n */\nexport enum User_Role {\n  /**\n   * @generated from enum value: ROLE_UNSPECIFIED = 0;\n   */\n  ROLE_UNSPECIFIED = 0,\n\n  /**\n   * Admin role with system access.\n   *\n   * @generated from enum value: ADMIN = 2;\n   */\n  ADMIN = 2,\n\n  /**\n   * User role with limited access.\n   *\n   * @generated from enum value: USER = 3;\n   */\n  USER = 3,\n}\n\n/**\n * Describes the enum memos.api.v1.User.Role.\n */\nexport const User_RoleSchema: GenEnum<User_Role> = /*@__PURE__*/\n  enumDesc(file_api_v1_user_service, 0, 0);\n\n/**\n * @generated from message memos.api.v1.ListUsersRequest\n */\nexport type ListUsersRequest = Message<\"memos.api.v1.ListUsersRequest\"> & {\n  /**\n   * Optional. The maximum number of users to return.\n   * The service may return fewer than this value.\n   * If unspecified, at most 50 users will be returned.\n   * The maximum value is 1000; values above 1000 will be coerced to 1000.\n   *\n   * @generated from field: int32 page_size = 1;\n   */\n  pageSize: number;\n\n  /**\n   * Optional. A page token, received from a previous `ListUsers` call.\n   * Provide this to retrieve the subsequent page.\n   *\n   * @generated from field: string page_token = 2;\n   */\n  pageToken: string;\n\n  /**\n   * Optional. Filter to apply to the list results.\n   * Example: \"username == 'steven'\"\n   * Supported operators: ==\n   * Supported fields: username\n   *\n   * @generated from field: string filter = 3;\n   */\n  filter: string;\n\n  /**\n   * Optional. If true, show deleted users in the response.\n   *\n   * @generated from field: bool show_deleted = 4;\n   */\n  showDeleted: boolean;\n};\n\n/**\n * Describes the message memos.api.v1.ListUsersRequest.\n * Use `create(ListUsersRequestSchema)` to create a new message.\n */\nexport const ListUsersRequestSchema: GenMessage<ListUsersRequest> = /*@__PURE__*/\n  messageDesc(file_api_v1_user_service, 1);\n\n/**\n * @generated from message memos.api.v1.ListUsersResponse\n */\nexport type ListUsersResponse = Message<\"memos.api.v1.ListUsersResponse\"> & {\n  /**\n   * The list of users.\n   *\n   * @generated from field: repeated memos.api.v1.User users = 1;\n   */\n  users: User[];\n\n  /**\n   * A token that can be sent as `page_token` to retrieve the next page.\n   * If this field is omitted, there are no subsequent pages.\n   *\n   * @generated from field: string next_page_token = 2;\n   */\n  nextPageToken: string;\n\n  /**\n   * The total count of users (may be approximate).\n   *\n   * @generated from field: int32 total_size = 3;\n   */\n  totalSize: number;\n};\n\n/**\n * Describes the message memos.api.v1.ListUsersResponse.\n * Use `create(ListUsersResponseSchema)` to create a new message.\n */\nexport const ListUsersResponseSchema: GenMessage<ListUsersResponse> = /*@__PURE__*/\n  messageDesc(file_api_v1_user_service, 2);\n\n/**\n * @generated from message memos.api.v1.GetUserRequest\n */\nexport type GetUserRequest = Message<\"memos.api.v1.GetUserRequest\"> & {\n  /**\n   * Required. The resource name of the user.\n   * Supports both numeric IDs and username strings:\n   *   - users/{id}       (e.g., users/101)\n   *   - users/{username} (e.g., users/steven)\n   * Format: users/{id_or_username}\n   *\n   * @generated from field: string name = 1;\n   */\n  name: string;\n\n  /**\n   * Optional. The fields to return in the response.\n   * If not specified, all fields are returned.\n   *\n   * @generated from field: google.protobuf.FieldMask read_mask = 2;\n   */\n  readMask?: FieldMask;\n};\n\n/**\n * Describes the message memos.api.v1.GetUserRequest.\n * Use `create(GetUserRequestSchema)` to create a new message.\n */\nexport const GetUserRequestSchema: GenMessage<GetUserRequest> = /*@__PURE__*/\n  messageDesc(file_api_v1_user_service, 3);\n\n/**\n * @generated from message memos.api.v1.CreateUserRequest\n */\nexport type CreateUserRequest = Message<\"memos.api.v1.CreateUserRequest\"> & {\n  /**\n   * Required. The user to create.\n   *\n   * @generated from field: memos.api.v1.User user = 1;\n   */\n  user?: User;\n\n  /**\n   * Optional. The user ID to use for this user.\n   * If empty, a unique ID will be generated.\n   * Must match the pattern [a-z0-9-]+\n   *\n   * @generated from field: string user_id = 2;\n   */\n  userId: string;\n\n  /**\n   * Optional. If set, validate the request but don't actually create the user.\n   *\n   * @generated from field: bool validate_only = 3;\n   */\n  validateOnly: boolean;\n\n  /**\n   * Optional. An idempotency token that can be used to ensure that multiple\n   * requests to create a user have the same result.\n   *\n   * @generated from field: string request_id = 4;\n   */\n  requestId: string;\n};\n\n/**\n * Describes the message memos.api.v1.CreateUserRequest.\n * Use `create(CreateUserRequestSchema)` to create a new message.\n */\nexport const CreateUserRequestSchema: GenMessage<CreateUserRequest> = /*@__PURE__*/\n  messageDesc(file_api_v1_user_service, 4);\n\n/**\n * @generated from message memos.api.v1.UpdateUserRequest\n */\nexport type UpdateUserRequest = Message<\"memos.api.v1.UpdateUserRequest\"> & {\n  /**\n   * Required. The user to update.\n   *\n   * @generated from field: memos.api.v1.User user = 1;\n   */\n  user?: User;\n\n  /**\n   * Required. The list of fields to update.\n   *\n   * @generated from field: google.protobuf.FieldMask update_mask = 2;\n   */\n  updateMask?: FieldMask;\n\n  /**\n   * Optional. If set to true, allows updating sensitive fields.\n   *\n   * @generated from field: bool allow_missing = 3;\n   */\n  allowMissing: boolean;\n};\n\n/**\n * Describes the message memos.api.v1.UpdateUserRequest.\n * Use `create(UpdateUserRequestSchema)` to create a new message.\n */\nexport const UpdateUserRequestSchema: GenMessage<UpdateUserRequest> = /*@__PURE__*/\n  messageDesc(file_api_v1_user_service, 5);\n\n/**\n * @generated from message memos.api.v1.DeleteUserRequest\n */\nexport type DeleteUserRequest = Message<\"memos.api.v1.DeleteUserRequest\"> & {\n  /**\n   * Required. The resource name of the user to delete.\n   * Format: users/{user}\n   *\n   * @generated from field: string name = 1;\n   */\n  name: string;\n\n  /**\n   * Optional. If set to true, the user will be deleted even if they have associated data.\n   *\n   * @generated from field: bool force = 2;\n   */\n  force: boolean;\n};\n\n/**\n * Describes the message memos.api.v1.DeleteUserRequest.\n * Use `create(DeleteUserRequestSchema)` to create a new message.\n */\nexport const DeleteUserRequestSchema: GenMessage<DeleteUserRequest> = /*@__PURE__*/\n  messageDesc(file_api_v1_user_service, 6);\n\n/**\n * User statistics messages\n *\n * @generated from message memos.api.v1.UserStats\n */\nexport type UserStats = Message<\"memos.api.v1.UserStats\"> & {\n  /**\n   * The resource name of the user whose stats these are.\n   * Format: users/{user}\n   *\n   * @generated from field: string name = 1;\n   */\n  name: string;\n\n  /**\n   * The timestamps when the memos were displayed.\n   *\n   * @generated from field: repeated google.protobuf.Timestamp memo_display_timestamps = 2;\n   */\n  memoDisplayTimestamps: Timestamp[];\n\n  /**\n   * The stats of memo types.\n   *\n   * @generated from field: memos.api.v1.UserStats.MemoTypeStats memo_type_stats = 3;\n   */\n  memoTypeStats?: UserStats_MemoTypeStats;\n\n  /**\n   * The count of tags.\n   *\n   * @generated from field: map<string, int32> tag_count = 4;\n   */\n  tagCount: { [key: string]: number };\n\n  /**\n   * The pinned memos of the user.\n   *\n   * @generated from field: repeated string pinned_memos = 5;\n   */\n  pinnedMemos: string[];\n\n  /**\n   * Total memo count.\n   *\n   * @generated from field: int32 total_memo_count = 6;\n   */\n  totalMemoCount: number;\n};\n\n/**\n * Describes the message memos.api.v1.UserStats.\n * Use `create(UserStatsSchema)` to create a new message.\n */\nexport const UserStatsSchema: GenMessage<UserStats> = /*@__PURE__*/\n  messageDesc(file_api_v1_user_service, 7);\n\n/**\n * Memo type statistics.\n *\n * @generated from message memos.api.v1.UserStats.MemoTypeStats\n */\nexport type UserStats_MemoTypeStats = Message<\"memos.api.v1.UserStats.MemoTypeStats\"> & {\n  /**\n   * @generated from field: int32 link_count = 1;\n   */\n  linkCount: number;\n\n  /**\n   * @generated from field: int32 code_count = 2;\n   */\n  codeCount: number;\n\n  /**\n   * @generated from field: int32 todo_count = 3;\n   */\n  todoCount: number;\n\n  /**\n   * @generated from field: int32 undo_count = 4;\n   */\n  undoCount: number;\n};\n\n/**\n * Describes the message memos.api.v1.UserStats.MemoTypeStats.\n * Use `create(UserStats_MemoTypeStatsSchema)` to create a new message.\n */\nexport const UserStats_MemoTypeStatsSchema: GenMessage<UserStats_MemoTypeStats> = /*@__PURE__*/\n  messageDesc(file_api_v1_user_service, 7, 0);\n\n/**\n * @generated from message memos.api.v1.GetUserStatsRequest\n */\nexport type GetUserStatsRequest = Message<\"memos.api.v1.GetUserStatsRequest\"> & {\n  /**\n   * Required. The resource name of the user.\n   * Format: users/{user}\n   *\n   * @generated from field: string name = 1;\n   */\n  name: string;\n};\n\n/**\n * Describes the message memos.api.v1.GetUserStatsRequest.\n * Use `create(GetUserStatsRequestSchema)` to create a new message.\n */\nexport const GetUserStatsRequestSchema: GenMessage<GetUserStatsRequest> = /*@__PURE__*/\n  messageDesc(file_api_v1_user_service, 8);\n\n/**\n * This endpoint doesn't take any parameters.\n *\n * @generated from message memos.api.v1.ListAllUserStatsRequest\n */\nexport type ListAllUserStatsRequest = Message<\"memos.api.v1.ListAllUserStatsRequest\"> & {\n};\n\n/**\n * Describes the message memos.api.v1.ListAllUserStatsRequest.\n * Use `create(ListAllUserStatsRequestSchema)` to create a new message.\n */\nexport const ListAllUserStatsRequestSchema: GenMessage<ListAllUserStatsRequest> = /*@__PURE__*/\n  messageDesc(file_api_v1_user_service, 9);\n\n/**\n * @generated from message memos.api.v1.ListAllUserStatsResponse\n */\nexport type ListAllUserStatsResponse = Message<\"memos.api.v1.ListAllUserStatsResponse\"> & {\n  /**\n   * The list of user statistics.\n   *\n   * @generated from field: repeated memos.api.v1.UserStats stats = 1;\n   */\n  stats: UserStats[];\n};\n\n/**\n * Describes the message memos.api.v1.ListAllUserStatsResponse.\n * Use `create(ListAllUserStatsResponseSchema)` to create a new message.\n */\nexport const ListAllUserStatsResponseSchema: GenMessage<ListAllUserStatsResponse> = /*@__PURE__*/\n  messageDesc(file_api_v1_user_service, 10);\n\n/**\n * User settings message\n *\n * @generated from message memos.api.v1.UserSetting\n */\nexport type UserSetting = Message<\"memos.api.v1.UserSetting\"> & {\n  /**\n   * The name of the user setting.\n   * Format: users/{user}/settings/{setting}, {setting} is the key for the setting.\n   * For example, \"users/123/settings/GENERAL\" for general settings.\n   *\n   * @generated from field: string name = 1;\n   */\n  name: string;\n\n  /**\n   * @generated from oneof memos.api.v1.UserSetting.value\n   */\n  value: {\n    /**\n     * @generated from field: memos.api.v1.UserSetting.GeneralSetting general_setting = 2;\n     */\n    value: UserSetting_GeneralSetting;\n    case: \"generalSetting\";\n  } | {\n    /**\n     * @generated from field: memos.api.v1.UserSetting.WebhooksSetting webhooks_setting = 5;\n     */\n    value: UserSetting_WebhooksSetting;\n    case: \"webhooksSetting\";\n  } | { case: undefined; value?: undefined };\n};\n\n/**\n * Describes the message memos.api.v1.UserSetting.\n * Use `create(UserSettingSchema)` to create a new message.\n */\nexport const UserSettingSchema: GenMessage<UserSetting> = /*@__PURE__*/\n  messageDesc(file_api_v1_user_service, 11);\n\n/**\n * General user settings configuration.\n *\n * @generated from message memos.api.v1.UserSetting.GeneralSetting\n */\nexport type UserSetting_GeneralSetting = Message<\"memos.api.v1.UserSetting.GeneralSetting\"> & {\n  /**\n   * The preferred locale of the user.\n   *\n   * @generated from field: string locale = 1;\n   */\n  locale: string;\n\n  /**\n   * The default visibility of the memo.\n   *\n   * @generated from field: string memo_visibility = 3;\n   */\n  memoVisibility: string;\n\n  /**\n   * The preferred theme of the user.\n   * This references a CSS file in the web/public/themes/ directory.\n   * If not set, the default theme will be used.\n   *\n   * @generated from field: string theme = 4;\n   */\n  theme: string;\n};\n\n/**\n * Describes the message memos.api.v1.UserSetting.GeneralSetting.\n * Use `create(UserSetting_GeneralSettingSchema)` to create a new message.\n */\nexport const UserSetting_GeneralSettingSchema: GenMessage<UserSetting_GeneralSetting> = /*@__PURE__*/\n  messageDesc(file_api_v1_user_service, 11, 0);\n\n/**\n * User webhooks configuration.\n *\n * @generated from message memos.api.v1.UserSetting.WebhooksSetting\n */\nexport type UserSetting_WebhooksSetting = Message<\"memos.api.v1.UserSetting.WebhooksSetting\"> & {\n  /**\n   * List of user webhooks.\n   *\n   * @generated from field: repeated memos.api.v1.UserWebhook webhooks = 1;\n   */\n  webhooks: UserWebhook[];\n};\n\n/**\n * Describes the message memos.api.v1.UserSetting.WebhooksSetting.\n * Use `create(UserSetting_WebhooksSettingSchema)` to create a new message.\n */\nexport const UserSetting_WebhooksSettingSchema: GenMessage<UserSetting_WebhooksSetting> = /*@__PURE__*/\n  messageDesc(file_api_v1_user_service, 11, 1);\n\n/**\n * Enumeration of user setting keys.\n *\n * @generated from enum memos.api.v1.UserSetting.Key\n */\nexport enum UserSetting_Key {\n  /**\n   * @generated from enum value: KEY_UNSPECIFIED = 0;\n   */\n  KEY_UNSPECIFIED = 0,\n\n  /**\n   * GENERAL is the key for general user settings.\n   *\n   * @generated from enum value: GENERAL = 1;\n   */\n  GENERAL = 1,\n\n  /**\n   * WEBHOOKS is the key for user webhooks.\n   *\n   * @generated from enum value: WEBHOOKS = 4;\n   */\n  WEBHOOKS = 4,\n}\n\n/**\n * Describes the enum memos.api.v1.UserSetting.Key.\n */\nexport const UserSetting_KeySchema: GenEnum<UserSetting_Key> = /*@__PURE__*/\n  enumDesc(file_api_v1_user_service, 11, 0);\n\n/**\n * @generated from message memos.api.v1.GetUserSettingRequest\n */\nexport type GetUserSettingRequest = Message<\"memos.api.v1.GetUserSettingRequest\"> & {\n  /**\n   * Required. The resource name of the user setting.\n   * Format: users/{user}/settings/{setting}\n   *\n   * @generated from field: string name = 1;\n   */\n  name: string;\n};\n\n/**\n * Describes the message memos.api.v1.GetUserSettingRequest.\n * Use `create(GetUserSettingRequestSchema)` to create a new message.\n */\nexport const GetUserSettingRequestSchema: GenMessage<GetUserSettingRequest> = /*@__PURE__*/\n  messageDesc(file_api_v1_user_service, 12);\n\n/**\n * @generated from message memos.api.v1.UpdateUserSettingRequest\n */\nexport type UpdateUserSettingRequest = Message<\"memos.api.v1.UpdateUserSettingRequest\"> & {\n  /**\n   * Required. The user setting to update.\n   *\n   * @generated from field: memos.api.v1.UserSetting setting = 1;\n   */\n  setting?: UserSetting;\n\n  /**\n   * Required. The list of fields to update.\n   *\n   * @generated from field: google.protobuf.FieldMask update_mask = 2;\n   */\n  updateMask?: FieldMask;\n};\n\n/**\n * Describes the message memos.api.v1.UpdateUserSettingRequest.\n * Use `create(UpdateUserSettingRequestSchema)` to create a new message.\n */\nexport const UpdateUserSettingRequestSchema: GenMessage<UpdateUserSettingRequest> = /*@__PURE__*/\n  messageDesc(file_api_v1_user_service, 13);\n\n/**\n * Request message for ListUserSettings method.\n *\n * @generated from message memos.api.v1.ListUserSettingsRequest\n */\nexport type ListUserSettingsRequest = Message<\"memos.api.v1.ListUserSettingsRequest\"> & {\n  /**\n   * Required. The parent resource whose settings will be listed.\n   * Format: users/{user}\n   *\n   * @generated from field: string parent = 1;\n   */\n  parent: string;\n\n  /**\n   * Optional. The maximum number of settings to return.\n   * The service may return fewer than this value.\n   * If unspecified, at most 50 settings will be returned.\n   * The maximum value is 1000; values above 1000 will be coerced to 1000.\n   *\n   * @generated from field: int32 page_size = 2;\n   */\n  pageSize: number;\n\n  /**\n   * Optional. A page token, received from a previous `ListUserSettings` call.\n   * Provide this to retrieve the subsequent page.\n   *\n   * @generated from field: string page_token = 3;\n   */\n  pageToken: string;\n};\n\n/**\n * Describes the message memos.api.v1.ListUserSettingsRequest.\n * Use `create(ListUserSettingsRequestSchema)` to create a new message.\n */\nexport const ListUserSettingsRequestSchema: GenMessage<ListUserSettingsRequest> = /*@__PURE__*/\n  messageDesc(file_api_v1_user_service, 14);\n\n/**\n * Response message for ListUserSettings method.\n *\n * @generated from message memos.api.v1.ListUserSettingsResponse\n */\nexport type ListUserSettingsResponse = Message<\"memos.api.v1.ListUserSettingsResponse\"> & {\n  /**\n   * The list of user settings.\n   *\n   * @generated from field: repeated memos.api.v1.UserSetting settings = 1;\n   */\n  settings: UserSetting[];\n\n  /**\n   * A token that can be sent as `page_token` to retrieve the next page.\n   * If this field is omitted, there are no subsequent pages.\n   *\n   * @generated from field: string next_page_token = 2;\n   */\n  nextPageToken: string;\n\n  /**\n   * The total count of settings (may be approximate).\n   *\n   * @generated from field: int32 total_size = 3;\n   */\n  totalSize: number;\n};\n\n/**\n * Describes the message memos.api.v1.ListUserSettingsResponse.\n * Use `create(ListUserSettingsResponseSchema)` to create a new message.\n */\nexport const ListUserSettingsResponseSchema: GenMessage<ListUserSettingsResponse> = /*@__PURE__*/\n  messageDesc(file_api_v1_user_service, 15);\n\n/**\n * PersonalAccessToken represents a long-lived token for API/script access.\n * PATs are distinct from short-lived JWT access tokens used for session authentication.\n *\n * @generated from message memos.api.v1.PersonalAccessToken\n */\nexport type PersonalAccessToken = Message<\"memos.api.v1.PersonalAccessToken\"> & {\n  /**\n   * The resource name of the personal access token.\n   * Format: users/{user}/personalAccessTokens/{personal_access_token}\n   *\n   * @generated from field: string name = 1;\n   */\n  name: string;\n\n  /**\n   * The description of the token.\n   *\n   * @generated from field: string description = 2;\n   */\n  description: string;\n\n  /**\n   * Output only. The creation timestamp.\n   *\n   * @generated from field: google.protobuf.Timestamp created_at = 3;\n   */\n  createdAt?: Timestamp;\n\n  /**\n   * Optional. The expiration timestamp.\n   *\n   * @generated from field: google.protobuf.Timestamp expires_at = 4;\n   */\n  expiresAt?: Timestamp;\n\n  /**\n   * Output only. The last used timestamp.\n   *\n   * @generated from field: google.protobuf.Timestamp last_used_at = 5;\n   */\n  lastUsedAt?: Timestamp;\n};\n\n/**\n * Describes the message memos.api.v1.PersonalAccessToken.\n * Use `create(PersonalAccessTokenSchema)` to create a new message.\n */\nexport const PersonalAccessTokenSchema: GenMessage<PersonalAccessToken> = /*@__PURE__*/\n  messageDesc(file_api_v1_user_service, 16);\n\n/**\n * @generated from message memos.api.v1.ListPersonalAccessTokensRequest\n */\nexport type ListPersonalAccessTokensRequest = Message<\"memos.api.v1.ListPersonalAccessTokensRequest\"> & {\n  /**\n   * Required. The parent resource whose personal access tokens will be listed.\n   * Format: users/{user}\n   *\n   * @generated from field: string parent = 1;\n   */\n  parent: string;\n\n  /**\n   * Optional. The maximum number of tokens to return.\n   *\n   * @generated from field: int32 page_size = 2;\n   */\n  pageSize: number;\n\n  /**\n   * Optional. A page token for pagination.\n   *\n   * @generated from field: string page_token = 3;\n   */\n  pageToken: string;\n};\n\n/**\n * Describes the message memos.api.v1.ListPersonalAccessTokensRequest.\n * Use `create(ListPersonalAccessTokensRequestSchema)` to create a new message.\n */\nexport const ListPersonalAccessTokensRequestSchema: GenMessage<ListPersonalAccessTokensRequest> = /*@__PURE__*/\n  messageDesc(file_api_v1_user_service, 17);\n\n/**\n * @generated from message memos.api.v1.ListPersonalAccessTokensResponse\n */\nexport type ListPersonalAccessTokensResponse = Message<\"memos.api.v1.ListPersonalAccessTokensResponse\"> & {\n  /**\n   * The list of personal access tokens.\n   *\n   * @generated from field: repeated memos.api.v1.PersonalAccessToken personal_access_tokens = 1;\n   */\n  personalAccessTokens: PersonalAccessToken[];\n\n  /**\n   * A token for the next page of results.\n   *\n   * @generated from field: string next_page_token = 2;\n   */\n  nextPageToken: string;\n\n  /**\n   * The total count of personal access tokens.\n   *\n   * @generated from field: int32 total_size = 3;\n   */\n  totalSize: number;\n};\n\n/**\n * Describes the message memos.api.v1.ListPersonalAccessTokensResponse.\n * Use `create(ListPersonalAccessTokensResponseSchema)` to create a new message.\n */\nexport const ListPersonalAccessTokensResponseSchema: GenMessage<ListPersonalAccessTokensResponse> = /*@__PURE__*/\n  messageDesc(file_api_v1_user_service, 18);\n\n/**\n * @generated from message memos.api.v1.CreatePersonalAccessTokenRequest\n */\nexport type CreatePersonalAccessTokenRequest = Message<\"memos.api.v1.CreatePersonalAccessTokenRequest\"> & {\n  /**\n   * Required. The parent resource where this token will be created.\n   * Format: users/{user}\n   *\n   * @generated from field: string parent = 1;\n   */\n  parent: string;\n\n  /**\n   * Optional. Description of the personal access token.\n   *\n   * @generated from field: string description = 2;\n   */\n  description: string;\n\n  /**\n   * Optional. Expiration duration in days (0 = never expires).\n   *\n   * @generated from field: int32 expires_in_days = 3;\n   */\n  expiresInDays: number;\n};\n\n/**\n * Describes the message memos.api.v1.CreatePersonalAccessTokenRequest.\n * Use `create(CreatePersonalAccessTokenRequestSchema)` to create a new message.\n */\nexport const CreatePersonalAccessTokenRequestSchema: GenMessage<CreatePersonalAccessTokenRequest> = /*@__PURE__*/\n  messageDesc(file_api_v1_user_service, 19);\n\n/**\n * @generated from message memos.api.v1.CreatePersonalAccessTokenResponse\n */\nexport type CreatePersonalAccessTokenResponse = Message<\"memos.api.v1.CreatePersonalAccessTokenResponse\"> & {\n  /**\n   * The personal access token metadata.\n   *\n   * @generated from field: memos.api.v1.PersonalAccessToken personal_access_token = 1;\n   */\n  personalAccessToken?: PersonalAccessToken;\n\n  /**\n   * The actual token value - only returned on creation.\n   * This is the only time the token value will be visible.\n   *\n   * @generated from field: string token = 2;\n   */\n  token: string;\n};\n\n/**\n * Describes the message memos.api.v1.CreatePersonalAccessTokenResponse.\n * Use `create(CreatePersonalAccessTokenResponseSchema)` to create a new message.\n */\nexport const CreatePersonalAccessTokenResponseSchema: GenMessage<CreatePersonalAccessTokenResponse> = /*@__PURE__*/\n  messageDesc(file_api_v1_user_service, 20);\n\n/**\n * @generated from message memos.api.v1.DeletePersonalAccessTokenRequest\n */\nexport type DeletePersonalAccessTokenRequest = Message<\"memos.api.v1.DeletePersonalAccessTokenRequest\"> & {\n  /**\n   * Required. The resource name of the personal access token to delete.\n   * Format: users/{user}/personalAccessTokens/{personal_access_token}\n   *\n   * @generated from field: string name = 1;\n   */\n  name: string;\n};\n\n/**\n * Describes the message memos.api.v1.DeletePersonalAccessTokenRequest.\n * Use `create(DeletePersonalAccessTokenRequestSchema)` to create a new message.\n */\nexport const DeletePersonalAccessTokenRequestSchema: GenMessage<DeletePersonalAccessTokenRequest> = /*@__PURE__*/\n  messageDesc(file_api_v1_user_service, 21);\n\n/**\n * UserWebhook represents a webhook owned by a user.\n *\n * @generated from message memos.api.v1.UserWebhook\n */\nexport type UserWebhook = Message<\"memos.api.v1.UserWebhook\"> & {\n  /**\n   * The name of the webhook.\n   * Format: users/{user}/webhooks/{webhook}\n   *\n   * @generated from field: string name = 1;\n   */\n  name: string;\n\n  /**\n   * The URL to send the webhook to.\n   *\n   * @generated from field: string url = 2;\n   */\n  url: string;\n\n  /**\n   * Optional. Human-readable name for the webhook.\n   *\n   * @generated from field: string display_name = 3;\n   */\n  displayName: string;\n\n  /**\n   * The creation time of the webhook.\n   *\n   * @generated from field: google.protobuf.Timestamp create_time = 4;\n   */\n  createTime?: Timestamp;\n\n  /**\n   * The last update time of the webhook.\n   *\n   * @generated from field: google.protobuf.Timestamp update_time = 5;\n   */\n  updateTime?: Timestamp;\n};\n\n/**\n * Describes the message memos.api.v1.UserWebhook.\n * Use `create(UserWebhookSchema)` to create a new message.\n */\nexport const UserWebhookSchema: GenMessage<UserWebhook> = /*@__PURE__*/\n  messageDesc(file_api_v1_user_service, 22);\n\n/**\n * @generated from message memos.api.v1.ListUserWebhooksRequest\n */\nexport type ListUserWebhooksRequest = Message<\"memos.api.v1.ListUserWebhooksRequest\"> & {\n  /**\n   * The parent user resource.\n   * Format: users/{user}\n   *\n   * @generated from field: string parent = 1;\n   */\n  parent: string;\n};\n\n/**\n * Describes the message memos.api.v1.ListUserWebhooksRequest.\n * Use `create(ListUserWebhooksRequestSchema)` to create a new message.\n */\nexport const ListUserWebhooksRequestSchema: GenMessage<ListUserWebhooksRequest> = /*@__PURE__*/\n  messageDesc(file_api_v1_user_service, 23);\n\n/**\n * @generated from message memos.api.v1.ListUserWebhooksResponse\n */\nexport type ListUserWebhooksResponse = Message<\"memos.api.v1.ListUserWebhooksResponse\"> & {\n  /**\n   * The list of webhooks.\n   *\n   * @generated from field: repeated memos.api.v1.UserWebhook webhooks = 1;\n   */\n  webhooks: UserWebhook[];\n};\n\n/**\n * Describes the message memos.api.v1.ListUserWebhooksResponse.\n * Use `create(ListUserWebhooksResponseSchema)` to create a new message.\n */\nexport const ListUserWebhooksResponseSchema: GenMessage<ListUserWebhooksResponse> = /*@__PURE__*/\n  messageDesc(file_api_v1_user_service, 24);\n\n/**\n * @generated from message memos.api.v1.CreateUserWebhookRequest\n */\nexport type CreateUserWebhookRequest = Message<\"memos.api.v1.CreateUserWebhookRequest\"> & {\n  /**\n   * The parent user resource.\n   * Format: users/{user}\n   *\n   * @generated from field: string parent = 1;\n   */\n  parent: string;\n\n  /**\n   * The webhook to create.\n   *\n   * @generated from field: memos.api.v1.UserWebhook webhook = 2;\n   */\n  webhook?: UserWebhook;\n};\n\n/**\n * Describes the message memos.api.v1.CreateUserWebhookRequest.\n * Use `create(CreateUserWebhookRequestSchema)` to create a new message.\n */\nexport const CreateUserWebhookRequestSchema: GenMessage<CreateUserWebhookRequest> = /*@__PURE__*/\n  messageDesc(file_api_v1_user_service, 25);\n\n/**\n * @generated from message memos.api.v1.UpdateUserWebhookRequest\n */\nexport type UpdateUserWebhookRequest = Message<\"memos.api.v1.UpdateUserWebhookRequest\"> & {\n  /**\n   * The webhook to update.\n   *\n   * @generated from field: memos.api.v1.UserWebhook webhook = 1;\n   */\n  webhook?: UserWebhook;\n\n  /**\n   * The list of fields to update.\n   *\n   * @generated from field: google.protobuf.FieldMask update_mask = 2;\n   */\n  updateMask?: FieldMask;\n};\n\n/**\n * Describes the message memos.api.v1.UpdateUserWebhookRequest.\n * Use `create(UpdateUserWebhookRequestSchema)` to create a new message.\n */\nexport const UpdateUserWebhookRequestSchema: GenMessage<UpdateUserWebhookRequest> = /*@__PURE__*/\n  messageDesc(file_api_v1_user_service, 26);\n\n/**\n * @generated from message memos.api.v1.DeleteUserWebhookRequest\n */\nexport type DeleteUserWebhookRequest = Message<\"memos.api.v1.DeleteUserWebhookRequest\"> & {\n  /**\n   * The name of the webhook to delete.\n   * Format: users/{user}/webhooks/{webhook}\n   *\n   * @generated from field: string name = 1;\n   */\n  name: string;\n};\n\n/**\n * Describes the message memos.api.v1.DeleteUserWebhookRequest.\n * Use `create(DeleteUserWebhookRequestSchema)` to create a new message.\n */\nexport const DeleteUserWebhookRequestSchema: GenMessage<DeleteUserWebhookRequest> = /*@__PURE__*/\n  messageDesc(file_api_v1_user_service, 27);\n\n/**\n * @generated from message memos.api.v1.UserNotification\n */\nexport type UserNotification = Message<\"memos.api.v1.UserNotification\"> & {\n  /**\n   * The resource name of the notification.\n   * Format: users/{user}/notifications/{notification}\n   *\n   * @generated from field: string name = 1;\n   */\n  name: string;\n\n  /**\n   * The sender of the notification.\n   * Format: users/{user}\n   *\n   * @generated from field: string sender = 2;\n   */\n  sender: string;\n\n  /**\n   * The status of the notification.\n   *\n   * @generated from field: memos.api.v1.UserNotification.Status status = 3;\n   */\n  status: UserNotification_Status;\n\n  /**\n   * The creation timestamp.\n   *\n   * @generated from field: google.protobuf.Timestamp create_time = 4;\n   */\n  createTime?: Timestamp;\n\n  /**\n   * The type of the notification.\n   *\n   * @generated from field: memos.api.v1.UserNotification.Type type = 5;\n   */\n  type: UserNotification_Type;\n\n  /**\n   * @generated from oneof memos.api.v1.UserNotification.payload\n   */\n  payload: {\n    /**\n     * @generated from field: memos.api.v1.UserNotification.MemoCommentPayload memo_comment = 6;\n     */\n    value: UserNotification_MemoCommentPayload;\n    case: \"memoComment\";\n  } | { case: undefined; value?: undefined };\n};\n\n/**\n * Describes the message memos.api.v1.UserNotification.\n * Use `create(UserNotificationSchema)` to create a new message.\n */\nexport const UserNotificationSchema: GenMessage<UserNotification> = /*@__PURE__*/\n  messageDesc(file_api_v1_user_service, 28);\n\n/**\n * @generated from message memos.api.v1.UserNotification.MemoCommentPayload\n */\nexport type UserNotification_MemoCommentPayload = Message<\"memos.api.v1.UserNotification.MemoCommentPayload\"> & {\n  /**\n   * The memo name of comment.\n   * Format: memos/{memo}\n   *\n   * @generated from field: string memo = 1;\n   */\n  memo: string;\n\n  /**\n   * The name of related memo.\n   * Format: memos/{memo}\n   *\n   * @generated from field: string related_memo = 2;\n   */\n  relatedMemo: string;\n};\n\n/**\n * Describes the message memos.api.v1.UserNotification.MemoCommentPayload.\n * Use `create(UserNotification_MemoCommentPayloadSchema)` to create a new message.\n */\nexport const UserNotification_MemoCommentPayloadSchema: GenMessage<UserNotification_MemoCommentPayload> = /*@__PURE__*/\n  messageDesc(file_api_v1_user_service, 28, 0);\n\n/**\n * @generated from enum memos.api.v1.UserNotification.Status\n */\nexport enum UserNotification_Status {\n  /**\n   * @generated from enum value: STATUS_UNSPECIFIED = 0;\n   */\n  STATUS_UNSPECIFIED = 0,\n\n  /**\n   * @generated from enum value: UNREAD = 1;\n   */\n  UNREAD = 1,\n\n  /**\n   * @generated from enum value: ARCHIVED = 2;\n   */\n  ARCHIVED = 2,\n}\n\n/**\n * Describes the enum memos.api.v1.UserNotification.Status.\n */\nexport const UserNotification_StatusSchema: GenEnum<UserNotification_Status> = /*@__PURE__*/\n  enumDesc(file_api_v1_user_service, 28, 0);\n\n/**\n * @generated from enum memos.api.v1.UserNotification.Type\n */\nexport enum UserNotification_Type {\n  /**\n   * @generated from enum value: TYPE_UNSPECIFIED = 0;\n   */\n  TYPE_UNSPECIFIED = 0,\n\n  /**\n   * @generated from enum value: MEMO_COMMENT = 1;\n   */\n  MEMO_COMMENT = 1,\n}\n\n/**\n * Describes the enum memos.api.v1.UserNotification.Type.\n */\nexport const UserNotification_TypeSchema: GenEnum<UserNotification_Type> = /*@__PURE__*/\n  enumDesc(file_api_v1_user_service, 28, 1);\n\n/**\n * @generated from message memos.api.v1.ListUserNotificationsRequest\n */\nexport type ListUserNotificationsRequest = Message<\"memos.api.v1.ListUserNotificationsRequest\"> & {\n  /**\n   * The parent user resource.\n   * Format: users/{user}\n   *\n   * @generated from field: string parent = 1;\n   */\n  parent: string;\n\n  /**\n   * @generated from field: int32 page_size = 2;\n   */\n  pageSize: number;\n\n  /**\n   * @generated from field: string page_token = 3;\n   */\n  pageToken: string;\n\n  /**\n   * @generated from field: string filter = 4;\n   */\n  filter: string;\n};\n\n/**\n * Describes the message memos.api.v1.ListUserNotificationsRequest.\n * Use `create(ListUserNotificationsRequestSchema)` to create a new message.\n */\nexport const ListUserNotificationsRequestSchema: GenMessage<ListUserNotificationsRequest> = /*@__PURE__*/\n  messageDesc(file_api_v1_user_service, 29);\n\n/**\n * @generated from message memos.api.v1.ListUserNotificationsResponse\n */\nexport type ListUserNotificationsResponse = Message<\"memos.api.v1.ListUserNotificationsResponse\"> & {\n  /**\n   * @generated from field: repeated memos.api.v1.UserNotification notifications = 1;\n   */\n  notifications: UserNotification[];\n\n  /**\n   * @generated from field: string next_page_token = 2;\n   */\n  nextPageToken: string;\n};\n\n/**\n * Describes the message memos.api.v1.ListUserNotificationsResponse.\n * Use `create(ListUserNotificationsResponseSchema)` to create a new message.\n */\nexport const ListUserNotificationsResponseSchema: GenMessage<ListUserNotificationsResponse> = /*@__PURE__*/\n  messageDesc(file_api_v1_user_service, 30);\n\n/**\n * @generated from message memos.api.v1.UpdateUserNotificationRequest\n */\nexport type UpdateUserNotificationRequest = Message<\"memos.api.v1.UpdateUserNotificationRequest\"> & {\n  /**\n   * @generated from field: memos.api.v1.UserNotification notification = 1;\n   */\n  notification?: UserNotification;\n\n  /**\n   * @generated from field: google.protobuf.FieldMask update_mask = 2;\n   */\n  updateMask?: FieldMask;\n};\n\n/**\n * Describes the message memos.api.v1.UpdateUserNotificationRequest.\n * Use `create(UpdateUserNotificationRequestSchema)` to create a new message.\n */\nexport const UpdateUserNotificationRequestSchema: GenMessage<UpdateUserNotificationRequest> = /*@__PURE__*/\n  messageDesc(file_api_v1_user_service, 31);\n\n/**\n * @generated from message memos.api.v1.DeleteUserNotificationRequest\n */\nexport type DeleteUserNotificationRequest = Message<\"memos.api.v1.DeleteUserNotificationRequest\"> & {\n  /**\n   * Format: users/{user}/notifications/{notification}\n   *\n   * @generated from field: string name = 1;\n   */\n  name: string;\n};\n\n/**\n * Describes the message memos.api.v1.DeleteUserNotificationRequest.\n * Use `create(DeleteUserNotificationRequestSchema)` to create a new message.\n */\nexport const DeleteUserNotificationRequestSchema: GenMessage<DeleteUserNotificationRequest> = /*@__PURE__*/\n  messageDesc(file_api_v1_user_service, 32);\n\n/**\n * @generated from service memos.api.v1.UserService\n */\nexport const UserService: GenService<{\n  /**\n   * ListUsers returns a list of users.\n   *\n   * @generated from rpc memos.api.v1.UserService.ListUsers\n   */\n  listUsers: {\n    methodKind: \"unary\";\n    input: typeof ListUsersRequestSchema;\n    output: typeof ListUsersResponseSchema;\n  },\n  /**\n   * GetUser gets a user by ID or username.\n   * Supports both numeric IDs and username strings:\n   *   - users/{id}       (e.g., users/101)\n   *   - users/{username} (e.g., users/steven)\n   *\n   * @generated from rpc memos.api.v1.UserService.GetUser\n   */\n  getUser: {\n    methodKind: \"unary\";\n    input: typeof GetUserRequestSchema;\n    output: typeof UserSchema;\n  },\n  /**\n   * CreateUser creates a new user.\n   *\n   * @generated from rpc memos.api.v1.UserService.CreateUser\n   */\n  createUser: {\n    methodKind: \"unary\";\n    input: typeof CreateUserRequestSchema;\n    output: typeof UserSchema;\n  },\n  /**\n   * UpdateUser updates a user.\n   *\n   * @generated from rpc memos.api.v1.UserService.UpdateUser\n   */\n  updateUser: {\n    methodKind: \"unary\";\n    input: typeof UpdateUserRequestSchema;\n    output: typeof UserSchema;\n  },\n  /**\n   * DeleteUser deletes a user.\n   *\n   * @generated from rpc memos.api.v1.UserService.DeleteUser\n   */\n  deleteUser: {\n    methodKind: \"unary\";\n    input: typeof DeleteUserRequestSchema;\n    output: typeof EmptySchema;\n  },\n  /**\n   * ListAllUserStats returns statistics for all users.\n   *\n   * @generated from rpc memos.api.v1.UserService.ListAllUserStats\n   */\n  listAllUserStats: {\n    methodKind: \"unary\";\n    input: typeof ListAllUserStatsRequestSchema;\n    output: typeof ListAllUserStatsResponseSchema;\n  },\n  /**\n   * GetUserStats returns statistics for a specific user.\n   *\n   * @generated from rpc memos.api.v1.UserService.GetUserStats\n   */\n  getUserStats: {\n    methodKind: \"unary\";\n    input: typeof GetUserStatsRequestSchema;\n    output: typeof UserStatsSchema;\n  },\n  /**\n   * GetUserSetting returns the user setting.\n   *\n   * @generated from rpc memos.api.v1.UserService.GetUserSetting\n   */\n  getUserSetting: {\n    methodKind: \"unary\";\n    input: typeof GetUserSettingRequestSchema;\n    output: typeof UserSettingSchema;\n  },\n  /**\n   * UpdateUserSetting updates the user setting.\n   *\n   * @generated from rpc memos.api.v1.UserService.UpdateUserSetting\n   */\n  updateUserSetting: {\n    methodKind: \"unary\";\n    input: typeof UpdateUserSettingRequestSchema;\n    output: typeof UserSettingSchema;\n  },\n  /**\n   * ListUserSettings returns a list of user settings.\n   *\n   * @generated from rpc memos.api.v1.UserService.ListUserSettings\n   */\n  listUserSettings: {\n    methodKind: \"unary\";\n    input: typeof ListUserSettingsRequestSchema;\n    output: typeof ListUserSettingsResponseSchema;\n  },\n  /**\n   * ListPersonalAccessTokens returns a list of Personal Access Tokens (PATs) for a user.\n   * PATs are long-lived tokens for API/script access, distinct from short-lived JWT access tokens.\n   *\n   * @generated from rpc memos.api.v1.UserService.ListPersonalAccessTokens\n   */\n  listPersonalAccessTokens: {\n    methodKind: \"unary\";\n    input: typeof ListPersonalAccessTokensRequestSchema;\n    output: typeof ListPersonalAccessTokensResponseSchema;\n  },\n  /**\n   * CreatePersonalAccessToken creates a new Personal Access Token for a user.\n   * The token value is only returned once upon creation.\n   *\n   * @generated from rpc memos.api.v1.UserService.CreatePersonalAccessToken\n   */\n  createPersonalAccessToken: {\n    methodKind: \"unary\";\n    input: typeof CreatePersonalAccessTokenRequestSchema;\n    output: typeof CreatePersonalAccessTokenResponseSchema;\n  },\n  /**\n   * DeletePersonalAccessToken deletes a Personal Access Token.\n   *\n   * @generated from rpc memos.api.v1.UserService.DeletePersonalAccessToken\n   */\n  deletePersonalAccessToken: {\n    methodKind: \"unary\";\n    input: typeof DeletePersonalAccessTokenRequestSchema;\n    output: typeof EmptySchema;\n  },\n  /**\n   * ListUserWebhooks returns a list of webhooks for a user.\n   *\n   * @generated from rpc memos.api.v1.UserService.ListUserWebhooks\n   */\n  listUserWebhooks: {\n    methodKind: \"unary\";\n    input: typeof ListUserWebhooksRequestSchema;\n    output: typeof ListUserWebhooksResponseSchema;\n  },\n  /**\n   * CreateUserWebhook creates a new webhook for a user.\n   *\n   * @generated from rpc memos.api.v1.UserService.CreateUserWebhook\n   */\n  createUserWebhook: {\n    methodKind: \"unary\";\n    input: typeof CreateUserWebhookRequestSchema;\n    output: typeof UserWebhookSchema;\n  },\n  /**\n   * UpdateUserWebhook updates an existing webhook for a user.\n   *\n   * @generated from rpc memos.api.v1.UserService.UpdateUserWebhook\n   */\n  updateUserWebhook: {\n    methodKind: \"unary\";\n    input: typeof UpdateUserWebhookRequestSchema;\n    output: typeof UserWebhookSchema;\n  },\n  /**\n   * DeleteUserWebhook deletes a webhook for a user.\n   *\n   * @generated from rpc memos.api.v1.UserService.DeleteUserWebhook\n   */\n  deleteUserWebhook: {\n    methodKind: \"unary\";\n    input: typeof DeleteUserWebhookRequestSchema;\n    output: typeof EmptySchema;\n  },\n  /**\n   * ListUserNotifications lists notifications for a user.\n   *\n   * @generated from rpc memos.api.v1.UserService.ListUserNotifications\n   */\n  listUserNotifications: {\n    methodKind: \"unary\";\n    input: typeof ListUserNotificationsRequestSchema;\n    output: typeof ListUserNotificationsResponseSchema;\n  },\n  /**\n   * UpdateUserNotification updates a notification.\n   *\n   * @generated from rpc memos.api.v1.UserService.UpdateUserNotification\n   */\n  updateUserNotification: {\n    methodKind: \"unary\";\n    input: typeof UpdateUserNotificationRequestSchema;\n    output: typeof UserNotificationSchema;\n  },\n  /**\n   * DeleteUserNotification deletes a notification.\n   *\n   * @generated from rpc memos.api.v1.UserService.DeleteUserNotification\n   */\n  deleteUserNotification: {\n    methodKind: \"unary\";\n    input: typeof DeleteUserNotificationRequestSchema;\n    output: typeof EmptySchema;\n  },\n}> = /*@__PURE__*/\n  serviceDesc(file_api_v1_user_service, 0);\n\n"
  },
  {
    "path": "web/src/types/proto/google/api/annotations_pb.ts",
    "content": "// Copyright 2025 Google LLC\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\n// @generated by protoc-gen-es v2.11.0 with parameter \"target=ts\"\n// @generated from file google/api/annotations.proto (package google.api, syntax proto3)\n/* eslint-disable */\n\nimport type { GenExtension, GenFile } from \"@bufbuild/protobuf/codegenv2\";\nimport { extDesc, fileDesc } from \"@bufbuild/protobuf/codegenv2\";\nimport type { HttpRule } from \"./http_pb\";\nimport { file_google_api_http } from \"./http_pb\";\nimport type { MethodOptions } from \"@bufbuild/protobuf/wkt\";\nimport { file_google_protobuf_descriptor } from \"@bufbuild/protobuf/wkt\";\n\n/**\n * Describes the file google/api/annotations.proto.\n */\nexport const file_google_api_annotations: GenFile = /*@__PURE__*/\n  fileDesc(\"Chxnb29nbGUvYXBpL2Fubm90YXRpb25zLnByb3RvEgpnb29nbGUuYXBpOksKBGh0dHASHi5nb29nbGUucHJvdG9idWYuTWV0aG9kT3B0aW9ucxiwyrwiIAEoCzIULmdvb2dsZS5hcGkuSHR0cFJ1bGVSBGh0dHBCrgEKDmNvbS5nb29nbGUuYXBpQhBBbm5vdGF0aW9uc1Byb3RvUAFaQWdvb2dsZS5nb2xhbmcub3JnL2dlbnByb3RvL2dvb2dsZWFwaXMvYXBpL2Fubm90YXRpb25zO2Fubm90YXRpb25zogIDR0FYqgIKR29vZ2xlLkFwacoCCkdvb2dsZVxBcGniAhZHb29nbGVcQXBpXEdQQk1ldGFkYXRh6gILR29vZ2xlOjpBcGliBnByb3RvMw\", [file_google_api_http, file_google_protobuf_descriptor]);\n\n/**\n * See `HttpRule`.\n *\n * @generated from extension: google.api.HttpRule http = 72295728;\n */\nexport const http: GenExtension<MethodOptions, HttpRule> = /*@__PURE__*/\n  extDesc(file_google_api_annotations, 0);\n\n"
  },
  {
    "path": "web/src/types/proto/google/api/client_pb.ts",
    "content": "// Copyright 2025 Google LLC\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\n// @generated by protoc-gen-es v2.11.0 with parameter \"target=ts\"\n// @generated from file google/api/client.proto (package google.api, syntax proto3)\n/* eslint-disable */\n\nimport type { GenEnum, GenExtension, GenFile, GenMessage } from \"@bufbuild/protobuf/codegenv2\";\nimport { enumDesc, extDesc, fileDesc, messageDesc } from \"@bufbuild/protobuf/codegenv2\";\nimport type { LaunchStage } from \"./launch_stage_pb\";\nimport { file_google_api_launch_stage } from \"./launch_stage_pb\";\nimport type { Duration, MethodOptions, ServiceOptions } from \"@bufbuild/protobuf/wkt\";\nimport { file_google_protobuf_descriptor, file_google_protobuf_duration } from \"@bufbuild/protobuf/wkt\";\nimport type { Message } from \"@bufbuild/protobuf\";\n\n/**\n * Describes the file google/api/client.proto.\n */\nexport const file_google_api_client: GenFile = /*@__PURE__*/\n  fileDesc(\"Chdnb29nbGUvYXBpL2NsaWVudC5wcm90bxIKZ29vZ2xlLmFwaSK+AQoWQ29tbW9uTGFuZ3VhZ2VTZXR0aW5ncxIeChJyZWZlcmVuY2VfZG9jc191cmkYASABKAlCAhgBEjoKDGRlc3RpbmF0aW9ucxgCIAMoDjIkLmdvb2dsZS5hcGkuQ2xpZW50TGlicmFyeURlc3RpbmF0aW9uEkgKGnNlbGVjdGl2ZV9nYXBpY19nZW5lcmF0aW9uGAMgASgLMiQuZ29vZ2xlLmFwaS5TZWxlY3RpdmVHYXBpY0dlbmVyYXRpb24i+wMKFUNsaWVudExpYnJhcnlTZXR0aW5ncxIPCgd2ZXJzaW9uGAEgASgJEi0KDGxhdW5jaF9zdGFnZRgCIAEoDjIXLmdvb2dsZS5hcGkuTGF1bmNoU3RhZ2USGgoScmVzdF9udW1lcmljX2VudW1zGAMgASgIEi8KDWphdmFfc2V0dGluZ3MYFSABKAsyGC5nb29nbGUuYXBpLkphdmFTZXR0aW5ncxItCgxjcHBfc2V0dGluZ3MYFiABKAsyFy5nb29nbGUuYXBpLkNwcFNldHRpbmdzEi0KDHBocF9zZXR0aW5ncxgXIAEoCzIXLmdvb2dsZS5hcGkuUGhwU2V0dGluZ3MSMwoPcHl0aG9uX3NldHRpbmdzGBggASgLMhouZ29vZ2xlLmFwaS5QeXRob25TZXR0aW5ncxIvCg1ub2RlX3NldHRpbmdzGBkgASgLMhguZ29vZ2xlLmFwaS5Ob2RlU2V0dGluZ3MSMwoPZG90bmV0X3NldHRpbmdzGBogASgLMhouZ29vZ2xlLmFwaS5Eb3RuZXRTZXR0aW5ncxIvCg1ydWJ5X3NldHRpbmdzGBsgASgLMhguZ29vZ2xlLmFwaS5SdWJ5U2V0dGluZ3MSKwoLZ29fc2V0dGluZ3MYHCABKAsyFi5nb29nbGUuYXBpLkdvU2V0dGluZ3MiqAMKClB1Ymxpc2hpbmcSMwoPbWV0aG9kX3NldHRpbmdzGAIgAygLMhouZ29vZ2xlLmFwaS5NZXRob2RTZXR0aW5ncxIVCg1uZXdfaXNzdWVfdXJpGGUgASgJEhkKEWRvY3VtZW50YXRpb25fdXJpGGYgASgJEhYKDmFwaV9zaG9ydF9uYW1lGGcgASgJEhQKDGdpdGh1Yl9sYWJlbBhoIAEoCRIeChZjb2Rlb3duZXJfZ2l0aHViX3RlYW1zGGkgAygJEhYKDmRvY190YWdfcHJlZml4GGogASgJEjsKDG9yZ2FuaXphdGlvbhhrIAEoDjIlLmdvb2dsZS5hcGkuQ2xpZW50TGlicmFyeU9yZ2FuaXphdGlvbhI7ChBsaWJyYXJ5X3NldHRpbmdzGG0gAygLMiEuZ29vZ2xlLmFwaS5DbGllbnRMaWJyYXJ5U2V0dGluZ3MSKQohcHJvdG9fcmVmZXJlbmNlX2RvY3VtZW50YXRpb25fdXJpGG4gASgJEigKIHJlc3RfcmVmZXJlbmNlX2RvY3VtZW50YXRpb25fdXJpGG8gASgJIuMBCgxKYXZhU2V0dGluZ3MSFwoPbGlicmFyeV9wYWNrYWdlGAEgASgJEkwKE3NlcnZpY2VfY2xhc3NfbmFtZXMYAiADKAsyLy5nb29nbGUuYXBpLkphdmFTZXR0aW5ncy5TZXJ2aWNlQ2xhc3NOYW1lc0VudHJ5EjIKBmNvbW1vbhgDIAEoCzIiLmdvb2dsZS5hcGkuQ29tbW9uTGFuZ3VhZ2VTZXR0aW5ncxo4ChZTZXJ2aWNlQ2xhc3NOYW1lc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiQQoLQ3BwU2V0dGluZ3MSMgoGY29tbW9uGAEgASgLMiIuZ29vZ2xlLmFwaS5Db21tb25MYW5ndWFnZVNldHRpbmdzIkEKC1BocFNldHRpbmdzEjIKBmNvbW1vbhgBIAEoCzIiLmdvb2dsZS5hcGkuQ29tbW9uTGFuZ3VhZ2VTZXR0aW5ncyKbAgoOUHl0aG9uU2V0dGluZ3MSMgoGY29tbW9uGAEgASgLMiIuZ29vZ2xlLmFwaS5Db21tb25MYW5ndWFnZVNldHRpbmdzEk4KFWV4cGVyaW1lbnRhbF9mZWF0dXJlcxgCIAEoCzIvLmdvb2dsZS5hcGkuUHl0aG9uU2V0dGluZ3MuRXhwZXJpbWVudGFsRmVhdHVyZXMahAEKFEV4cGVyaW1lbnRhbEZlYXR1cmVzEh0KFXJlc3RfYXN5bmNfaW9fZW5hYmxlZBgBIAEoCBInCh9wcm90b2J1Zl9weXRob25pY190eXBlc19lbmFibGVkGAIgASgIEiQKHHVudmVyc2lvbmVkX3BhY2thZ2VfZGlzYWJsZWQYAyABKAgiQgoMTm9kZVNldHRpbmdzEjIKBmNvbW1vbhgBIAEoCzIiLmdvb2dsZS5hcGkuQ29tbW9uTGFuZ3VhZ2VTZXR0aW5ncyKqAwoORG90bmV0U2V0dGluZ3MSMgoGY29tbW9uGAEgASgLMiIuZ29vZ2xlLmFwaS5Db21tb25MYW5ndWFnZVNldHRpbmdzEkkKEHJlbmFtZWRfc2VydmljZXMYAiADKAsyLy5nb29nbGUuYXBpLkRvdG5ldFNldHRpbmdzLlJlbmFtZWRTZXJ2aWNlc0VudHJ5EksKEXJlbmFtZWRfcmVzb3VyY2VzGAMgAygLMjAuZ29vZ2xlLmFwaS5Eb3RuZXRTZXR0aW5ncy5SZW5hbWVkUmVzb3VyY2VzRW50cnkSGQoRaWdub3JlZF9yZXNvdXJjZXMYBCADKAkSIAoYZm9yY2VkX25hbWVzcGFjZV9hbGlhc2VzGAUgAygJEh4KFmhhbmR3cml0dGVuX3NpZ25hdHVyZXMYBiADKAkaNgoUUmVuYW1lZFNlcnZpY2VzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ARo3ChVSZW5hbWVkUmVzb3VyY2VzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASJCCgxSdWJ5U2V0dGluZ3MSMgoGY29tbW9uGAEgASgLMiIuZ29vZ2xlLmFwaS5Db21tb25MYW5ndWFnZVNldHRpbmdzIr8BCgpHb1NldHRpbmdzEjIKBmNvbW1vbhgBIAEoCzIiLmdvb2dsZS5hcGkuQ29tbW9uTGFuZ3VhZ2VTZXR0aW5ncxJFChByZW5hbWVkX3NlcnZpY2VzGAIgAygLMisuZ29vZ2xlLmFwaS5Hb1NldHRpbmdzLlJlbmFtZWRTZXJ2aWNlc0VudHJ5GjYKFFJlbmFtZWRTZXJ2aWNlc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEizwIKDk1ldGhvZFNldHRpbmdzEhAKCHNlbGVjdG9yGAEgASgJEjwKDGxvbmdfcnVubmluZxgCIAEoCzImLmdvb2dsZS5hcGkuTWV0aG9kU2V0dGluZ3MuTG9uZ1J1bm5pbmcSHQoVYXV0b19wb3B1bGF0ZWRfZmllbGRzGAMgAygJGs0BCgtMb25nUnVubmluZxI1ChJpbml0aWFsX3BvbGxfZGVsYXkYASABKAsyGS5nb29nbGUucHJvdG9idWYuRHVyYXRpb24SHQoVcG9sbF9kZWxheV9tdWx0aXBsaWVyGAIgASgCEjEKDm1heF9wb2xsX2RlbGF5GAMgASgLMhkuZ29vZ2xlLnByb3RvYnVmLkR1cmF0aW9uEjUKEnRvdGFsX3BvbGxfdGltZW91dBgEIAEoCzIZLmdvb2dsZS5wcm90b2J1Zi5EdXJhdGlvbiJRChhTZWxlY3RpdmVHYXBpY0dlbmVyYXRpb24SDwoHbWV0aG9kcxgBIAMoCRIkChxnZW5lcmF0ZV9vbWl0dGVkX2FzX2ludGVybmFsGAIgASgIKqMBChlDbGllbnRMaWJyYXJ5T3JnYW5pemF0aW9uEisKJ0NMSUVOVF9MSUJSQVJZX09SR0FOSVpBVElPTl9VTlNQRUNJRklFRBAAEgkKBUNMT1VEEAESBwoDQURTEAISCgoGUEhPVE9TEAMSDwoLU1RSRUVUX1ZJRVcQBBIMCghTSE9QUElORxAFEgcKA0dFTxAGEhEKDUdFTkVSQVRJVkVfQUkQBypnChhDbGllbnRMaWJyYXJ5RGVzdGluYXRpb24SKgomQ0xJRU5UX0xJQlJBUllfREVTVElOQVRJT05fVU5TUEVDSUZJRUQQABIKCgZHSVRIVUIQChITCg9QQUNLQUdFX01BTkFHRVIQFDpKChBtZXRob2Rfc2lnbmF0dXJlEh4uZ29vZ2xlLnByb3RvYnVmLk1ldGhvZE9wdGlvbnMYmwggAygJUg9tZXRob2RTaWduYXR1cmU6QwoMZGVmYXVsdF9ob3N0Eh8uZ29vZ2xlLnByb3RvYnVmLlNlcnZpY2VPcHRpb25zGJkIIAEoCVILZGVmYXVsdEhvc3Q6QwoMb2F1dGhfc2NvcGVzEh8uZ29vZ2xlLnByb3RvYnVmLlNlcnZpY2VPcHRpb25zGJoIIAEoCVILb2F1dGhTY29wZXM6RAoLYXBpX3ZlcnNpb24SHy5nb29nbGUucHJvdG9idWYuU2VydmljZU9wdGlvbnMYwbqr+gEgASgJUgphcGlWZXJzaW9uQqkBCg5jb20uZ29vZ2xlLmFwaUILQ2xpZW50UHJvdG9QAVpBZ29vZ2xlLmdvbGFuZy5vcmcvZ2VucHJvdG8vZ29vZ2xlYXBpcy9hcGkvYW5ub3RhdGlvbnM7YW5ub3RhdGlvbnOiAgNHQViqAgpHb29nbGUuQXBpygIKR29vZ2xlXEFwaeICFkdvb2dsZVxBcGlcR1BCTWV0YWRhdGHqAgtHb29nbGU6OkFwaWIGcHJvdG8z\", [file_google_api_launch_stage, file_google_protobuf_descriptor, file_google_protobuf_duration]);\n\n/**\n * Required information for every language.\n *\n * @generated from message google.api.CommonLanguageSettings\n */\nexport type CommonLanguageSettings = Message<\"google.api.CommonLanguageSettings\"> & {\n  /**\n   * Link to automatically generated reference documentation.  Example:\n   * https://cloud.google.com/nodejs/docs/reference/asset/latest\n   *\n   * @generated from field: string reference_docs_uri = 1 [deprecated = true];\n   * @deprecated\n   */\n  referenceDocsUri: string;\n\n  /**\n   * The destination where API teams want this client library to be published.\n   *\n   * @generated from field: repeated google.api.ClientLibraryDestination destinations = 2;\n   */\n  destinations: ClientLibraryDestination[];\n\n  /**\n   * Configuration for which RPCs should be generated in the GAPIC client.\n   *\n   * @generated from field: google.api.SelectiveGapicGeneration selective_gapic_generation = 3;\n   */\n  selectiveGapicGeneration?: SelectiveGapicGeneration;\n};\n\n/**\n * Describes the message google.api.CommonLanguageSettings.\n * Use `create(CommonLanguageSettingsSchema)` to create a new message.\n */\nexport const CommonLanguageSettingsSchema: GenMessage<CommonLanguageSettings> = /*@__PURE__*/\n  messageDesc(file_google_api_client, 0);\n\n/**\n * Details about how and where to publish client libraries.\n *\n * @generated from message google.api.ClientLibrarySettings\n */\nexport type ClientLibrarySettings = Message<\"google.api.ClientLibrarySettings\"> & {\n  /**\n   * Version of the API to apply these settings to. This is the full protobuf\n   * package for the API, ending in the version element.\n   * Examples: \"google.cloud.speech.v1\" and \"google.spanner.admin.database.v1\".\n   *\n   * @generated from field: string version = 1;\n   */\n  version: string;\n\n  /**\n   * Launch stage of this version of the API.\n   *\n   * @generated from field: google.api.LaunchStage launch_stage = 2;\n   */\n  launchStage: LaunchStage;\n\n  /**\n   * When using transport=rest, the client request will encode enums as\n   * numbers rather than strings.\n   *\n   * @generated from field: bool rest_numeric_enums = 3;\n   */\n  restNumericEnums: boolean;\n\n  /**\n   * Settings for legacy Java features, supported in the Service YAML.\n   *\n   * @generated from field: google.api.JavaSettings java_settings = 21;\n   */\n  javaSettings?: JavaSettings;\n\n  /**\n   * Settings for C++ client libraries.\n   *\n   * @generated from field: google.api.CppSettings cpp_settings = 22;\n   */\n  cppSettings?: CppSettings;\n\n  /**\n   * Settings for PHP client libraries.\n   *\n   * @generated from field: google.api.PhpSettings php_settings = 23;\n   */\n  phpSettings?: PhpSettings;\n\n  /**\n   * Settings for Python client libraries.\n   *\n   * @generated from field: google.api.PythonSettings python_settings = 24;\n   */\n  pythonSettings?: PythonSettings;\n\n  /**\n   * Settings for Node client libraries.\n   *\n   * @generated from field: google.api.NodeSettings node_settings = 25;\n   */\n  nodeSettings?: NodeSettings;\n\n  /**\n   * Settings for .NET client libraries.\n   *\n   * @generated from field: google.api.DotnetSettings dotnet_settings = 26;\n   */\n  dotnetSettings?: DotnetSettings;\n\n  /**\n   * Settings for Ruby client libraries.\n   *\n   * @generated from field: google.api.RubySettings ruby_settings = 27;\n   */\n  rubySettings?: RubySettings;\n\n  /**\n   * Settings for Go client libraries.\n   *\n   * @generated from field: google.api.GoSettings go_settings = 28;\n   */\n  goSettings?: GoSettings;\n};\n\n/**\n * Describes the message google.api.ClientLibrarySettings.\n * Use `create(ClientLibrarySettingsSchema)` to create a new message.\n */\nexport const ClientLibrarySettingsSchema: GenMessage<ClientLibrarySettings> = /*@__PURE__*/\n  messageDesc(file_google_api_client, 1);\n\n/**\n * This message configures the settings for publishing [Google Cloud Client\n * libraries](https://cloud.google.com/apis/docs/cloud-client-libraries)\n * generated from the service config.\n *\n * @generated from message google.api.Publishing\n */\nexport type Publishing = Message<\"google.api.Publishing\"> & {\n  /**\n   * A list of API method settings, e.g. the behavior for methods that use the\n   * long-running operation pattern.\n   *\n   * @generated from field: repeated google.api.MethodSettings method_settings = 2;\n   */\n  methodSettings: MethodSettings[];\n\n  /**\n   * Link to a *public* URI where users can report issues.  Example:\n   * https://issuetracker.google.com/issues/new?component=190865&template=1161103\n   *\n   * @generated from field: string new_issue_uri = 101;\n   */\n  newIssueUri: string;\n\n  /**\n   * Link to product home page.  Example:\n   * https://cloud.google.com/asset-inventory/docs/overview\n   *\n   * @generated from field: string documentation_uri = 102;\n   */\n  documentationUri: string;\n\n  /**\n   * Used as a tracking tag when collecting data about the APIs developer\n   * relations artifacts like docs, packages delivered to package managers,\n   * etc.  Example: \"speech\".\n   *\n   * @generated from field: string api_short_name = 103;\n   */\n  apiShortName: string;\n\n  /**\n   * GitHub label to apply to issues and pull requests opened for this API.\n   *\n   * @generated from field: string github_label = 104;\n   */\n  githubLabel: string;\n\n  /**\n   * GitHub teams to be added to CODEOWNERS in the directory in GitHub\n   * containing source code for the client libraries for this API.\n   *\n   * @generated from field: repeated string codeowner_github_teams = 105;\n   */\n  codeownerGithubTeams: string[];\n\n  /**\n   * A prefix used in sample code when demarking regions to be included in\n   * documentation.\n   *\n   * @generated from field: string doc_tag_prefix = 106;\n   */\n  docTagPrefix: string;\n\n  /**\n   * For whom the client library is being published.\n   *\n   * @generated from field: google.api.ClientLibraryOrganization organization = 107;\n   */\n  organization: ClientLibraryOrganization;\n\n  /**\n   * Client library settings.  If the same version string appears multiple\n   * times in this list, then the last one wins.  Settings from earlier\n   * settings with the same version string are discarded.\n   *\n   * @generated from field: repeated google.api.ClientLibrarySettings library_settings = 109;\n   */\n  librarySettings: ClientLibrarySettings[];\n\n  /**\n   * Optional link to proto reference documentation.  Example:\n   * https://cloud.google.com/pubsub/lite/docs/reference/rpc\n   *\n   * @generated from field: string proto_reference_documentation_uri = 110;\n   */\n  protoReferenceDocumentationUri: string;\n\n  /**\n   * Optional link to REST reference documentation.  Example:\n   * https://cloud.google.com/pubsub/lite/docs/reference/rest\n   *\n   * @generated from field: string rest_reference_documentation_uri = 111;\n   */\n  restReferenceDocumentationUri: string;\n};\n\n/**\n * Describes the message google.api.Publishing.\n * Use `create(PublishingSchema)` to create a new message.\n */\nexport const PublishingSchema: GenMessage<Publishing> = /*@__PURE__*/\n  messageDesc(file_google_api_client, 2);\n\n/**\n * Settings for Java client libraries.\n *\n * @generated from message google.api.JavaSettings\n */\nexport type JavaSettings = Message<\"google.api.JavaSettings\"> & {\n  /**\n   * The package name to use in Java. Clobbers the java_package option\n   * set in the protobuf. This should be used **only** by APIs\n   * who have already set the language_settings.java.package_name\" field\n   * in gapic.yaml. API teams should use the protobuf java_package option\n   * where possible.\n   *\n   * Example of a YAML configuration::\n   *\n   *  publishing:\n   *    java_settings:\n   *      library_package: com.google.cloud.pubsub.v1\n   *\n   * @generated from field: string library_package = 1;\n   */\n  libraryPackage: string;\n\n  /**\n   * Configure the Java class name to use instead of the service's for its\n   * corresponding generated GAPIC client. Keys are fully-qualified\n   * service names as they appear in the protobuf (including the full\n   * the language_settings.java.interface_names\" field in gapic.yaml. API\n   * teams should otherwise use the service name as it appears in the\n   * protobuf.\n   *\n   * Example of a YAML configuration::\n   *\n   *  publishing:\n   *    java_settings:\n   *      service_class_names:\n   *        - google.pubsub.v1.Publisher: TopicAdmin\n   *        - google.pubsub.v1.Subscriber: SubscriptionAdmin\n   *\n   * @generated from field: map<string, string> service_class_names = 2;\n   */\n  serviceClassNames: { [key: string]: string };\n\n  /**\n   * Some settings.\n   *\n   * @generated from field: google.api.CommonLanguageSettings common = 3;\n   */\n  common?: CommonLanguageSettings;\n};\n\n/**\n * Describes the message google.api.JavaSettings.\n * Use `create(JavaSettingsSchema)` to create a new message.\n */\nexport const JavaSettingsSchema: GenMessage<JavaSettings> = /*@__PURE__*/\n  messageDesc(file_google_api_client, 3);\n\n/**\n * Settings for C++ client libraries.\n *\n * @generated from message google.api.CppSettings\n */\nexport type CppSettings = Message<\"google.api.CppSettings\"> & {\n  /**\n   * Some settings.\n   *\n   * @generated from field: google.api.CommonLanguageSettings common = 1;\n   */\n  common?: CommonLanguageSettings;\n};\n\n/**\n * Describes the message google.api.CppSettings.\n * Use `create(CppSettingsSchema)` to create a new message.\n */\nexport const CppSettingsSchema: GenMessage<CppSettings> = /*@__PURE__*/\n  messageDesc(file_google_api_client, 4);\n\n/**\n * Settings for Php client libraries.\n *\n * @generated from message google.api.PhpSettings\n */\nexport type PhpSettings = Message<\"google.api.PhpSettings\"> & {\n  /**\n   * Some settings.\n   *\n   * @generated from field: google.api.CommonLanguageSettings common = 1;\n   */\n  common?: CommonLanguageSettings;\n};\n\n/**\n * Describes the message google.api.PhpSettings.\n * Use `create(PhpSettingsSchema)` to create a new message.\n */\nexport const PhpSettingsSchema: GenMessage<PhpSettings> = /*@__PURE__*/\n  messageDesc(file_google_api_client, 5);\n\n/**\n * Settings for Python client libraries.\n *\n * @generated from message google.api.PythonSettings\n */\nexport type PythonSettings = Message<\"google.api.PythonSettings\"> & {\n  /**\n   * Some settings.\n   *\n   * @generated from field: google.api.CommonLanguageSettings common = 1;\n   */\n  common?: CommonLanguageSettings;\n\n  /**\n   * Experimental features to be included during client library generation.\n   *\n   * @generated from field: google.api.PythonSettings.ExperimentalFeatures experimental_features = 2;\n   */\n  experimentalFeatures?: PythonSettings_ExperimentalFeatures;\n};\n\n/**\n * Describes the message google.api.PythonSettings.\n * Use `create(PythonSettingsSchema)` to create a new message.\n */\nexport const PythonSettingsSchema: GenMessage<PythonSettings> = /*@__PURE__*/\n  messageDesc(file_google_api_client, 6);\n\n/**\n * Experimental features to be included during client library generation.\n * These fields will be deprecated once the feature graduates and is enabled\n * by default.\n *\n * @generated from message google.api.PythonSettings.ExperimentalFeatures\n */\nexport type PythonSettings_ExperimentalFeatures = Message<\"google.api.PythonSettings.ExperimentalFeatures\"> & {\n  /**\n   * Enables generation of asynchronous REST clients if `rest` transport is\n   * enabled. By default, asynchronous REST clients will not be generated.\n   * This feature will be enabled by default 1 month after launching the\n   * feature in preview packages.\n   *\n   * @generated from field: bool rest_async_io_enabled = 1;\n   */\n  restAsyncIoEnabled: boolean;\n\n  /**\n   * Enables generation of protobuf code using new types that are more\n   * Pythonic which are included in `protobuf>=5.29.x`. This feature will be\n   * enabled by default 1 month after launching the feature in preview\n   * packages.\n   *\n   * @generated from field: bool protobuf_pythonic_types_enabled = 2;\n   */\n  protobufPythonicTypesEnabled: boolean;\n\n  /**\n   * Disables generation of an unversioned Python package for this client\n   * library. This means that the module names will need to be versioned in\n   * import statements. For example `import google.cloud.library_v2` instead\n   * of `import google.cloud.library`.\n   *\n   * @generated from field: bool unversioned_package_disabled = 3;\n   */\n  unversionedPackageDisabled: boolean;\n};\n\n/**\n * Describes the message google.api.PythonSettings.ExperimentalFeatures.\n * Use `create(PythonSettings_ExperimentalFeaturesSchema)` to create a new message.\n */\nexport const PythonSettings_ExperimentalFeaturesSchema: GenMessage<PythonSettings_ExperimentalFeatures> = /*@__PURE__*/\n  messageDesc(file_google_api_client, 6, 0);\n\n/**\n * Settings for Node client libraries.\n *\n * @generated from message google.api.NodeSettings\n */\nexport type NodeSettings = Message<\"google.api.NodeSettings\"> & {\n  /**\n   * Some settings.\n   *\n   * @generated from field: google.api.CommonLanguageSettings common = 1;\n   */\n  common?: CommonLanguageSettings;\n};\n\n/**\n * Describes the message google.api.NodeSettings.\n * Use `create(NodeSettingsSchema)` to create a new message.\n */\nexport const NodeSettingsSchema: GenMessage<NodeSettings> = /*@__PURE__*/\n  messageDesc(file_google_api_client, 7);\n\n/**\n * Settings for Dotnet client libraries.\n *\n * @generated from message google.api.DotnetSettings\n */\nexport type DotnetSettings = Message<\"google.api.DotnetSettings\"> & {\n  /**\n   * Some settings.\n   *\n   * @generated from field: google.api.CommonLanguageSettings common = 1;\n   */\n  common?: CommonLanguageSettings;\n\n  /**\n   * Map from original service names to renamed versions.\n   * This is used when the default generated types\n   * would cause a naming conflict. (Neither name is\n   * fully-qualified.)\n   * Example: Subscriber to SubscriberServiceApi.\n   *\n   * @generated from field: map<string, string> renamed_services = 2;\n   */\n  renamedServices: { [key: string]: string };\n\n  /**\n   * Map from full resource types to the effective short name\n   * for the resource. This is used when otherwise resource\n   * named from different services would cause naming collisions.\n   * Example entry:\n   * \"datalabeling.googleapis.com/Dataset\": \"DataLabelingDataset\"\n   *\n   * @generated from field: map<string, string> renamed_resources = 3;\n   */\n  renamedResources: { [key: string]: string };\n\n  /**\n   * List of full resource types to ignore during generation.\n   * This is typically used for API-specific Location resources,\n   * which should be handled by the generator as if they were actually\n   * the common Location resources.\n   * Example entry: \"documentai.googleapis.com/Location\"\n   *\n   * @generated from field: repeated string ignored_resources = 4;\n   */\n  ignoredResources: string[];\n\n  /**\n   * Namespaces which must be aliased in snippets due to\n   * a known (but non-generator-predictable) naming collision\n   *\n   * @generated from field: repeated string forced_namespace_aliases = 5;\n   */\n  forcedNamespaceAliases: string[];\n\n  /**\n   * Method signatures (in the form \"service.method(signature)\")\n   * which are provided separately, so shouldn't be generated.\n   * Snippets *calling* these methods are still generated, however.\n   *\n   * @generated from field: repeated string handwritten_signatures = 6;\n   */\n  handwrittenSignatures: string[];\n};\n\n/**\n * Describes the message google.api.DotnetSettings.\n * Use `create(DotnetSettingsSchema)` to create a new message.\n */\nexport const DotnetSettingsSchema: GenMessage<DotnetSettings> = /*@__PURE__*/\n  messageDesc(file_google_api_client, 8);\n\n/**\n * Settings for Ruby client libraries.\n *\n * @generated from message google.api.RubySettings\n */\nexport type RubySettings = Message<\"google.api.RubySettings\"> & {\n  /**\n   * Some settings.\n   *\n   * @generated from field: google.api.CommonLanguageSettings common = 1;\n   */\n  common?: CommonLanguageSettings;\n};\n\n/**\n * Describes the message google.api.RubySettings.\n * Use `create(RubySettingsSchema)` to create a new message.\n */\nexport const RubySettingsSchema: GenMessage<RubySettings> = /*@__PURE__*/\n  messageDesc(file_google_api_client, 9);\n\n/**\n * Settings for Go client libraries.\n *\n * @generated from message google.api.GoSettings\n */\nexport type GoSettings = Message<\"google.api.GoSettings\"> & {\n  /**\n   * Some settings.\n   *\n   * @generated from field: google.api.CommonLanguageSettings common = 1;\n   */\n  common?: CommonLanguageSettings;\n\n  /**\n   * Map of service names to renamed services. Keys are the package relative\n   * service names and values are the name to be used for the service client\n   * and call options.\n   *\n   * publishing:\n   *   go_settings:\n   *     renamed_services:\n   *       Publisher: TopicAdmin\n   *\n   * @generated from field: map<string, string> renamed_services = 2;\n   */\n  renamedServices: { [key: string]: string };\n};\n\n/**\n * Describes the message google.api.GoSettings.\n * Use `create(GoSettingsSchema)` to create a new message.\n */\nexport const GoSettingsSchema: GenMessage<GoSettings> = /*@__PURE__*/\n  messageDesc(file_google_api_client, 10);\n\n/**\n * Describes the generator configuration for a method.\n *\n * @generated from message google.api.MethodSettings\n */\nexport type MethodSettings = Message<\"google.api.MethodSettings\"> & {\n  /**\n   * The fully qualified name of the method, for which the options below apply.\n   * This is used to find the method to apply the options.\n   *\n   * Example:\n   *\n   *    publishing:\n   *      method_settings:\n   *      - selector: google.storage.control.v2.StorageControl.CreateFolder\n   *        # method settings for CreateFolder...\n   *\n   * @generated from field: string selector = 1;\n   */\n  selector: string;\n\n  /**\n   * Describes settings to use for long-running operations when generating\n   * API methods for RPCs. Complements RPCs that use the annotations in\n   * google/longrunning/operations.proto.\n   *\n   * Example of a YAML configuration::\n   *\n   *    publishing:\n   *      method_settings:\n   *      - selector: google.cloud.speech.v2.Speech.BatchRecognize\n   *        long_running:\n   *          initial_poll_delay: 60s # 1 minute\n   *          poll_delay_multiplier: 1.5\n   *          max_poll_delay: 360s # 6 minutes\n   *          total_poll_timeout: 54000s # 90 minutes\n   *\n   * @generated from field: google.api.MethodSettings.LongRunning long_running = 2;\n   */\n  longRunning?: MethodSettings_LongRunning;\n\n  /**\n   * List of top-level fields of the request message, that should be\n   * automatically populated by the client libraries based on their\n   * (google.api.field_info).format. Currently supported format: UUID4.\n   *\n   * Example of a YAML configuration:\n   *\n   *    publishing:\n   *      method_settings:\n   *      - selector: google.example.v1.ExampleService.CreateExample\n   *        auto_populated_fields:\n   *        - request_id\n   *\n   * @generated from field: repeated string auto_populated_fields = 3;\n   */\n  autoPopulatedFields: string[];\n};\n\n/**\n * Describes the message google.api.MethodSettings.\n * Use `create(MethodSettingsSchema)` to create a new message.\n */\nexport const MethodSettingsSchema: GenMessage<MethodSettings> = /*@__PURE__*/\n  messageDesc(file_google_api_client, 11);\n\n/**\n * Describes settings to use when generating API methods that use the\n * long-running operation pattern.\n * All default values below are from those used in the client library\n * generators (e.g.\n * [Java](https://github.com/googleapis/gapic-generator-java/blob/04c2faa191a9b5a10b92392fe8482279c4404803/src/main/java/com/google/api/generator/gapic/composer/common/RetrySettingsComposer.java)).\n *\n * @generated from message google.api.MethodSettings.LongRunning\n */\nexport type MethodSettings_LongRunning = Message<\"google.api.MethodSettings.LongRunning\"> & {\n  /**\n   * Initial delay after which the first poll request will be made.\n   * Default value: 5 seconds.\n   *\n   * @generated from field: google.protobuf.Duration initial_poll_delay = 1;\n   */\n  initialPollDelay?: Duration;\n\n  /**\n   * Multiplier to gradually increase delay between subsequent polls until it\n   * reaches max_poll_delay.\n   * Default value: 1.5.\n   *\n   * @generated from field: float poll_delay_multiplier = 2;\n   */\n  pollDelayMultiplier: number;\n\n  /**\n   * Maximum time between two subsequent poll requests.\n   * Default value: 45 seconds.\n   *\n   * @generated from field: google.protobuf.Duration max_poll_delay = 3;\n   */\n  maxPollDelay?: Duration;\n\n  /**\n   * Total polling timeout.\n   * Default value: 5 minutes.\n   *\n   * @generated from field: google.protobuf.Duration total_poll_timeout = 4;\n   */\n  totalPollTimeout?: Duration;\n};\n\n/**\n * Describes the message google.api.MethodSettings.LongRunning.\n * Use `create(MethodSettings_LongRunningSchema)` to create a new message.\n */\nexport const MethodSettings_LongRunningSchema: GenMessage<MethodSettings_LongRunning> = /*@__PURE__*/\n  messageDesc(file_google_api_client, 11, 0);\n\n/**\n * This message is used to configure the generation of a subset of the RPCs in\n * a service for client libraries.\n *\n * @generated from message google.api.SelectiveGapicGeneration\n */\nexport type SelectiveGapicGeneration = Message<\"google.api.SelectiveGapicGeneration\"> & {\n  /**\n   * An allowlist of the fully qualified names of RPCs that should be included\n   * on public client surfaces.\n   *\n   * @generated from field: repeated string methods = 1;\n   */\n  methods: string[];\n\n  /**\n   * Setting this to true indicates to the client generators that methods\n   * that would be excluded from the generation should instead be generated\n   * in a way that indicates these methods should not be consumed by\n   * end users. How this is expressed is up to individual language\n   * implementations to decide. Some examples may be: added annotations,\n   * obfuscated identifiers, or other language idiomatic patterns.\n   *\n   * @generated from field: bool generate_omitted_as_internal = 2;\n   */\n  generateOmittedAsInternal: boolean;\n};\n\n/**\n * Describes the message google.api.SelectiveGapicGeneration.\n * Use `create(SelectiveGapicGenerationSchema)` to create a new message.\n */\nexport const SelectiveGapicGenerationSchema: GenMessage<SelectiveGapicGeneration> = /*@__PURE__*/\n  messageDesc(file_google_api_client, 12);\n\n/**\n * The organization for which the client libraries are being published.\n * Affects the url where generated docs are published, etc.\n *\n * @generated from enum google.api.ClientLibraryOrganization\n */\nexport enum ClientLibraryOrganization {\n  /**\n   * Not useful.\n   *\n   * @generated from enum value: CLIENT_LIBRARY_ORGANIZATION_UNSPECIFIED = 0;\n   */\n  CLIENT_LIBRARY_ORGANIZATION_UNSPECIFIED = 0,\n\n  /**\n   * Google Cloud Platform Org.\n   *\n   * @generated from enum value: CLOUD = 1;\n   */\n  CLOUD = 1,\n\n  /**\n   * Ads (Advertising) Org.\n   *\n   * @generated from enum value: ADS = 2;\n   */\n  ADS = 2,\n\n  /**\n   * Photos Org.\n   *\n   * @generated from enum value: PHOTOS = 3;\n   */\n  PHOTOS = 3,\n\n  /**\n   * Street View Org.\n   *\n   * @generated from enum value: STREET_VIEW = 4;\n   */\n  STREET_VIEW = 4,\n\n  /**\n   * Shopping Org.\n   *\n   * @generated from enum value: SHOPPING = 5;\n   */\n  SHOPPING = 5,\n\n  /**\n   * Geo Org.\n   *\n   * @generated from enum value: GEO = 6;\n   */\n  GEO = 6,\n\n  /**\n   * Generative AI - https://developers.generativeai.google\n   *\n   * @generated from enum value: GENERATIVE_AI = 7;\n   */\n  GENERATIVE_AI = 7,\n}\n\n/**\n * Describes the enum google.api.ClientLibraryOrganization.\n */\nexport const ClientLibraryOrganizationSchema: GenEnum<ClientLibraryOrganization> = /*@__PURE__*/\n  enumDesc(file_google_api_client, 0);\n\n/**\n * To where should client libraries be published?\n *\n * @generated from enum google.api.ClientLibraryDestination\n */\nexport enum ClientLibraryDestination {\n  /**\n   * Client libraries will neither be generated nor published to package\n   * managers.\n   *\n   * @generated from enum value: CLIENT_LIBRARY_DESTINATION_UNSPECIFIED = 0;\n   */\n  CLIENT_LIBRARY_DESTINATION_UNSPECIFIED = 0,\n\n  /**\n   * Generate the client library in a repo under github.com/googleapis,\n   * but don't publish it to package managers.\n   *\n   * @generated from enum value: GITHUB = 10;\n   */\n  GITHUB = 10,\n\n  /**\n   * Publish the library to package managers like nuget.org and npmjs.com.\n   *\n   * @generated from enum value: PACKAGE_MANAGER = 20;\n   */\n  PACKAGE_MANAGER = 20,\n}\n\n/**\n * Describes the enum google.api.ClientLibraryDestination.\n */\nexport const ClientLibraryDestinationSchema: GenEnum<ClientLibraryDestination> = /*@__PURE__*/\n  enumDesc(file_google_api_client, 1);\n\n/**\n * A definition of a client library method signature.\n *\n * In client libraries, each proto RPC corresponds to one or more methods\n * which the end user is able to call, and calls the underlying RPC.\n * Normally, this method receives a single argument (a struct or instance\n * corresponding to the RPC request object). Defining this field will\n * add one or more overloads providing flattened or simpler method signatures\n * in some languages.\n *\n * The fields on the method signature are provided as a comma-separated\n * string.\n *\n * For example, the proto RPC and annotation:\n *\n *   rpc CreateSubscription(CreateSubscriptionRequest)\n *       returns (Subscription) {\n *     option (google.api.method_signature) = \"name,topic\";\n *   }\n *\n * Would add the following Java overload (in addition to the method accepting\n * the request object):\n *\n *   public final Subscription createSubscription(String name, String topic)\n *\n * The following backwards-compatibility guidelines apply:\n *\n *   * Adding this annotation to an unannotated method is backwards\n *     compatible.\n *   * Adding this annotation to a method which already has existing\n *     method signature annotations is backwards compatible if and only if\n *     the new method signature annotation is last in the sequence.\n *   * Modifying or removing an existing method signature annotation is\n *     a breaking change.\n *   * Re-ordering existing method signature annotations is a breaking\n *     change.\n *\n * @generated from extension: repeated string method_signature = 1051;\n */\nexport const method_signature: GenExtension<MethodOptions, string[]> = /*@__PURE__*/\n  extDesc(file_google_api_client, 0);\n\n/**\n * The hostname for this service.\n * This should be specified with no prefix or protocol.\n *\n * Example:\n *\n *   service Foo {\n *     option (google.api.default_host) = \"foo.googleapi.com\";\n *     ...\n *   }\n *\n * @generated from extension: string default_host = 1049;\n */\nexport const default_host: GenExtension<ServiceOptions, string> = /*@__PURE__*/\n  extDesc(file_google_api_client, 1);\n\n/**\n * OAuth scopes needed for the client.\n *\n * Example:\n *\n *   service Foo {\n *     option (google.api.oauth_scopes) = \\\n *       \"https://www.googleapis.com/auth/cloud-platform\";\n *     ...\n *   }\n *\n * If there is more than one scope, use a comma-separated string:\n *\n * Example:\n *\n *   service Foo {\n *     option (google.api.oauth_scopes) = \\\n *       \"https://www.googleapis.com/auth/cloud-platform,\"\n *       \"https://www.googleapis.com/auth/monitoring\";\n *     ...\n *   }\n *\n * @generated from extension: string oauth_scopes = 1050;\n */\nexport const oauth_scopes: GenExtension<ServiceOptions, string> = /*@__PURE__*/\n  extDesc(file_google_api_client, 2);\n\n/**\n * The API version of this service, which should be sent by version-aware\n * clients to the service. This allows services to abide by the schema and\n * behavior of the service at the time this API version was deployed.\n * The format of the API version must be treated as opaque by clients.\n * Services may use a format with an apparent structure, but clients must\n * not rely on this to determine components within an API version, or attempt\n * to construct other valid API versions. Note that this is for upcoming\n * functionality and may not be implemented for all services.\n *\n * Example:\n *\n *   service Foo {\n *     option (google.api.api_version) = \"v1_20230821_preview\";\n *   }\n *\n * @generated from extension: string api_version = 525000001;\n */\nexport const api_version: GenExtension<ServiceOptions, string> = /*@__PURE__*/\n  extDesc(file_google_api_client, 3);\n\n"
  },
  {
    "path": "web/src/types/proto/google/api/field_behavior_pb.ts",
    "content": "// Copyright 2025 Google LLC\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\n// @generated by protoc-gen-es v2.11.0 with parameter \"target=ts\"\n// @generated from file google/api/field_behavior.proto (package google.api, syntax proto3)\n/* eslint-disable */\n\nimport type { GenEnum, GenExtension, GenFile } from \"@bufbuild/protobuf/codegenv2\";\nimport { enumDesc, extDesc, fileDesc } from \"@bufbuild/protobuf/codegenv2\";\nimport type { FieldOptions } from \"@bufbuild/protobuf/wkt\";\nimport { file_google_protobuf_descriptor } from \"@bufbuild/protobuf/wkt\";\n\n/**\n * Describes the file google/api/field_behavior.proto.\n */\nexport const file_google_api_field_behavior: GenFile = /*@__PURE__*/\n  fileDesc(\"Ch9nb29nbGUvYXBpL2ZpZWxkX2JlaGF2aW9yLnByb3RvEgpnb29nbGUuYXBpKrYBCg1GaWVsZEJlaGF2aW9yEh4KGkZJRUxEX0JFSEFWSU9SX1VOU1BFQ0lGSUVEEAASDAoIT1BUSU9OQUwQARIMCghSRVFVSVJFRBACEg8KC09VVFBVVF9PTkxZEAMSDgoKSU5QVVRfT05MWRAEEg0KCUlNTVVUQUJMRRAFEhIKDlVOT1JERVJFRF9MSVNUEAYSFQoRTk9OX0VNUFRZX0RFRkFVTFQQBxIOCgpJREVOVElGSUVSEAg6ZAoOZmllbGRfYmVoYXZpb3ISHS5nb29nbGUucHJvdG9idWYuRmllbGRPcHRpb25zGJwIIAMoDjIZLmdvb2dsZS5hcGkuRmllbGRCZWhhdmlvckICEABSDWZpZWxkQmVoYXZpb3JCsAEKDmNvbS5nb29nbGUuYXBpQhJGaWVsZEJlaGF2aW9yUHJvdG9QAVpBZ29vZ2xlLmdvbGFuZy5vcmcvZ2VucHJvdG8vZ29vZ2xlYXBpcy9hcGkvYW5ub3RhdGlvbnM7YW5ub3RhdGlvbnOiAgNHQViqAgpHb29nbGUuQXBpygIKR29vZ2xlXEFwaeICFkdvb2dsZVxBcGlcR1BCTWV0YWRhdGHqAgtHb29nbGU6OkFwaWIGcHJvdG8z\", [file_google_protobuf_descriptor]);\n\n/**\n * An indicator of the behavior of a given field (for example, that a field\n * is required in requests, or given as output but ignored as input).\n * This **does not** change the behavior in protocol buffers itself; it only\n * denotes the behavior and may affect how API tooling handles the field.\n *\n * Note: This enum **may** receive new values in the future.\n *\n * @generated from enum google.api.FieldBehavior\n */\nexport enum FieldBehavior {\n  /**\n   * Conventional default for enums. Do not use this.\n   *\n   * @generated from enum value: FIELD_BEHAVIOR_UNSPECIFIED = 0;\n   */\n  FIELD_BEHAVIOR_UNSPECIFIED = 0,\n\n  /**\n   * Specifically denotes a field as optional.\n   * While all fields in protocol buffers are optional, this may be specified\n   * for emphasis if appropriate.\n   *\n   * @generated from enum value: OPTIONAL = 1;\n   */\n  OPTIONAL = 1,\n\n  /**\n   * Denotes a field as required.\n   * This indicates that the field **must** be provided as part of the request,\n   * and failure to do so will cause an error (usually `INVALID_ARGUMENT`).\n   *\n   * @generated from enum value: REQUIRED = 2;\n   */\n  REQUIRED = 2,\n\n  /**\n   * Denotes a field as output only.\n   * This indicates that the field is provided in responses, but including the\n   * field in a request does nothing (the server *must* ignore it and\n   * *must not* throw an error as a result of the field's presence).\n   *\n   * @generated from enum value: OUTPUT_ONLY = 3;\n   */\n  OUTPUT_ONLY = 3,\n\n  /**\n   * Denotes a field as input only.\n   * This indicates that the field is provided in requests, and the\n   * corresponding field is not included in output.\n   *\n   * @generated from enum value: INPUT_ONLY = 4;\n   */\n  INPUT_ONLY = 4,\n\n  /**\n   * Denotes a field as immutable.\n   * This indicates that the field may be set once in a request to create a\n   * resource, but may not be changed thereafter.\n   *\n   * @generated from enum value: IMMUTABLE = 5;\n   */\n  IMMUTABLE = 5,\n\n  /**\n   * Denotes that a (repeated) field is an unordered list.\n   * This indicates that the service may provide the elements of the list\n   * in any arbitrary  order, rather than the order the user originally\n   * provided. Additionally, the list's order may or may not be stable.\n   *\n   * @generated from enum value: UNORDERED_LIST = 6;\n   */\n  UNORDERED_LIST = 6,\n\n  /**\n   * Denotes that this field returns a non-empty default value if not set.\n   * This indicates that if the user provides the empty value in a request,\n   * a non-empty value will be returned. The user will not be aware of what\n   * non-empty value to expect.\n   *\n   * @generated from enum value: NON_EMPTY_DEFAULT = 7;\n   */\n  NON_EMPTY_DEFAULT = 7,\n\n  /**\n   * Denotes that the field in a resource (a message annotated with\n   * google.api.resource) is used in the resource name to uniquely identify the\n   * resource. For AIP-compliant APIs, this should only be applied to the\n   * `name` field on the resource.\n   *\n   * This behavior should not be applied to references to other resources within\n   * the message.\n   *\n   * The identifier field of resources often have different field behavior\n   * depending on the request it is embedded in (e.g. for Create methods name\n   * is optional and unused, while for Update methods it is required). Instead\n   * of method-specific annotations, only `IDENTIFIER` is required.\n   *\n   * @generated from enum value: IDENTIFIER = 8;\n   */\n  IDENTIFIER = 8,\n}\n\n/**\n * Describes the enum google.api.FieldBehavior.\n */\nexport const FieldBehaviorSchema: GenEnum<FieldBehavior> = /*@__PURE__*/\n  enumDesc(file_google_api_field_behavior, 0);\n\n/**\n * A designation of a specific field behavior (required, output only, etc.)\n * in protobuf messages.\n *\n * Examples:\n *\n *   string name = 1 [(google.api.field_behavior) = REQUIRED];\n *   State state = 1 [(google.api.field_behavior) = OUTPUT_ONLY];\n *   google.protobuf.Duration ttl = 1\n *     [(google.api.field_behavior) = INPUT_ONLY];\n *   google.protobuf.Timestamp expire_time = 1\n *     [(google.api.field_behavior) = OUTPUT_ONLY,\n *      (google.api.field_behavior) = IMMUTABLE];\n *\n * @generated from extension: repeated google.api.FieldBehavior field_behavior = 1052 [packed = false];\n */\nexport const field_behavior: GenExtension<FieldOptions, FieldBehavior[]> = /*@__PURE__*/\n  extDesc(file_google_api_field_behavior, 0);\n\n"
  },
  {
    "path": "web/src/types/proto/google/api/http_pb.ts",
    "content": "// Copyright 2025 Google LLC\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\n// @generated by protoc-gen-es v2.11.0 with parameter \"target=ts\"\n// @generated from file google/api/http.proto (package google.api, syntax proto3)\n/* eslint-disable */\n\nimport type { GenFile, GenMessage } from \"@bufbuild/protobuf/codegenv2\";\nimport { fileDesc, messageDesc } from \"@bufbuild/protobuf/codegenv2\";\nimport type { Message } from \"@bufbuild/protobuf\";\n\n/**\n * Describes the file google/api/http.proto.\n */\nexport const file_google_api_http: GenFile = /*@__PURE__*/\n  fileDesc(\"ChVnb29nbGUvYXBpL2h0dHAucHJvdG8SCmdvb2dsZS5hcGkiVAoESHR0cBIjCgVydWxlcxgBIAMoCzIULmdvb2dsZS5hcGkuSHR0cFJ1bGUSJwofZnVsbHlfZGVjb2RlX3Jlc2VydmVkX2V4cGFuc2lvbhgCIAEoCCKBAgoISHR0cFJ1bGUSEAoIc2VsZWN0b3IYASABKAkSDQoDZ2V0GAIgASgJSAASDQoDcHV0GAMgASgJSAASDgoEcG9zdBgEIAEoCUgAEhAKBmRlbGV0ZRgFIAEoCUgAEg8KBXBhdGNoGAYgASgJSAASLwoGY3VzdG9tGAggASgLMh0uZ29vZ2xlLmFwaS5DdXN0b21IdHRwUGF0dGVybkgAEgwKBGJvZHkYByABKAkSFQoNcmVzcG9uc2VfYm9keRgMIAEoCRIxChNhZGRpdGlvbmFsX2JpbmRpbmdzGAsgAygLMhQuZ29vZ2xlLmFwaS5IdHRwUnVsZUIJCgdwYXR0ZXJuIi8KEUN1c3RvbUh0dHBQYXR0ZXJuEgwKBGtpbmQYASABKAkSDAoEcGF0aBgCIAEoCUKnAQoOY29tLmdvb2dsZS5hcGlCCUh0dHBQcm90b1ABWkFnb29nbGUuZ29sYW5nLm9yZy9nZW5wcm90by9nb29nbGVhcGlzL2FwaS9hbm5vdGF0aW9uczthbm5vdGF0aW9uc6ICA0dBWKoCCkdvb2dsZS5BcGnKAgpHb29nbGVcQXBp4gIWR29vZ2xlXEFwaVxHUEJNZXRhZGF0YeoCC0dvb2dsZTo6QXBpYgZwcm90bzM\");\n\n/**\n * Defines the HTTP configuration for an API service. It contains a list of\n * [HttpRule][google.api.HttpRule], each specifying the mapping of an RPC method\n * to one or more HTTP REST API methods.\n *\n * @generated from message google.api.Http\n */\nexport type Http = Message<\"google.api.Http\"> & {\n  /**\n   * A list of HTTP configuration rules that apply to individual API methods.\n   *\n   * **NOTE:** All service configuration rules follow \"last one wins\" order.\n   *\n   * @generated from field: repeated google.api.HttpRule rules = 1;\n   */\n  rules: HttpRule[];\n\n  /**\n   * When set to true, URL path parameters will be fully URI-decoded except in\n   * cases of single segment matches in reserved expansion, where \"%2F\" will be\n   * left encoded.\n   *\n   * The default behavior is to not decode RFC 6570 reserved characters in multi\n   * segment matches.\n   *\n   * @generated from field: bool fully_decode_reserved_expansion = 2;\n   */\n  fullyDecodeReservedExpansion: boolean;\n};\n\n/**\n * Describes the message google.api.Http.\n * Use `create(HttpSchema)` to create a new message.\n */\nexport const HttpSchema: GenMessage<Http> = /*@__PURE__*/\n  messageDesc(file_google_api_http, 0);\n\n/**\n * gRPC Transcoding\n *\n * gRPC Transcoding is a feature for mapping between a gRPC method and one or\n * more HTTP REST endpoints. It allows developers to build a single API service\n * that supports both gRPC APIs and REST APIs. Many systems, including [Google\n * APIs](https://github.com/googleapis/googleapis),\n * [Cloud Endpoints](https://cloud.google.com/endpoints), [gRPC\n * Gateway](https://github.com/grpc-ecosystem/grpc-gateway),\n * and [Envoy](https://github.com/envoyproxy/envoy) proxy support this feature\n * and use it for large scale production services.\n *\n * `HttpRule` defines the schema of the gRPC/REST mapping. The mapping specifies\n * how different portions of the gRPC request message are mapped to the URL\n * path, URL query parameters, and HTTP request body. It also controls how the\n * gRPC response message is mapped to the HTTP response body. `HttpRule` is\n * typically specified as an `google.api.http` annotation on the gRPC method.\n *\n * Each mapping specifies a URL path template and an HTTP method. The path\n * template may refer to one or more fields in the gRPC request message, as long\n * as each field is a non-repeated field with a primitive (non-message) type.\n * The path template controls how fields of the request message are mapped to\n * the URL path.\n *\n * Example:\n *\n *     service Messaging {\n *       rpc GetMessage(GetMessageRequest) returns (Message) {\n *         option (google.api.http) = {\n *             get: \"/v1/{name=messages/*}\"\n *         };\n *       }\n *     }\n *     message GetMessageRequest {\n *       string name = 1; // Mapped to URL path.\n *     }\n *     message Message {\n *       string text = 1; // The resource content.\n *     }\n *\n * This enables an HTTP REST to gRPC mapping as below:\n *\n * - HTTP: `GET /v1/messages/123456`\n * - gRPC: `GetMessage(name: \"messages/123456\")`\n *\n * Any fields in the request message which are not bound by the path template\n * automatically become HTTP query parameters if there is no HTTP request body.\n * For example:\n *\n *     service Messaging {\n *       rpc GetMessage(GetMessageRequest) returns (Message) {\n *         option (google.api.http) = {\n *             get:\"/v1/messages/{message_id}\"\n *         };\n *       }\n *     }\n *     message GetMessageRequest {\n *       message SubMessage {\n *         string subfield = 1;\n *       }\n *       string message_id = 1; // Mapped to URL path.\n *       int64 revision = 2;    // Mapped to URL query parameter `revision`.\n *       SubMessage sub = 3;    // Mapped to URL query parameter `sub.subfield`.\n *     }\n *\n * This enables a HTTP JSON to RPC mapping as below:\n *\n * - HTTP: `GET /v1/messages/123456?revision=2&sub.subfield=foo`\n * - gRPC: `GetMessage(message_id: \"123456\" revision: 2 sub:\n * SubMessage(subfield: \"foo\"))`\n *\n * Note that fields which are mapped to URL query parameters must have a\n * primitive type or a repeated primitive type or a non-repeated message type.\n * In the case of a repeated type, the parameter can be repeated in the URL\n * as `...?param=A&param=B`. In the case of a message type, each field of the\n * message is mapped to a separate parameter, such as\n * `...?foo.a=A&foo.b=B&foo.c=C`.\n *\n * For HTTP methods that allow a request body, the `body` field\n * specifies the mapping. Consider a REST update method on the\n * message resource collection:\n *\n *     service Messaging {\n *       rpc UpdateMessage(UpdateMessageRequest) returns (Message) {\n *         option (google.api.http) = {\n *           patch: \"/v1/messages/{message_id}\"\n *           body: \"message\"\n *         };\n *       }\n *     }\n *     message UpdateMessageRequest {\n *       string message_id = 1; // mapped to the URL\n *       Message message = 2;   // mapped to the body\n *     }\n *\n * The following HTTP JSON to RPC mapping is enabled, where the\n * representation of the JSON in the request body is determined by\n * protos JSON encoding:\n *\n * - HTTP: `PATCH /v1/messages/123456 { \"text\": \"Hi!\" }`\n * - gRPC: `UpdateMessage(message_id: \"123456\" message { text: \"Hi!\" })`\n *\n * The special name `*` can be used in the body mapping to define that\n * every field not bound by the path template should be mapped to the\n * request body.  This enables the following alternative definition of\n * the update method:\n *\n *     service Messaging {\n *       rpc UpdateMessage(Message) returns (Message) {\n *         option (google.api.http) = {\n *           patch: \"/v1/messages/{message_id}\"\n *           body: \"*\"\n *         };\n *       }\n *     }\n *     message Message {\n *       string message_id = 1;\n *       string text = 2;\n *     }\n *\n *\n * The following HTTP JSON to RPC mapping is enabled:\n *\n * - HTTP: `PATCH /v1/messages/123456 { \"text\": \"Hi!\" }`\n * - gRPC: `UpdateMessage(message_id: \"123456\" text: \"Hi!\")`\n *\n * Note that when using `*` in the body mapping, it is not possible to\n * have HTTP parameters, as all fields not bound by the path end in\n * the body. This makes this option more rarely used in practice when\n * defining REST APIs. The common usage of `*` is in custom methods\n * which don't use the URL at all for transferring data.\n *\n * It is possible to define multiple HTTP methods for one RPC by using\n * the `additional_bindings` option. Example:\n *\n *     service Messaging {\n *       rpc GetMessage(GetMessageRequest) returns (Message) {\n *         option (google.api.http) = {\n *           get: \"/v1/messages/{message_id}\"\n *           additional_bindings {\n *             get: \"/v1/users/{user_id}/messages/{message_id}\"\n *           }\n *         };\n *       }\n *     }\n *     message GetMessageRequest {\n *       string message_id = 1;\n *       string user_id = 2;\n *     }\n *\n * This enables the following two alternative HTTP JSON to RPC mappings:\n *\n * - HTTP: `GET /v1/messages/123456`\n * - gRPC: `GetMessage(message_id: \"123456\")`\n *\n * - HTTP: `GET /v1/users/me/messages/123456`\n * - gRPC: `GetMessage(user_id: \"me\" message_id: \"123456\")`\n *\n * Rules for HTTP mapping\n *\n * 1. Leaf request fields (recursive expansion nested messages in the request\n *    message) are classified into three categories:\n *    - Fields referred by the path template. They are passed via the URL path.\n *    - Fields referred by the [HttpRule.body][google.api.HttpRule.body]. They\n *    are passed via the HTTP\n *      request body.\n *    - All other fields are passed via the URL query parameters, and the\n *      parameter name is the field path in the request message. A repeated\n *      field can be represented as multiple query parameters under the same\n *      name.\n *  2. If [HttpRule.body][google.api.HttpRule.body] is \"*\", there is no URL\n *  query parameter, all fields\n *     are passed via URL path and HTTP request body.\n *  3. If [HttpRule.body][google.api.HttpRule.body] is omitted, there is no HTTP\n *  request body, all\n *     fields are passed via URL path and URL query parameters.\n *\n * Path template syntax\n *\n *     Template = \"/\" Segments [ Verb ] ;\n *     Segments = Segment { \"/\" Segment } ;\n *     Segment  = \"*\" | \"**\" | LITERAL | Variable ;\n *     Variable = \"{\" FieldPath [ \"=\" Segments ] \"}\" ;\n *     FieldPath = IDENT { \".\" IDENT } ;\n *     Verb     = \":\" LITERAL ;\n *\n * The syntax `*` matches a single URL path segment. The syntax `**` matches\n * zero or more URL path segments, which must be the last part of the URL path\n * except the `Verb`.\n *\n * The syntax `Variable` matches part of the URL path as specified by its\n * template. A variable template must not contain other variables. If a variable\n * matches a single path segment, its template may be omitted, e.g. `{var}`\n * is equivalent to `{var=*}`.\n *\n * The syntax `LITERAL` matches literal text in the URL path. If the `LITERAL`\n * contains any reserved character, such characters should be percent-encoded\n * before the matching.\n *\n * If a variable contains exactly one path segment, such as `\"{var}\"` or\n * `\"{var=*}\"`, when such a variable is expanded into a URL path on the client\n * side, all characters except `[-_.~0-9a-zA-Z]` are percent-encoded. The\n * server side does the reverse decoding. Such variables show up in the\n * [Discovery\n * Document](https://developers.google.com/discovery/v1/reference/apis) as\n * `{var}`.\n *\n * If a variable contains multiple path segments, such as `\"{var=foo/*}\"`\n * or `\"{var=**}\"`, when such a variable is expanded into a URL path on the\n * client side, all characters except `[-_.~/0-9a-zA-Z]` are percent-encoded.\n * The server side does the reverse decoding, except \"%2F\" and \"%2f\" are left\n * unchanged. Such variables show up in the\n * [Discovery\n * Document](https://developers.google.com/discovery/v1/reference/apis) as\n * `{+var}`.\n *\n * Using gRPC API Service Configuration\n *\n * gRPC API Service Configuration (service config) is a configuration language\n * for configuring a gRPC service to become a user-facing product. The\n * service config is simply the YAML representation of the `google.api.Service`\n * proto message.\n *\n * As an alternative to annotating your proto file, you can configure gRPC\n * transcoding in your service config YAML files. You do this by specifying a\n * `HttpRule` that maps the gRPC method to a REST endpoint, achieving the same\n * effect as the proto annotation. This can be particularly useful if you\n * have a proto that is reused in multiple services. Note that any transcoding\n * specified in the service config will override any matching transcoding\n * configuration in the proto.\n *\n * The following example selects a gRPC method and applies an `HttpRule` to it:\n *\n *     http:\n *       rules:\n *         - selector: example.v1.Messaging.GetMessage\n *           get: /v1/messages/{message_id}/{sub.subfield}\n *\n * Special notes\n *\n * When gRPC Transcoding is used to map a gRPC to JSON REST endpoints, the\n * proto to JSON conversion must follow the [proto3\n * specification](https://developers.google.com/protocol-buffers/docs/proto3#json).\n *\n * While the single segment variable follows the semantics of\n * [RFC 6570](https://tools.ietf.org/html/rfc6570) Section 3.2.2 Simple String\n * Expansion, the multi segment variable **does not** follow RFC 6570 Section\n * 3.2.3 Reserved Expansion. The reason is that the Reserved Expansion\n * does not expand special characters like `?` and `#`, which would lead\n * to invalid URLs. As the result, gRPC Transcoding uses a custom encoding\n * for multi segment variables.\n *\n * The path variables **must not** refer to any repeated or mapped field,\n * because client libraries are not capable of handling such variable expansion.\n *\n * The path variables **must not** capture the leading \"/\" character. The reason\n * is that the most common use case \"{var}\" does not capture the leading \"/\"\n * character. For consistency, all path variables must share the same behavior.\n *\n * Repeated message fields must not be mapped to URL query parameters, because\n * no client library can support such complicated mapping.\n *\n * If an API needs to use a JSON array for request or response body, it can map\n * the request or response body to a repeated field. However, some gRPC\n * Transcoding implementations may not support this feature.\n *\n * @generated from message google.api.HttpRule\n */\nexport type HttpRule = Message<\"google.api.HttpRule\"> & {\n  /**\n   * Selects a method to which this rule applies.\n   *\n   * Refer to [selector][google.api.DocumentationRule.selector] for syntax\n   * details.\n   *\n   * @generated from field: string selector = 1;\n   */\n  selector: string;\n\n  /**\n   * Determines the URL pattern is matched by this rules. This pattern can be\n   * used with any of the {get|put|post|delete|patch} methods. A custom method\n   * can be defined using the 'custom' field.\n   *\n   * @generated from oneof google.api.HttpRule.pattern\n   */\n  pattern: {\n    /**\n     * Maps to HTTP GET. Used for listing and getting information about\n     * resources.\n     *\n     * @generated from field: string get = 2;\n     */\n    value: string;\n    case: \"get\";\n  } | {\n    /**\n     * Maps to HTTP PUT. Used for replacing a resource.\n     *\n     * @generated from field: string put = 3;\n     */\n    value: string;\n    case: \"put\";\n  } | {\n    /**\n     * Maps to HTTP POST. Used for creating a resource or performing an action.\n     *\n     * @generated from field: string post = 4;\n     */\n    value: string;\n    case: \"post\";\n  } | {\n    /**\n     * Maps to HTTP DELETE. Used for deleting a resource.\n     *\n     * @generated from field: string delete = 5;\n     */\n    value: string;\n    case: \"delete\";\n  } | {\n    /**\n     * Maps to HTTP PATCH. Used for updating a resource.\n     *\n     * @generated from field: string patch = 6;\n     */\n    value: string;\n    case: \"patch\";\n  } | {\n    /**\n     * The custom pattern is used for specifying an HTTP method that is not\n     * included in the `pattern` field, such as HEAD, or \"*\" to leave the\n     * HTTP method unspecified for this rule. The wild-card rule is useful\n     * for services that provide content to Web (HTML) clients.\n     *\n     * @generated from field: google.api.CustomHttpPattern custom = 8;\n     */\n    value: CustomHttpPattern;\n    case: \"custom\";\n  } | { case: undefined; value?: undefined };\n\n  /**\n   * The name of the request field whose value is mapped to the HTTP request\n   * body, or `*` for mapping all request fields not captured by the path\n   * pattern to the HTTP body, or omitted for not having any HTTP request body.\n   *\n   * NOTE: the referred field must be present at the top-level of the request\n   * message type.\n   *\n   * @generated from field: string body = 7;\n   */\n  body: string;\n\n  /**\n   * Optional. The name of the response field whose value is mapped to the HTTP\n   * response body. When omitted, the entire response message will be used\n   * as the HTTP response body.\n   *\n   * NOTE: The referred field must be present at the top-level of the response\n   * message type.\n   *\n   * @generated from field: string response_body = 12;\n   */\n  responseBody: string;\n\n  /**\n   * Additional HTTP bindings for the selector. Nested bindings must\n   * not contain an `additional_bindings` field themselves (that is,\n   * the nesting may only be one level deep).\n   *\n   * @generated from field: repeated google.api.HttpRule additional_bindings = 11;\n   */\n  additionalBindings: HttpRule[];\n};\n\n/**\n * Describes the message google.api.HttpRule.\n * Use `create(HttpRuleSchema)` to create a new message.\n */\nexport const HttpRuleSchema: GenMessage<HttpRule> = /*@__PURE__*/\n  messageDesc(file_google_api_http, 1);\n\n/**\n * A custom pattern is used for defining custom HTTP verb.\n *\n * @generated from message google.api.CustomHttpPattern\n */\nexport type CustomHttpPattern = Message<\"google.api.CustomHttpPattern\"> & {\n  /**\n   * The name of this custom HTTP verb.\n   *\n   * @generated from field: string kind = 1;\n   */\n  kind: string;\n\n  /**\n   * The path matched by this custom verb.\n   *\n   * @generated from field: string path = 2;\n   */\n  path: string;\n};\n\n/**\n * Describes the message google.api.CustomHttpPattern.\n * Use `create(CustomHttpPatternSchema)` to create a new message.\n */\nexport const CustomHttpPatternSchema: GenMessage<CustomHttpPattern> = /*@__PURE__*/\n  messageDesc(file_google_api_http, 2);\n\n"
  },
  {
    "path": "web/src/types/proto/google/api/launch_stage_pb.ts",
    "content": "// Copyright 2025 Google LLC\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\n// @generated by protoc-gen-es v2.11.0 with parameter \"target=ts\"\n// @generated from file google/api/launch_stage.proto (package google.api, syntax proto3)\n/* eslint-disable */\n\nimport type { GenEnum, GenFile } from \"@bufbuild/protobuf/codegenv2\";\nimport { enumDesc, fileDesc } from \"@bufbuild/protobuf/codegenv2\";\n\n/**\n * Describes the file google/api/launch_stage.proto.\n */\nexport const file_google_api_launch_stage: GenFile = /*@__PURE__*/\n  fileDesc(\"Ch1nb29nbGUvYXBpL2xhdW5jaF9zdGFnZS5wcm90bxIKZ29vZ2xlLmFwaSqMAQoLTGF1bmNoU3RhZ2USHAoYTEFVTkNIX1NUQUdFX1VOU1BFQ0lGSUVEEAASEQoNVU5JTVBMRU1FTlRFRBAGEg0KCVBSRUxBVU5DSBAHEhAKDEVBUkxZX0FDQ0VTUxABEgkKBUFMUEhBEAISCAoEQkVUQRADEgYKAkdBEAQSDgoKREVQUkVDQVRFRBAFQpoBCg5jb20uZ29vZ2xlLmFwaUIQTGF1bmNoU3RhZ2VQcm90b1ABWi1nb29nbGUuZ29sYW5nLm9yZy9nZW5wcm90by9nb29nbGVhcGlzL2FwaTthcGmiAgNHQViqAgpHb29nbGUuQXBpygIKR29vZ2xlXEFwaeICFkdvb2dsZVxBcGlcR1BCTWV0YWRhdGHqAgtHb29nbGU6OkFwaWIGcHJvdG8z\");\n\n/**\n * The launch stage as defined by [Google Cloud Platform\n * Launch Stages](https://cloud.google.com/terms/launch-stages).\n *\n * @generated from enum google.api.LaunchStage\n */\nexport enum LaunchStage {\n  /**\n   * Do not use this default value.\n   *\n   * @generated from enum value: LAUNCH_STAGE_UNSPECIFIED = 0;\n   */\n  LAUNCH_STAGE_UNSPECIFIED = 0,\n\n  /**\n   * The feature is not yet implemented. Users can not use it.\n   *\n   * @generated from enum value: UNIMPLEMENTED = 6;\n   */\n  UNIMPLEMENTED = 6,\n\n  /**\n   * Prelaunch features are hidden from users and are only visible internally.\n   *\n   * @generated from enum value: PRELAUNCH = 7;\n   */\n  PRELAUNCH = 7,\n\n  /**\n   * Early Access features are limited to a closed group of testers. To use\n   * these features, you must sign up in advance and sign a Trusted Tester\n   * agreement (which includes confidentiality provisions). These features may\n   * be unstable, changed in backward-incompatible ways, and are not\n   * guaranteed to be released.\n   *\n   * @generated from enum value: EARLY_ACCESS = 1;\n   */\n  EARLY_ACCESS = 1,\n\n  /**\n   * Alpha is a limited availability test for releases before they are cleared\n   * for widespread use. By Alpha, all significant design issues are resolved\n   * and we are in the process of verifying functionality. Alpha customers\n   * need to apply for access, agree to applicable terms, and have their\n   * projects allowlisted. Alpha releases don't have to be feature complete,\n   * no SLAs are provided, and there are no technical support obligations, but\n   * they will be far enough along that customers can actually use them in\n   * test environments or for limited-use tests -- just like they would in\n   * normal production cases.\n   *\n   * @generated from enum value: ALPHA = 2;\n   */\n  ALPHA = 2,\n\n  /**\n   * Beta is the point at which we are ready to open a release for any\n   * customer to use. There are no SLA or technical support obligations in a\n   * Beta release. Products will be complete from a feature perspective, but\n   * may have some open outstanding issues. Beta releases are suitable for\n   * limited production use cases.\n   *\n   * @generated from enum value: BETA = 3;\n   */\n  BETA = 3,\n\n  /**\n   * GA features are open to all developers and are considered stable and\n   * fully qualified for production use.\n   *\n   * @generated from enum value: GA = 4;\n   */\n  GA = 4,\n\n  /**\n   * Deprecated features are scheduled to be shut down and removed. For more\n   * information, see the \"Deprecation Policy\" section of our [Terms of\n   * Service](https://cloud.google.com/terms/)\n   * and the [Google Cloud Platform Subject to the Deprecation\n   * Policy](https://cloud.google.com/terms/deprecation) documentation.\n   *\n   * @generated from enum value: DEPRECATED = 5;\n   */\n  DEPRECATED = 5,\n}\n\n/**\n * Describes the enum google.api.LaunchStage.\n */\nexport const LaunchStageSchema: GenEnum<LaunchStage> = /*@__PURE__*/\n  enumDesc(file_google_api_launch_stage, 0);\n\n"
  },
  {
    "path": "web/src/types/proto/google/api/resource_pb.ts",
    "content": "// Copyright 2025 Google LLC\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\n// @generated by protoc-gen-es v2.11.0 with parameter \"target=ts\"\n// @generated from file google/api/resource.proto (package google.api, syntax proto3)\n/* eslint-disable */\n\nimport type { GenEnum, GenExtension, GenFile, GenMessage } from \"@bufbuild/protobuf/codegenv2\";\nimport { enumDesc, extDesc, fileDesc, messageDesc } from \"@bufbuild/protobuf/codegenv2\";\nimport type { FieldOptions, FileOptions, MessageOptions } from \"@bufbuild/protobuf/wkt\";\nimport { file_google_protobuf_descriptor } from \"@bufbuild/protobuf/wkt\";\nimport type { Message } from \"@bufbuild/protobuf\";\n\n/**\n * Describes the file google/api/resource.proto.\n */\nexport const file_google_api_resource: GenFile = /*@__PURE__*/\n  fileDesc(\"Chlnb29nbGUvYXBpL3Jlc291cmNlLnByb3RvEgpnb29nbGUuYXBpIu4CChJSZXNvdXJjZURlc2NyaXB0b3ISDAoEdHlwZRgBIAEoCRIPCgdwYXR0ZXJuGAIgAygJEhIKCm5hbWVfZmllbGQYAyABKAkSNwoHaGlzdG9yeRgEIAEoDjImLmdvb2dsZS5hcGkuUmVzb3VyY2VEZXNjcmlwdG9yLkhpc3RvcnkSDgoGcGx1cmFsGAUgASgJEhAKCHNpbmd1bGFyGAYgASgJEjMKBXN0eWxlGAogAygOMiQuZ29vZ2xlLmFwaS5SZXNvdXJjZURlc2NyaXB0b3IuU3R5bGUiWwoHSGlzdG9yeRIXChNISVNUT1JZX1VOU1BFQ0lGSUVEEAASHQoZT1JJR0lOQUxMWV9TSU5HTEVfUEFUVEVSThABEhgKFEZVVFVSRV9NVUxUSV9QQVRURVJOEAIiOAoFU3R5bGUSFQoRU1RZTEVfVU5TUEVDSUZJRUQQABIYChRERUNMQVJBVElWRV9GUklFTkRMWRABIjUKEVJlc291cmNlUmVmZXJlbmNlEgwKBHR5cGUYASABKAkSEgoKY2hpbGRfdHlwZRgCIAEoCTpsChJyZXNvdXJjZV9yZWZlcmVuY2USHS5nb29nbGUucHJvdG9idWYuRmllbGRPcHRpb25zGJ8IIAEoCzIdLmdvb2dsZS5hcGkuUmVzb3VyY2VSZWZlcmVuY2VSEXJlc291cmNlUmVmZXJlbmNlOm4KE3Jlc291cmNlX2RlZmluaXRpb24SHC5nb29nbGUucHJvdG9idWYuRmlsZU9wdGlvbnMYnQggAygLMh4uZ29vZ2xlLmFwaS5SZXNvdXJjZURlc2NyaXB0b3JSEnJlc291cmNlRGVmaW5pdGlvbjpcCghyZXNvdXJjZRIfLmdvb2dsZS5wcm90b2J1Zi5NZXNzYWdlT3B0aW9ucxidCCABKAsyHi5nb29nbGUuYXBpLlJlc291cmNlRGVzY3JpcHRvclIIcmVzb3VyY2VCqwEKDmNvbS5nb29nbGUuYXBpQg1SZXNvdXJjZVByb3RvUAFaQWdvb2dsZS5nb2xhbmcub3JnL2dlbnByb3RvL2dvb2dsZWFwaXMvYXBpL2Fubm90YXRpb25zO2Fubm90YXRpb25zogIDR0FYqgIKR29vZ2xlLkFwacoCCkdvb2dsZVxBcGniAhZHb29nbGVcQXBpXEdQQk1ldGFkYXRh6gILR29vZ2xlOjpBcGliBnByb3RvMw\", [file_google_protobuf_descriptor]);\n\n/**\n * A simple descriptor of a resource type.\n *\n * ResourceDescriptor annotates a resource message (either by means of a\n * protobuf annotation or use in the service config), and associates the\n * resource's schema, the resource type, and the pattern of the resource name.\n *\n * Example:\n *\n *     message Topic {\n *       // Indicates this message defines a resource schema.\n *       // Declares the resource type in the format of {service}/{kind}.\n *       // For Kubernetes resources, the format is {api group}/{kind}.\n *       option (google.api.resource) = {\n *         type: \"pubsub.googleapis.com/Topic\"\n *         pattern: \"projects/{project}/topics/{topic}\"\n *       };\n *     }\n *\n * The ResourceDescriptor Yaml config will look like:\n *\n *     resources:\n *     - type: \"pubsub.googleapis.com/Topic\"\n *       pattern: \"projects/{project}/topics/{topic}\"\n *\n * Sometimes, resources have multiple patterns, typically because they can\n * live under multiple parents.\n *\n * Example:\n *\n *     message LogEntry {\n *       option (google.api.resource) = {\n *         type: \"logging.googleapis.com/LogEntry\"\n *         pattern: \"projects/{project}/logs/{log}\"\n *         pattern: \"folders/{folder}/logs/{log}\"\n *         pattern: \"organizations/{organization}/logs/{log}\"\n *         pattern: \"billingAccounts/{billing_account}/logs/{log}\"\n *       };\n *     }\n *\n * The ResourceDescriptor Yaml config will look like:\n *\n *     resources:\n *     - type: 'logging.googleapis.com/LogEntry'\n *       pattern: \"projects/{project}/logs/{log}\"\n *       pattern: \"folders/{folder}/logs/{log}\"\n *       pattern: \"organizations/{organization}/logs/{log}\"\n *       pattern: \"billingAccounts/{billing_account}/logs/{log}\"\n *\n * @generated from message google.api.ResourceDescriptor\n */\nexport type ResourceDescriptor = Message<\"google.api.ResourceDescriptor\"> & {\n  /**\n   * The resource type. It must be in the format of\n   * {service_name}/{resource_type_kind}. The `resource_type_kind` must be\n   * singular and must not include version numbers.\n   *\n   * Example: `storage.googleapis.com/Bucket`\n   *\n   * The value of the resource_type_kind must follow the regular expression\n   * /[A-Za-z][a-zA-Z0-9]+/. It should start with an upper case character and\n   * should use PascalCase (UpperCamelCase). The maximum number of\n   * characters allowed for the `resource_type_kind` is 100.\n   *\n   * @generated from field: string type = 1;\n   */\n  type: string;\n\n  /**\n   * Optional. The relative resource name pattern associated with this resource\n   * type. The DNS prefix of the full resource name shouldn't be specified here.\n   *\n   * The path pattern must follow the syntax, which aligns with HTTP binding\n   * syntax:\n   *\n   *     Template = Segment { \"/\" Segment } ;\n   *     Segment = LITERAL | Variable ;\n   *     Variable = \"{\" LITERAL \"}\" ;\n   *\n   * Examples:\n   *\n   *     - \"projects/{project}/topics/{topic}\"\n   *     - \"projects/{project}/knowledgeBases/{knowledge_base}\"\n   *\n   * The components in braces correspond to the IDs for each resource in the\n   * hierarchy. It is expected that, if multiple patterns are provided,\n   * the same component name (e.g. \"project\") refers to IDs of the same\n   * type of resource.\n   *\n   * @generated from field: repeated string pattern = 2;\n   */\n  pattern: string[];\n\n  /**\n   * Optional. The field on the resource that designates the resource name\n   * field. If omitted, this is assumed to be \"name\".\n   *\n   * @generated from field: string name_field = 3;\n   */\n  nameField: string;\n\n  /**\n   * Optional. The historical or future-looking state of the resource pattern.\n   *\n   * Example:\n   *\n   *     // The InspectTemplate message originally only supported resource\n   *     // names with organization, and project was added later.\n   *     message InspectTemplate {\n   *       option (google.api.resource) = {\n   *         type: \"dlp.googleapis.com/InspectTemplate\"\n   *         pattern:\n   *         \"organizations/{organization}/inspectTemplates/{inspect_template}\"\n   *         pattern: \"projects/{project}/inspectTemplates/{inspect_template}\"\n   *         history: ORIGINALLY_SINGLE_PATTERN\n   *       };\n   *     }\n   *\n   * @generated from field: google.api.ResourceDescriptor.History history = 4;\n   */\n  history: ResourceDescriptor_History;\n\n  /**\n   * The plural name used in the resource name and permission names, such as\n   * 'projects' for the resource name of 'projects/{project}' and the permission\n   * name of 'cloudresourcemanager.googleapis.com/projects.get'. One exception\n   * to this is for Nested Collections that have stuttering names, as defined\n   * in [AIP-122](https://google.aip.dev/122#nested-collections), where the\n   * collection ID in the resource name pattern does not necessarily directly\n   * match the `plural` value.\n   *\n   * It is the same concept of the `plural` field in k8s CRD spec\n   * https://kubernetes.io/docs/tasks/access-kubernetes-api/custom-resources/custom-resource-definitions/\n   *\n   * Note: The plural form is required even for singleton resources. See\n   * https://aip.dev/156\n   *\n   * @generated from field: string plural = 5;\n   */\n  plural: string;\n\n  /**\n   * The same concept of the `singular` field in k8s CRD spec\n   * https://kubernetes.io/docs/tasks/access-kubernetes-api/custom-resources/custom-resource-definitions/\n   * Such as \"project\" for the `resourcemanager.googleapis.com/Project` type.\n   *\n   * @generated from field: string singular = 6;\n   */\n  singular: string;\n\n  /**\n   * Style flag(s) for this resource.\n   * These indicate that a resource is expected to conform to a given\n   * style. See the specific style flags for additional information.\n   *\n   * @generated from field: repeated google.api.ResourceDescriptor.Style style = 10;\n   */\n  style: ResourceDescriptor_Style[];\n};\n\n/**\n * Describes the message google.api.ResourceDescriptor.\n * Use `create(ResourceDescriptorSchema)` to create a new message.\n */\nexport const ResourceDescriptorSchema: GenMessage<ResourceDescriptor> = /*@__PURE__*/\n  messageDesc(file_google_api_resource, 0);\n\n/**\n * A description of the historical or future-looking state of the\n * resource pattern.\n *\n * @generated from enum google.api.ResourceDescriptor.History\n */\nexport enum ResourceDescriptor_History {\n  /**\n   * The \"unset\" value.\n   *\n   * @generated from enum value: HISTORY_UNSPECIFIED = 0;\n   */\n  HISTORY_UNSPECIFIED = 0,\n\n  /**\n   * The resource originally had one pattern and launched as such, and\n   * additional patterns were added later.\n   *\n   * @generated from enum value: ORIGINALLY_SINGLE_PATTERN = 1;\n   */\n  ORIGINALLY_SINGLE_PATTERN = 1,\n\n  /**\n   * The resource has one pattern, but the API owner expects to add more\n   * later. (This is the inverse of ORIGINALLY_SINGLE_PATTERN, and prevents\n   * that from being necessary once there are multiple patterns.)\n   *\n   * @generated from enum value: FUTURE_MULTI_PATTERN = 2;\n   */\n  FUTURE_MULTI_PATTERN = 2,\n}\n\n/**\n * Describes the enum google.api.ResourceDescriptor.History.\n */\nexport const ResourceDescriptor_HistorySchema: GenEnum<ResourceDescriptor_History> = /*@__PURE__*/\n  enumDesc(file_google_api_resource, 0, 0);\n\n/**\n * A flag representing a specific style that a resource claims to conform to.\n *\n * @generated from enum google.api.ResourceDescriptor.Style\n */\nexport enum ResourceDescriptor_Style {\n  /**\n   * The unspecified value. Do not use.\n   *\n   * @generated from enum value: STYLE_UNSPECIFIED = 0;\n   */\n  STYLE_UNSPECIFIED = 0,\n\n  /**\n   * This resource is intended to be \"declarative-friendly\".\n   *\n   * Declarative-friendly resources must be more strictly consistent, and\n   * setting this to true communicates to tools that this resource should\n   * adhere to declarative-friendly expectations.\n   *\n   * Note: This is used by the API linter (linter.aip.dev) to enable\n   * additional checks.\n   *\n   * @generated from enum value: DECLARATIVE_FRIENDLY = 1;\n   */\n  DECLARATIVE_FRIENDLY = 1,\n}\n\n/**\n * Describes the enum google.api.ResourceDescriptor.Style.\n */\nexport const ResourceDescriptor_StyleSchema: GenEnum<ResourceDescriptor_Style> = /*@__PURE__*/\n  enumDesc(file_google_api_resource, 0, 1);\n\n/**\n * Defines a proto annotation that describes a string field that refers to\n * an API resource.\n *\n * @generated from message google.api.ResourceReference\n */\nexport type ResourceReference = Message<\"google.api.ResourceReference\"> & {\n  /**\n   * The resource type that the annotated field references.\n   *\n   * Example:\n   *\n   *     message Subscription {\n   *       string topic = 2 [(google.api.resource_reference) = {\n   *         type: \"pubsub.googleapis.com/Topic\"\n   *       }];\n   *     }\n   *\n   * Occasionally, a field may reference an arbitrary resource. In this case,\n   * APIs use the special value * in their resource reference.\n   *\n   * Example:\n   *\n   *     message GetIamPolicyRequest {\n   *       string resource = 2 [(google.api.resource_reference) = {\n   *         type: \"*\"\n   *       }];\n   *     }\n   *\n   * @generated from field: string type = 1;\n   */\n  type: string;\n\n  /**\n   * The resource type of a child collection that the annotated field\n   * references. This is useful for annotating the `parent` field that\n   * doesn't have a fixed resource type.\n   *\n   * Example:\n   *\n   *     message ListLogEntriesRequest {\n   *       string parent = 1 [(google.api.resource_reference) = {\n   *         child_type: \"logging.googleapis.com/LogEntry\"\n   *       };\n   *     }\n   *\n   * @generated from field: string child_type = 2;\n   */\n  childType: string;\n};\n\n/**\n * Describes the message google.api.ResourceReference.\n * Use `create(ResourceReferenceSchema)` to create a new message.\n */\nexport const ResourceReferenceSchema: GenMessage<ResourceReference> = /*@__PURE__*/\n  messageDesc(file_google_api_resource, 1);\n\n/**\n * An annotation that describes a resource reference, see\n * [ResourceReference][].\n *\n * @generated from extension: google.api.ResourceReference resource_reference = 1055;\n */\nexport const resource_reference: GenExtension<FieldOptions, ResourceReference> = /*@__PURE__*/\n  extDesc(file_google_api_resource, 0);\n\n/**\n * An annotation that describes a resource definition without a corresponding\n * message; see [ResourceDescriptor][].\n *\n * @generated from extension: repeated google.api.ResourceDescriptor resource_definition = 1053;\n */\nexport const resource_definition: GenExtension<FileOptions, ResourceDescriptor[]> = /*@__PURE__*/\n  extDesc(file_google_api_resource, 1);\n\n/**\n * An annotation that describes a resource definition, see\n * [ResourceDescriptor][].\n *\n * @generated from extension: google.api.ResourceDescriptor resource = 1053;\n */\nexport const resource: GenExtension<MessageOptions, ResourceDescriptor> = /*@__PURE__*/\n  extDesc(file_google_api_resource, 2);\n\n"
  },
  {
    "path": "web/src/types/proto/google/type/color_pb.ts",
    "content": "// Copyright 2025 Google LLC\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\n// @generated by protoc-gen-es v2.11.0 with parameter \"target=ts\"\n// @generated from file google/type/color.proto (package google.type, syntax proto3)\n/* eslint-disable */\n\nimport type { GenFile, GenMessage } from \"@bufbuild/protobuf/codegenv2\";\nimport { fileDesc, messageDesc } from \"@bufbuild/protobuf/codegenv2\";\nimport { file_google_protobuf_wrappers } from \"@bufbuild/protobuf/wkt\";\nimport type { Message } from \"@bufbuild/protobuf\";\n\n/**\n * Describes the file google/type/color.proto.\n */\nexport const file_google_type_color: GenFile = /*@__PURE__*/\n  fileDesc(\"Chdnb29nbGUvdHlwZS9jb2xvci5wcm90bxILZ29vZ2xlLnR5cGUiXQoFQ29sb3ISCwoDcmVkGAEgASgCEg0KBWdyZWVuGAIgASgCEgwKBGJsdWUYAyABKAISKgoFYWxwaGEYBCABKAsyGy5nb29nbGUucHJvdG9idWYuRmxvYXRWYWx1ZUKlAQoPY29tLmdvb2dsZS50eXBlQgpDb2xvclByb3RvUAFaNmdvb2dsZS5nb2xhbmcub3JnL2dlbnByb3RvL2dvb2dsZWFwaXMvdHlwZS9jb2xvcjtjb2xvcvgBAaICA0dUWKoCC0dvb2dsZS5UeXBlygILR29vZ2xlXFR5cGXiAhdHb29nbGVcVHlwZVxHUEJNZXRhZGF0YeoCDEdvb2dsZTo6VHlwZWIGcHJvdG8z\", [file_google_protobuf_wrappers]);\n\n/**\n * Represents a color in the RGBA color space. This representation is designed\n * for simplicity of conversion to/from color representations in various\n * languages over compactness. For example, the fields of this representation\n * can be trivially provided to the constructor of `java.awt.Color` in Java; it\n * can also be trivially provided to UIColor's `+colorWithRed:green:blue:alpha`\n * method in iOS; and, with just a little work, it can be easily formatted into\n * a CSS `rgba()` string in JavaScript.\n *\n * This reference page doesn't carry information about the absolute color\n * space\n * that should be used to interpret the RGB value (e.g. sRGB, Adobe RGB,\n * DCI-P3, BT.2020, etc.). By default, applications should assume the sRGB color\n * space.\n *\n * When color equality needs to be decided, implementations, unless\n * documented otherwise, treat two colors as equal if all their red,\n * green, blue, and alpha values each differ by at most 1e-5.\n *\n * Example (Java):\n *\n *      import com.google.type.Color;\n *\n *      // ...\n *      public static java.awt.Color fromProto(Color protocolor) {\n *        float alpha = protocolor.hasAlpha()\n *            ? protocolor.getAlpha().getValue()\n *            : 1.0;\n *\n *        return new java.awt.Color(\n *            protocolor.getRed(),\n *            protocolor.getGreen(),\n *            protocolor.getBlue(),\n *            alpha);\n *      }\n *\n *      public static Color toProto(java.awt.Color color) {\n *        float red = (float) color.getRed();\n *        float green = (float) color.getGreen();\n *        float blue = (float) color.getBlue();\n *        float denominator = 255.0;\n *        Color.Builder resultBuilder =\n *            Color\n *                .newBuilder()\n *                .setRed(red / denominator)\n *                .setGreen(green / denominator)\n *                .setBlue(blue / denominator);\n *        int alpha = color.getAlpha();\n *        if (alpha != 255) {\n *          result.setAlpha(\n *              FloatValue\n *                  .newBuilder()\n *                  .setValue(((float) alpha) / denominator)\n *                  .build());\n *        }\n *        return resultBuilder.build();\n *      }\n *      // ...\n *\n * Example (iOS / Obj-C):\n *\n *      // ...\n *      static UIColor* fromProto(Color* protocolor) {\n *         float red = [protocolor red];\n *         float green = [protocolor green];\n *         float blue = [protocolor blue];\n *         FloatValue* alpha_wrapper = [protocolor alpha];\n *         float alpha = 1.0;\n *         if (alpha_wrapper != nil) {\n *           alpha = [alpha_wrapper value];\n *         }\n *         return [UIColor colorWithRed:red green:green blue:blue alpha:alpha];\n *      }\n *\n *      static Color* toProto(UIColor* color) {\n *          CGFloat red, green, blue, alpha;\n *          if (![color getRed:&red green:&green blue:&blue alpha:&alpha]) {\n *            return nil;\n *          }\n *          Color* result = [[Color alloc] init];\n *          [result setRed:red];\n *          [result setGreen:green];\n *          [result setBlue:blue];\n *          if (alpha <= 0.9999) {\n *            [result setAlpha:floatWrapperWithValue(alpha)];\n *          }\n *          [result autorelease];\n *          return result;\n *     }\n *     // ...\n *\n *  Example (JavaScript):\n *\n *     // ...\n *\n *     var protoToCssColor = function(rgb_color) {\n *        var redFrac = rgb_color.red || 0.0;\n *        var greenFrac = rgb_color.green || 0.0;\n *        var blueFrac = rgb_color.blue || 0.0;\n *        var red = Math.floor(redFrac * 255);\n *        var green = Math.floor(greenFrac * 255);\n *        var blue = Math.floor(blueFrac * 255);\n *\n *        if (!('alpha' in rgb_color)) {\n *           return rgbToCssColor(red, green, blue);\n *        }\n *\n *        var alphaFrac = rgb_color.alpha.value || 0.0;\n *        var rgbParams = [red, green, blue].join(',');\n *        return ['rgba(', rgbParams, ',', alphaFrac, ')'].join('');\n *     };\n *\n *     var rgbToCssColor = function(red, green, blue) {\n *       var rgbNumber = new Number((red << 16) | (green << 8) | blue);\n *       var hexString = rgbNumber.toString(16);\n *       var missingZeros = 6 - hexString.length;\n *       var resultBuilder = ['#'];\n *       for (var i = 0; i < missingZeros; i++) {\n *          resultBuilder.push('0');\n *       }\n *       resultBuilder.push(hexString);\n *       return resultBuilder.join('');\n *     };\n *\n *     // ...\n *\n * @generated from message google.type.Color\n */\nexport type Color = Message<\"google.type.Color\"> & {\n  /**\n   * The amount of red in the color as a value in the interval [0, 1].\n   *\n   * @generated from field: float red = 1;\n   */\n  red: number;\n\n  /**\n   * The amount of green in the color as a value in the interval [0, 1].\n   *\n   * @generated from field: float green = 2;\n   */\n  green: number;\n\n  /**\n   * The amount of blue in the color as a value in the interval [0, 1].\n   *\n   * @generated from field: float blue = 3;\n   */\n  blue: number;\n\n  /**\n   * The fraction of this color that should be applied to the pixel. That is,\n   * the final pixel color is defined by the equation:\n   *\n   *   `pixel color = alpha * (this color) + (1.0 - alpha) * (background color)`\n   *\n   * This means that a value of 1.0 corresponds to a solid color, whereas\n   * a value of 0.0 corresponds to a completely transparent color. This\n   * uses a wrapper message rather than a simple float scalar so that it is\n   * possible to distinguish between a default value and the value being unset.\n   * If omitted, this color object is rendered as a solid color\n   * (as if the alpha value had been explicitly given a value of 1.0).\n   *\n   * @generated from field: google.protobuf.FloatValue alpha = 4;\n   */\n  alpha?: number;\n};\n\n/**\n * Describes the message google.type.Color.\n * Use `create(ColorSchema)` to create a new message.\n */\nexport const ColorSchema: GenMessage<Color> = /*@__PURE__*/\n  messageDesc(file_google_type_color, 0);\n\n"
  },
  {
    "path": "web/src/types/statistics.ts",
    "content": "export interface StatisticsViewProps {\n  className?: string;\n}\n\nexport interface MonthNavigatorProps {\n  visibleMonth: string;\n  onMonthChange: (month: string) => void;\n  activityStats: Record<string, number>;\n}\n\nexport interface StatisticsData {\n  activityStats: Record<string, number>;\n}\n"
  },
  {
    "path": "web/src/types/view.d.ts",
    "content": "interface DialogCallback {\n  destroy: FunctionType;\n}\n\ntype DialogProps = DialogCallback;\n"
  },
  {
    "path": "web/src/utils/attachment.ts",
    "content": "import { Attachment } from \"@/types/proto/api/v1/attachment_service_pb\";\n\nexport const getAttachmentUrl = (attachment: Attachment) => {\n  if (attachment.externalLink) {\n    return attachment.externalLink;\n  }\n\n  return `${window.location.origin}/file/${attachment.name}/${attachment.filename}`;\n};\n\nexport const getAttachmentThumbnailUrl = (attachment: Attachment) => {\n  return `${window.location.origin}/file/${attachment.name}/${attachment.filename}?thumbnail=true`;\n};\n\nexport const getAttachmentType = (attachment: Attachment) => {\n  if (isImage(attachment.type)) {\n    return \"image/*\";\n  } else if (attachment.type.startsWith(\"video\")) {\n    return \"video/*\";\n  } else if (attachment.type.startsWith(\"audio\") && !isMidiFile(attachment.type)) {\n    return \"audio/*\";\n  } else if (attachment.type.startsWith(\"text\")) {\n    return \"text/*\";\n  } else if (attachment.type.startsWith(\"application/epub+zip\")) {\n    return \"application/epub+zip\";\n  } else if (attachment.type.startsWith(\"application/pdf\")) {\n    return \"application/pdf\";\n  } else if (attachment.type.includes(\"word\")) {\n    return \"application/msword\";\n  } else if (attachment.type.includes(\"excel\")) {\n    return \"application/msexcel\";\n  } else if (attachment.type.startsWith(\"application/zip\")) {\n    return \"application/zip\";\n  } else if (attachment.type.startsWith(\"application/x-java-archive\")) {\n    return \"application/x-java-archive\";\n  } else {\n    return \"application/octet-stream\";\n  }\n};\n\n// isImage returns true if the given mime type is an image.\nexport const isImage = (t: string) => {\n  // Don't show PSDs as images.\n  return t.startsWith(\"image/\") && !isPSD(t);\n};\n\n// isMidiFile returns true if the given mime type is a MIDI file.\nexport const isMidiFile = (mimeType: string): boolean => {\n  return mimeType === \"audio/midi\" || mimeType === \"audio/mid\" || mimeType === \"audio/x-midi\" || mimeType === \"application/x-midi\";\n};\n\nconst isPSD = (t: string) => {\n  return t === \"image/vnd.adobe.photoshop\" || t === \"image/x-photoshop\" || t === \"image/photoshop\";\n};\n"
  },
  {
    "path": "web/src/utils/auth-redirect.ts",
    "content": "import { clearAccessToken } from \"@/auth-state\";\nimport { ROUTES } from \"@/router/routes\";\n\nconst PUBLIC_ROUTES = [\n  ROUTES.AUTH, // Authentication pages\n  ROUTES.EXPLORE, // Explore page\n  ROUTES.SHARED_MEMO + \"/\", // Shared memo pages (share-link viewer)\n  \"/u/\", // User profile pages (dynamic)\n  \"/memos/\", // Individual memo detail pages (dynamic)\n] as const;\n\nfunction isPublicRoute(path: string): boolean {\n  return PUBLIC_ROUTES.some((route) => path.startsWith(route));\n}\n\nexport function redirectOnAuthFailure(forceRedirect = false): void {\n  const currentPath = window.location.pathname;\n\n  // Already on auth page, nothing to do.\n  if (currentPath.startsWith(ROUTES.AUTH)) {\n    return;\n  }\n\n  // Don't redirect if it's a public route (unless forced, e.g. public visibility is disallowed).\n  if (!forceRedirect && isPublicRoute(currentPath)) {\n    return;\n  }\n\n  clearAccessToken();\n  window.location.replace(ROUTES.AUTH);\n}\n"
  },
  {
    "path": "web/src/utils/format.ts",
    "content": "export function formatFileSize(bytes: number): string {\n  if (bytes === 0) return \"0 B\";\n  if (bytes < 0) return \"Invalid size\";\n\n  const units = [\"B\", \"KB\", \"MB\", \"GB\", \"TB\"];\n  const k = 1024;\n  const i = Math.floor(Math.log(bytes) / Math.log(k));\n  const size = bytes / Math.pow(k, i);\n  const formatted = i === 0 ? size.toString() : size.toFixed(1);\n\n  return `${formatted} ${units[i]}`;\n}\n\nexport function getFileTypeLabel(mimeType: string): string {\n  if (!mimeType) return \"File\";\n\n  const [category, subtype] = mimeType.split(\"/\");\n\n  const specialCases: Record<string, string> = {\n    \"application/pdf\": \"PDF\",\n    \"application/zip\": \"ZIP\",\n    \"application/x-zip-compressed\": \"ZIP\",\n    \"application/json\": \"JSON\",\n    \"application/xml\": \"XML\",\n    \"text/plain\": \"TXT\",\n    \"text/html\": \"HTML\",\n    \"text/css\": \"CSS\",\n    \"text/javascript\": \"JS\",\n    \"application/javascript\": \"JS\",\n  };\n\n  if (specialCases[mimeType]) {\n    return specialCases[mimeType];\n  }\n\n  if (category === \"image\") {\n    const imageTypes: Record<string, string> = {\n      jpeg: \"JPEG\",\n      jpg: \"JPEG\",\n      png: \"PNG\",\n      gif: \"GIF\",\n      webp: \"WebP\",\n      svg: \"SVG\",\n      \"svg+xml\": \"SVG\",\n      bmp: \"BMP\",\n      ico: \"ICO\",\n    };\n    return imageTypes[subtype] || subtype.toUpperCase();\n  }\n\n  if (category === \"video\") {\n    const videoTypes: Record<string, string> = {\n      mp4: \"MP4\",\n      webm: \"WebM\",\n      ogg: \"OGG\",\n      avi: \"AVI\",\n      mov: \"MOV\",\n      quicktime: \"MOV\",\n    };\n    return videoTypes[subtype] || subtype.toUpperCase();\n  }\n\n  if (category === \"audio\") {\n    const audioTypes: Record<string, string> = {\n      mp3: \"MP3\",\n      mpeg: \"MP3\",\n      wav: \"WAV\",\n      ogg: \"OGG\",\n      webm: \"WebM\",\n    };\n    return audioTypes[subtype] || subtype.toUpperCase();\n  }\n\n  return subtype ? subtype.toUpperCase() : category.toUpperCase();\n}\n"
  },
  {
    "path": "web/src/utils/i18n.ts",
    "content": "import { FallbackLngObjList } from \"i18next\";\nimport { useTranslation } from \"react-i18next\";\nimport i18n, { locales, TLocale } from \"@/i18n\";\nimport enTranslation from \"@/locales/en.json\";\n\nconst LOCALE_STORAGE_KEY = \"memos-locale\";\n\nconst getStoredLocale = (): Locale | null => {\n  try {\n    const stored = localStorage.getItem(LOCALE_STORAGE_KEY);\n    return stored && locales.includes(stored) ? (stored as Locale) : null;\n  } catch {\n    return null;\n  }\n};\n\nconst setStoredLocale = (locale: Locale): void => {\n  try {\n    localStorage.setItem(LOCALE_STORAGE_KEY, locale);\n  } catch {\n    // localStorage might not be available\n  }\n};\n\nexport const findNearestMatchedLanguage = (language: string): Locale => {\n  if (locales.includes(language as TLocale)) {\n    return language as Locale;\n  }\n\n  const i18nFallbacks = Object.entries(i18n.store.options.fallbackLng as FallbackLngObjList);\n  for (const [main, fallbacks] of i18nFallbacks) {\n    if (language === main) {\n      return fallbacks[0] as Locale;\n    }\n  }\n\n  const shortCode = language.substring(0, 2);\n  if (locales.includes(shortCode as TLocale)) {\n    return shortCode as Locale;\n  }\n\n  // Try to match \"xx-YY\" to existing translation for \"xx-ZZ\" as a last resort\n  // If some match is undesired, it can be overridden in src/i18n.ts `fallbacks` option\n  for (const existing of locales) {\n    if (shortCode == existing.substring(0, 2)) {\n      return existing as Locale;\n    }\n  }\n\n  // should be \"en\", so the selector is not empty if there isn't a translation for current user's language\n  return (i18n.store.options.fallbackLng as FallbackLngObjList).default[0] as Locale;\n};\n\ntype NestedKeyOf<T, K = keyof T> = K extends keyof T & (string | number)\n  ? `${K}` | (T[K] extends object ? `${K}.${NestedKeyOf<T[K]>}` : never)\n  : never;\n\n// Represents the keys of nested translation objects.\nexport type Translations = NestedKeyOf<typeof enTranslation>;\n\n// Represents a typed translation function.\ntype TypedT = (key: Translations, params?: Record<string, unknown>) => string;\n\nexport const useTranslate = (): TypedT => {\n  const { t } = useTranslation<Translations>();\n  return t;\n};\n\nexport const isValidLocale = (locale: string | undefined | null): boolean => {\n  if (!locale) return false;\n  return locales.includes(locale);\n};\n\n// Gets the locale to use with proper priority:\n// 1. User setting (if logged in and has preference)\n// 2. localStorage (from previous session)\n// 3. Browser language preference\nexport const getLocaleWithFallback = (userLocale?: string): Locale => {\n  // Priority 1: User setting (if logged in and valid)\n  if (userLocale && isValidLocale(userLocale)) {\n    return userLocale as Locale;\n  }\n\n  // Priority 2: localStorage\n  const stored = getStoredLocale();\n  if (stored) {\n    return stored;\n  }\n\n  // Priority 3: Browser language\n  return findNearestMatchedLanguage(navigator.language);\n};\n\n// Applies and persists a locale setting\nexport const loadLocale = (locale: string): Locale => {\n  const validLocale = isValidLocale(locale) ? (locale as Locale) : findNearestMatchedLanguage(navigator.language);\n  setStoredLocale(validLocale);\n  i18n.changeLanguage(validLocale);\n  return validLocale;\n};\n\n/**\n * Applies locale early during initial page load to prevent language flash.\n * Uses only localStorage and browser language (no user settings yet).\n */\nexport const applyLocaleEarly = (): void => {\n  const stored = getStoredLocale();\n  const locale = stored ?? findNearestMatchedLanguage(navigator.language);\n  loadLocale(locale);\n};\n\n// Get the display name for a locale in its native language\nexport const getLocaleDisplayName = (locale: string): string => {\n  try {\n    const displayName = new Intl.DisplayNames([locale], { type: \"language\" }).of(locale);\n    if (displayName) {\n      return displayName.charAt(0).toUpperCase() + displayName.slice(1);\n    }\n  } catch {\n    // Intl.DisplayNames might not be available or might fail for some locales\n  }\n  return locale;\n};\n"
  },
  {
    "path": "web/src/utils/markdown-list-detection.ts",
    "content": "export interface ListItemInfo {\n  type: \"task\" | \"unordered\" | \"ordered\" | null;\n  symbol?: string; // For task/unordered lists: \"- \", \"* \", \"+ \"\n  number?: number; // For ordered lists: 1, 2, 3, etc.\n  indent?: string; // Leading whitespace\n}\n\n// Detect the list item type of the last line before cursor\nexport function detectLastListItem(contentBeforeCursor: string): ListItemInfo {\n  const lines = contentBeforeCursor.split(\"\\n\");\n  const lastLine = lines[lines.length - 1];\n\n  // Extract indentation\n  const indentMatch = lastLine.match(/^(\\s*)/);\n  const indent = indentMatch ? indentMatch[1] : \"\";\n\n  // Task list: - [ ] or - [x] or - [X]\n  const taskMatch = lastLine.match(/^(\\s*)([-*+])\\s+\\[([ xX])\\]\\s+/);\n  if (taskMatch) {\n    return {\n      type: \"task\",\n      symbol: taskMatch[2], // -, *, or +\n      indent,\n    };\n  }\n\n  // Unordered list: - foo or * foo or + foo\n  const unorderedMatch = lastLine.match(/^(\\s*)([-*+])\\s+/);\n  if (unorderedMatch) {\n    return {\n      type: \"unordered\",\n      symbol: unorderedMatch[2],\n      indent,\n    };\n  }\n\n  // Ordered list: 1. foo or 2) foo\n  const orderedMatch = lastLine.match(/^(\\s*)(\\d+)[.)]\\s+/);\n  if (orderedMatch) {\n    return {\n      type: \"ordered\",\n      number: parseInt(orderedMatch[2]),\n      indent,\n    };\n  }\n\n  return {\n    type: null,\n    indent,\n  };\n}\n\n// Generate the text to insert when pressing Enter on a list item\nexport function generateListContinuation(listInfo: ListItemInfo): string {\n  const indent = listInfo.indent || \"\";\n\n  switch (listInfo.type) {\n    case \"task\":\n      return `${indent}${listInfo.symbol} [ ] `;\n    case \"unordered\":\n      return `${indent}${listInfo.symbol} `;\n    case \"ordered\":\n      return `${indent}${(listInfo.number || 0) + 1}. `;\n    default:\n      return indent;\n  }\n}\n"
  },
  {
    "path": "web/src/utils/markdown-manipulation.ts",
    "content": "// Utilities for manipulating markdown strings using AST parsing\n// Uses mdast for accurate task detection that properly handles code blocks\n\nimport type { ListItem } from \"mdast\";\nimport { fromMarkdown } from \"mdast-util-from-markdown\";\nimport { gfmFromMarkdown } from \"mdast-util-gfm\";\nimport { gfm } from \"micromark-extension-gfm\";\nimport { visit } from \"unist-util-visit\";\n\ninterface TaskInfo {\n  lineNumber: number;\n  checked: boolean;\n}\n\n// Extract all task list items from markdown using AST parsing\n// This correctly ignores task-like patterns inside code blocks\nfunction extractTasksFromAst(markdown: string): TaskInfo[] {\n  const tree = fromMarkdown(markdown, {\n    extensions: [gfm()],\n    mdastExtensions: [gfmFromMarkdown()],\n  });\n\n  const tasks: TaskInfo[] = [];\n\n  visit(tree, \"listItem\", (node: ListItem) => {\n    // Only process actual task list items (those with a checkbox)\n    if (typeof node.checked === \"boolean\" && node.position?.start.line) {\n      tasks.push({\n        lineNumber: node.position.start.line - 1, // Convert to 0-based\n        checked: node.checked,\n      });\n    }\n  });\n\n  return tasks;\n}\n\nexport function toggleTaskAtLine(markdown: string, lineNumber: number, checked: boolean): string {\n  const lines = markdown.split(\"\\n\");\n\n  if (lineNumber < 0 || lineNumber >= lines.length) {\n    return markdown;\n  }\n\n  const line = lines[lineNumber];\n\n  // Match task list patterns: - [ ], - [x], - [X], etc.\n  const taskPattern = /^(\\s*[-*+]\\s+)\\[([ xX])\\](\\s+.*)$/;\n  const match = line.match(taskPattern);\n\n  if (!match) {\n    return markdown;\n  }\n\n  const [, prefix, , suffix] = match;\n  const newCheckmark = checked ? \"x\" : \" \";\n  lines[lineNumber] = `${prefix}[${newCheckmark}]${suffix}`;\n\n  return lines.join(\"\\n\");\n}\n\nexport function toggleTaskAtIndex(markdown: string, taskIndex: number, checked: boolean): string {\n  const tasks = extractTasksFromAst(markdown);\n\n  if (taskIndex < 0 || taskIndex >= tasks.length) {\n    return markdown;\n  }\n\n  const task = tasks[taskIndex];\n  return toggleTaskAtLine(markdown, task.lineNumber, checked);\n}\n\nexport function countTasks(markdown: string): {\n  total: number;\n  completed: number;\n  incomplete: number;\n} {\n  const tasks = extractTasksFromAst(markdown);\n\n  const total = tasks.length;\n  const completed = tasks.filter((t) => t.checked).length;\n\n  return {\n    total,\n    completed,\n    incomplete: total - completed,\n  };\n}\n\nexport function getTaskLineNumber(markdown: string, taskIndex: number): number {\n  const tasks = extractTasksFromAst(markdown);\n\n  if (taskIndex < 0 || taskIndex >= tasks.length) {\n    return -1;\n  }\n\n  return tasks[taskIndex].lineNumber;\n}\n\nexport interface TaskItem {\n  lineNumber: number;\n  taskIndex: number;\n  checked: boolean;\n  content: string;\n  indentation: number;\n}\n\nexport function extractTasks(markdown: string): TaskItem[] {\n  const tree = fromMarkdown(markdown, {\n    extensions: [gfm()],\n    mdastExtensions: [gfmFromMarkdown()],\n  });\n\n  const lines = markdown.split(\"\\n\");\n  const tasks: TaskItem[] = [];\n  let taskIndex = 0;\n\n  visit(tree, \"listItem\", (node: ListItem) => {\n    if (typeof node.checked === \"boolean\" && node.position?.start.line) {\n      const lineNumber = node.position.start.line - 1;\n      const line = lines[lineNumber];\n\n      // Extract indentation\n      const indentMatch = line.match(/^(\\s*)/);\n      const indentation = indentMatch ? indentMatch[1].length : 0;\n\n      // Extract content (text after the checkbox)\n      const contentMatch = line.match(/^\\s*[-*+]\\s+\\[[ xX]\\]\\s+(.*)/);\n      const content = contentMatch ? contentMatch[1] : \"\";\n\n      tasks.push({\n        lineNumber,\n        taskIndex: taskIndex++,\n        checked: node.checked,\n        content,\n        indentation,\n      });\n    }\n  });\n\n  return tasks;\n}\n"
  },
  {
    "path": "web/src/utils/memo.ts",
    "content": "import { Visibility } from \"@/types/proto/api/v1/memo_service_pb\";\n\nexport const convertVisibilityFromString = (visibility: string) => {\n  switch (visibility) {\n    case \"PUBLIC\":\n      return Visibility.PUBLIC;\n    case \"PROTECTED\":\n      return Visibility.PROTECTED;\n    case \"PRIVATE\":\n      return Visibility.PRIVATE;\n    default:\n      return Visibility.PUBLIC;\n  }\n};\n\nexport const convertVisibilityToString = (visibility: Visibility) => {\n  switch (visibility) {\n    case Visibility.PUBLIC:\n      return \"PUBLIC\";\n    case Visibility.PROTECTED:\n      return \"PROTECTED\";\n    case Visibility.PRIVATE:\n      return \"PRIVATE\";\n    default:\n      return \"PRIVATE\";\n  }\n};\n"
  },
  {
    "path": "web/src/utils/oauth.ts",
    "content": "const STATE_STORAGE_KEY = \"oauth_state\";\nconst STATE_EXPIRY_MS = 10 * 60 * 1000; // 10 minutes\n\ninterface OAuthState {\n  state: string;\n  identityProviderName: string;\n  timestamp: number;\n  returnUrl?: string;\n  codeVerifier?: string; // PKCE code_verifier\n}\n\n// Generate a cryptographically secure random state value\nfunction generateSecureState(): string {\n  const array = new Uint8Array(32);\n  crypto.getRandomValues(array);\n  return Array.from(array, (byte) => byte.toString(16).padStart(2, \"0\")).join(\"\");\n}\n\n// Generate a cryptographically secure random code_verifier for PKCE (RFC 7636)\n// Returns a URL-safe base64 string (43-128 characters)\nfunction generateCodeVerifier(): string {\n  const array = new Uint8Array(32); // 256 bits = 32 bytes\n  crypto.getRandomValues(array);\n  // Convert to base64url (URL-safe base64 without padding)\n  return base64UrlEncode(array);\n}\n\n// Generate code_challenge from code_verifier using SHA-256\nasync function generateCodeChallenge(codeVerifier: string): Promise<string> {\n  const encoder = new TextEncoder();\n  const data = encoder.encode(codeVerifier);\n  const hash = await crypto.subtle.digest(\"SHA-256\", data);\n  return base64UrlEncode(new Uint8Array(hash));\n}\n\n// Base64URL encoding (RFC 4648 base64url without padding)\nfunction base64UrlEncode(buffer: Uint8Array): string {\n  const base64 = btoa(String.fromCharCode(...buffer));\n  return base64.replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\");\n}\n\n// Store OAuth state and PKCE parameters in sessionStorage\n// Returns state and optional codeChallenge for use in authorization URL\n// PKCE is optional - if crypto APIs are unavailable (HTTP context), falls back to standard OAuth\nexport async function storeOAuthState(\n  identityProviderName: string,\n  returnUrl?: string,\n): Promise<{ state: string; codeChallenge?: string }> {\n  const state = generateSecureState();\n\n  // Try to generate PKCE parameters if crypto.subtle is available (HTTPS/localhost)\n  // Falls back to standard OAuth flow if unavailable (HTTP context)\n  let codeVerifier: string | undefined;\n  let codeChallenge: string | undefined;\n\n  try {\n    // Check if crypto.subtle is available (requires secure context: HTTPS or localhost)\n    if (typeof crypto !== \"undefined\" && crypto.subtle) {\n      codeVerifier = generateCodeVerifier();\n      codeChallenge = await generateCodeChallenge(codeVerifier);\n    } else {\n      console.warn(\n        \"PKCE not available: crypto.subtle requires HTTPS. Falling back to standard OAuth flow without PKCE. \" +\n          \"For enhanced security, please access Memos over HTTPS.\",\n      );\n    }\n  } catch (error) {\n    // If PKCE generation fails for any reason, fall back to standard OAuth\n    console.warn(\"Failed to generate PKCE parameters, falling back to standard OAuth:\", error);\n    codeVerifier = undefined;\n    codeChallenge = undefined;\n  }\n\n  const stateData: OAuthState = {\n    state,\n    identityProviderName,\n    timestamp: Date.now(),\n    returnUrl,\n    codeVerifier, // Store for later retrieval in callback (undefined if PKCE not available)\n  };\n\n  try {\n    sessionStorage.setItem(STATE_STORAGE_KEY, JSON.stringify(stateData));\n  } catch (error) {\n    console.error(\"Failed to store OAuth state:\", error);\n    throw new Error(\"Failed to initialize OAuth flow\");\n  }\n\n  return { state, codeChallenge };\n}\n\n// Validate and retrieve OAuth state from storage (CSRF protection)\n// Returns identityProviderName, returnUrl, and codeVerifier for PKCE\nexport function validateOAuthState(stateParam: string): { identityProviderName: string; returnUrl?: string; codeVerifier?: string } | null {\n  try {\n    const storedData = sessionStorage.getItem(STATE_STORAGE_KEY);\n    if (!storedData) {\n      console.error(\"No OAuth state found in storage\");\n      return null;\n    }\n\n    const stateData: OAuthState = JSON.parse(storedData);\n\n    // Check if state has expired\n    if (Date.now() - stateData.timestamp > STATE_EXPIRY_MS) {\n      console.error(\"OAuth state has expired\");\n      sessionStorage.removeItem(STATE_STORAGE_KEY);\n      return null;\n    }\n\n    // Validate state matches (CSRF protection)\n    if (stateData.state !== stateParam) {\n      console.error(\"OAuth state mismatch - possible CSRF attack\");\n      sessionStorage.removeItem(STATE_STORAGE_KEY);\n      return null;\n    }\n\n    // State is valid, clean up and return data\n    sessionStorage.removeItem(STATE_STORAGE_KEY);\n    return {\n      identityProviderName: stateData.identityProviderName,\n      returnUrl: stateData.returnUrl,\n      codeVerifier: stateData.codeVerifier, // Return PKCE code_verifier\n    };\n  } catch (error) {\n    console.error(\"Failed to validate OAuth state:\", error);\n    sessionStorage.removeItem(STATE_STORAGE_KEY);\n    return null;\n  }\n}\n\n// Clean up expired OAuth states (call on app init)\nexport function cleanupExpiredOAuthState(): void {\n  try {\n    const storedData = sessionStorage.getItem(STATE_STORAGE_KEY);\n    if (!storedData) {\n      return;\n    }\n\n    const stateData: OAuthState = JSON.parse(storedData);\n    if (Date.now() - stateData.timestamp > STATE_EXPIRY_MS) {\n      sessionStorage.removeItem(STATE_STORAGE_KEY);\n    }\n  } catch {\n    // If parsing fails, remove the corrupted data\n    sessionStorage.removeItem(STATE_STORAGE_KEY);\n  }\n}\n"
  },
  {
    "path": "web/src/utils/remark-plugins/remark-disable-setext.ts",
    "content": "export function remarkDisableSetext(this: unknown) {\n  const data = (this as { data: () => Record<string, unknown> }).data();\n\n  add(\"micromarkExtensions\", {\n    disable: {\n      null: [\"setextUnderline\"],\n    },\n  });\n\n  function add(field: string, value: unknown) {\n    const list = data[field] ? (data[field] as unknown[]) : (data[field] = []);\n    list.push(value);\n  }\n}\n"
  },
  {
    "path": "web/src/utils/remark-plugins/remark-preserve-type.ts",
    "content": "import type { Root } from \"mdast\";\nimport { visit } from \"unist-util-visit\";\nimport type { ExtendedData } from \"@/types/markdown\";\n\nconst STANDARD_NODE_TYPES = new Set([\"text\", \"root\", \"paragraph\", \"heading\", \"list\", \"listItem\"]);\n\nexport const remarkPreserveType = () => {\n  return (tree: Root) => {\n    visit(tree, (node) => {\n      if (STANDARD_NODE_TYPES.has(node.type)) {\n        return;\n      }\n\n      if (!node.data) {\n        node.data = {};\n      }\n\n      const data = node.data as ExtendedData;\n      data.mdastType = node.type;\n    });\n  };\n};\n"
  },
  {
    "path": "web/src/utils/remark-plugins/remark-tag.ts",
    "content": "import type { Root, Text } from \"mdast\";\nimport type { Node as UnistNode } from \"unist\";\nimport { visit } from \"unist-util-visit\";\nimport type { TagNode, TagNodeData } from \"@/types/markdown\";\n\nconst MAX_TAG_LENGTH = 100;\n\nfunction isTagChar(char: string): boolean {\n  if (/\\p{L}/u.test(char)) {\n    return true;\n  }\n\n  if (/\\p{N}/u.test(char)) {\n    return true;\n  }\n\n  if (/\\p{S}/u.test(char)) {\n    return true;\n  }\n\n  return char === \"_\" || char === \"-\" || char === \"/\" || char === \"&\";\n}\n\nfunction parseTagsFromText(text: string): Array<{ type: \"text\"; value: string } | { type: \"tag\"; value: string }> {\n  const segments: Array<{ type: \"text\"; value: string } | { type: \"tag\"; value: string }> = [];\n\n  const chars = [...text];\n  let i = 0;\n\n  while (i < chars.length) {\n    if (chars[i] === \"#\" && i + 1 < chars.length && isTagChar(chars[i + 1])) {\n      const prevChar = i > 0 ? chars[i - 1] : \"\";\n      const nextChar = i + 1 < chars.length ? chars[i + 1] : \"\";\n\n      if (prevChar === \"#\" || nextChar === \"#\" || nextChar === \" \") {\n        segments.push({ type: \"text\", value: chars[i] });\n        i++;\n        continue;\n      }\n\n      let j = i + 1;\n      while (j < chars.length && isTagChar(chars[j])) {\n        j++;\n      }\n\n      const tagContent = chars.slice(i + 1, j).join(\"\");\n\n      const runeCount = [...tagContent].length;\n      if (runeCount > 0 && runeCount <= MAX_TAG_LENGTH) {\n        segments.push({ type: \"tag\", value: tagContent });\n        i = j;\n        continue;\n      }\n    }\n\n    let j = i + 1;\n    while (j < chars.length && chars[j] !== \"#\") {\n      j++;\n    }\n    segments.push({ type: \"text\", value: chars.slice(i, j).join(\"\") });\n    i = j;\n  }\n\n  return segments;\n}\n\nfunction createTagNode(tagValue: string): TagNode {\n  const data: TagNodeData = {\n    hName: \"span\",\n    hProperties: {\n      className: \"tag\",\n      \"data-tag\": tagValue,\n    },\n    hChildren: [{ type: \"text\", value: `#${tagValue}` }],\n  };\n\n  return {\n    type: \"tagNode\",\n    value: tagValue,\n    data,\n  } as TagNode;\n}\n\nexport const remarkTag = () => {\n  return (tree: Root) => {\n    visit(tree, (node, index, parent) => {\n      if (node.type !== \"text\" || !parent || index === null) return;\n\n      const textNode = node as Text;\n      const text = textNode.value;\n      const segments = parseTagsFromText(text);\n\n      if (segments.every((seg) => seg.type === \"text\")) {\n        return;\n      }\n\n      const newNodes = segments.map((segment) => {\n        if (segment.type === \"tag\") {\n          return createTagNode(segment.value);\n        }\n        return {\n          type: \"text\",\n          value: segment.value,\n        } as Text;\n      });\n\n      if (typeof index === \"number\" && parent) {\n        (parent.children as UnistNode[]).splice(index, 1, ...(newNodes as UnistNode[]));\n      }\n    });\n  };\n};\n"
  },
  {
    "path": "web/src/utils/theme.ts",
    "content": "import defaultDarkThemeContent from \"../themes/default-dark.css?raw\";\nimport paperThemeContent from \"../themes/paper.css?raw\";\n\n// ============================================================================\n// Types and Constants\n// ============================================================================\n\nconst VALID_THEMES = [\"system\", \"default\", \"default-dark\", \"paper\"] as const;\n\nexport type Theme = (typeof VALID_THEMES)[number];\nexport type ResolvedTheme = Exclude<Theme, \"system\">;\n\nexport interface ThemeOption {\n  value: string;\n  label: string;\n}\n\nconst STORAGE_KEY = \"memos-theme\";\nconst STYLE_ELEMENT_ID = \"instance-theme\";\n\nconst THEME_CONTENT: Record<ResolvedTheme, string | null> = {\n  default: null,\n  \"default-dark\": defaultDarkThemeContent,\n  paper: paperThemeContent,\n};\n\nconst THEME_COLORS: Record<ResolvedTheme, string> = {\n  default: \"#faf9f5\",\n  \"default-dark\": \"#020204\",\n  paper: \"#f5ede4\",\n};\n\nexport const THEME_OPTIONS: ThemeOption[] = [\n  { value: \"system\", label: \"Sync with system\" },\n  { value: \"default\", label: \"Light\" },\n  { value: \"default-dark\", label: \"Dark\" },\n  { value: \"paper\", label: \"Paper\" },\n];\n\n// ============================================================================\n// Theme Validation and Detection\n// ============================================================================\n\n/**\n * Validates and normalizes a theme string to a valid theme.\n * Falls back to \"default\" for invalid themes.\n */\nconst validateTheme = (theme: string): Theme => {\n  return VALID_THEMES.includes(theme as Theme) ? (theme as Theme) : \"default\";\n};\n\n/**\n * Detects the system's preferred color scheme.\n * @returns \"default-dark\" for dark mode, \"default\" for light mode\n */\nexport const getSystemTheme = (): ResolvedTheme => {\n  if (typeof window !== \"undefined\" && window.matchMedia?.(\"(prefers-color-scheme: dark)\").matches) {\n    return \"default-dark\";\n  }\n  return \"default\";\n};\n\n/**\n * Resolves \"system\" theme to the actual theme based on OS preference.\n * Other themes are returned as-is after validation.\n */\nexport const resolveTheme = (theme: string): ResolvedTheme => {\n  const validTheme = validateTheme(theme);\n  return validTheme === \"system\" ? getSystemTheme() : validTheme;\n};\n\n// ============================================================================\n// LocalStorage Helpers\n// ============================================================================\n\n/**\n * Safely reads the theme from localStorage.\n * @returns The stored theme, or null if not found or unavailable\n */\nconst getStoredTheme = (): Theme | null => {\n  try {\n    const stored = localStorage.getItem(STORAGE_KEY);\n    return stored && VALID_THEMES.includes(stored as Theme) ? (stored as Theme) : null;\n  } catch {\n    return null;\n  }\n};\n\n/**\n * Safely stores the theme to localStorage.\n */\nconst setStoredTheme = (theme: Theme): void => {\n  try {\n    localStorage.setItem(STORAGE_KEY, theme);\n  } catch {\n    // localStorage might not be available (SSR, private browsing, etc.)\n  }\n};\n\n// ============================================================================\n// Theme Selection with Fallbacks\n// ============================================================================\n\n/**\n * Gets the theme for initial page load (before user settings are available).\n * Priority: localStorage -> system preference\n */\nexport const getInitialTheme = (): Theme => {\n  return getStoredTheme() ?? \"system\";\n};\n\n/**\n * Gets the theme with full fallback chain.\n * Priority:\n * 1. User setting (if logged in and has preference)\n * 2. localStorage (from previous session)\n * 3. System preference\n */\nexport const getThemeWithFallback = (userTheme?: string): Theme => {\n  // Priority 1: User setting\n  if (userTheme && VALID_THEMES.includes(userTheme as Theme)) {\n    return userTheme as Theme;\n  }\n\n  // Priority 2: localStorage\n  const stored = getStoredTheme();\n  if (stored) {\n    return stored;\n  }\n\n  // Priority 3: System preference\n  return \"system\";\n};\n\n// ============================================================================\n// DOM Manipulation\n// ============================================================================\n\n/**\n * Removes the existing theme style element from the DOM.\n */\nconst removeThemeStyle = (): void => {\n  document.getElementById(STYLE_ELEMENT_ID)?.remove();\n};\n\n/**\n * Injects theme CSS into the document head.\n * Skips injection for the default theme (uses base CSS).\n */\nconst injectThemeStyle = (theme: ResolvedTheme): void => {\n  removeThemeStyle();\n\n  if (theme === \"default\") {\n    return; // Use base CSS for default theme\n  }\n\n  const css = THEME_CONTENT[theme];\n  if (css) {\n    const style = document.createElement(\"style\");\n    style.id = STYLE_ELEMENT_ID;\n    style.textContent = css;\n    document.head.appendChild(style);\n  }\n};\n\n/**\n * Sets the data-theme attribute on the document element.\n * This allows CSS to react to the current theme.\n */\nconst setThemeAttribute = (theme: ResolvedTheme): void => {\n  document.documentElement.setAttribute(\"data-theme\", theme);\n};\n\n/**\n * Updates the theme-color meta tag to match the current theme background.\n * This colors the browser/status bar on mobile devices.\n */\nconst updateThemeColorMeta = (theme: ResolvedTheme): void => {\n  const meta = document.querySelector<HTMLMetaElement>('meta[name=\"theme-color\"]');\n  if (meta) {\n    meta.content = THEME_COLORS[theme];\n  }\n};\n\n// ============================================================================\n// Main Theme Loading\n// ============================================================================\n\n/**\n * Loads and applies a theme.\n * This function:\n * 1. Validates the theme\n * 2. Resolves \"system\" to actual theme\n * 3. Injects theme CSS\n * 4. Sets data-theme attribute\n * 5. Persists to localStorage\n */\nexport const loadTheme = (themeName: string): void => {\n  const validTheme = validateTheme(themeName);\n  const resolvedTheme = resolveTheme(validTheme);\n\n  injectThemeStyle(resolvedTheme);\n  setThemeAttribute(resolvedTheme);\n  updateThemeColorMeta(resolvedTheme);\n  setStoredTheme(validTheme); // Store original theme preference (not resolved)\n};\n\n/**\n * Applies theme early during initial page load to prevent FOUC.\n * Uses only localStorage and system preference (no user settings yet).\n */\nexport const applyThemeEarly = (): void => {\n  const theme = getInitialTheme();\n  loadTheme(theme);\n};\n\n// ============================================================================\n// System Theme Listener\n// ============================================================================\n\n/**\n * Sets up a listener for OS-level theme preference changes.\n * Supports both modern (addEventListener) and legacy (addListener) APIs.\n *\n * @param onThemeChange - Callback invoked when system theme changes\n * @returns Cleanup function to remove the listener\n */\nexport const setupSystemThemeListener = (onThemeChange: () => void): (() => void) => {\n  // Guard against SSR\n  if (typeof window === \"undefined\" || !window.matchMedia) {\n    return () => {};\n  }\n\n  const mediaQuery = window.matchMedia(\"(prefers-color-scheme: dark)\");\n\n  // Modern API (preferred)\n  if (mediaQuery.addEventListener) {\n    mediaQuery.addEventListener(\"change\", onThemeChange);\n    return () => mediaQuery.removeEventListener(\"change\", onThemeChange);\n  }\n\n  // Legacy API (Safari < 14)\n  if (mediaQuery.addListener) {\n    mediaQuery.addListener(onThemeChange);\n    return () => mediaQuery.removeListener(onThemeChange);\n  }\n\n  return () => {};\n};\n"
  },
  {
    "path": "web/src/utils/user.ts",
    "content": "import { User, User_Role } from \"@/types/proto/api/v1/user_service_pb\";\n\nexport const isSuperUser = (user: User | undefined) => {\n  return user && user.role === User_Role.ADMIN;\n};\n"
  },
  {
    "path": "web/src/utils/uuid.ts",
    "content": "import { v4 as uuidv4 } from \"uuid\";\n\nexport const generateUUID = () => {\n  return uuidv4();\n};\n"
  },
  {
    "path": "web/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ESNext\",\n    \"lib\": [\"DOM\", \"DOM.Iterable\", \"ESNext\"],\n    \"types\": [\"vite/client\"],\n    \"allowJs\": false,\n    \"skipLibCheck\": false,\n    \"esModuleInterop\": false,\n    \"allowSyntheticDefaultImports\": true,\n    \"strict\": true,\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"Node\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"noEmit\": true,\n    \"jsx\": \"react-jsx\",\n    \"paths\": {\n      \"@/*\": [\"./src/*\"]\n    }\n  },\n  \"include\": [\"./src\"]\n}\n"
  },
  {
    "path": "web/vite.config.mts",
    "content": "import react from \"@vitejs/plugin-react\";\nimport { resolve } from \"path\";\nimport { defineConfig } from \"vite\";\nimport tailwindcss from \"@tailwindcss/vite\";\n\nlet devProxyServer = \"http://localhost:8081\";\nif (process.env.DEV_PROXY_SERVER && process.env.DEV_PROXY_SERVER.length > 0) {\n  console.log(\"Use devProxyServer from environment: \", process.env.DEV_PROXY_SERVER);\n  devProxyServer = process.env.DEV_PROXY_SERVER;\n}\n\n// https://vitejs.dev/config/\nexport default defineConfig({\n  plugins: [react(), tailwindcss()],\n  server: {\n    host: \"0.0.0.0\",\n    port: 3001,\n    proxy: {\n      \"^/api/v1/sse\": {\n        target: devProxyServer,\n        xfwd: true,\n        // SSE requires no response buffering and longer timeout.\n        timeout: 0,\n      },\n      \"^/api\": {\n        target: devProxyServer,\n        xfwd: true,\n      },\n      \"^/memos.api.v1\": {\n        target: devProxyServer,\n        xfwd: true,\n      },\n      \"^/file\": {\n        target: devProxyServer,\n        xfwd: true,\n      },\n    },\n  },\n  resolve: {\n    alias: {\n      \"@/\": `${resolve(__dirname, \"src\")}/`,\n    },\n  },\n  build: {\n    rollupOptions: {\n      output: {\n        manualChunks: {\n          \"utils-vendor\": [\"dayjs\", \"lodash-es\"],\n          \"mermaid-vendor\": [\"mermaid\"],\n          \"leaflet-vendor\": [\"leaflet\", \"react-leaflet\"],\n        },\n      },\n    },\n  },\n});\n"
  }
]